
1. 为什么一个“简单指标”需要一套专属基础设施1.1 RSI策略远不是“算个指标”那么简单我第一次意识到RSI这套系统也需要正经的infra框架是在一次实盘事故之后。当时团队已经用几十行Python把RSI指标算得滚瓜烂熟回测曲线也漂亮但一接上实盘数据源就各种对不上同一根K线行情端显示已收盘策略端却还在等最后一笔tick回测里信号在收盘瞬间触发实盘里却因为计算延迟晚了两秒才发单结果滑点吃掉大半个点。先说清楚RSI是什么。相对强弱指标的计算公式很简单取N个周期内的平均上涨幅度和平均下跌幅度算出相对强弱值RS再映射成0到100的RSI。默认参数N14用Wilder的平滑方式。正因为公式简单很多人觉得“这有什么好做框架的”这也是后来踩坑的根源——指标简单不代表承载它的系统简单。真正复杂的是指标外面的那层东西行情数据怎么接入、以什么粒度接入K线收盘一瞬间信号该由谁触发、触发后走什么路径回测用的数据流和实盘用的数据流是否完全一致一个参数网格几百组组合在跑会不会把下单进程拖死数据源断流五分钟策略是停下来还是继续盲跑。这些问题的复杂度叠加起来指数级上升。reef这个名字的由来很简单R打头对应RSIreef本身有“珊瑚礁”的意思。珊瑚礁是海洋生态的基座我希望这个框架像珊瑚礁一样给RSI策略提供一个稳定栖居的环境。它不直接替策略做判断而是把行情接入、指标计算、信号生成、风控、执行、追踪这件整套链路固定下来让策略开发者只写“RSI大于70且持续回落就平多”这种业务逻辑其余全部交给框架。1.2 通用量化框架在RSI场景下的尴尬处境可能有人问业界现成的量化框架那么多backtrader、vn.py、甚至直接pandastushare为什么还要自己搭我的结论是这些工具解决“算指标”没有问题但不解决“跑系统”的问题。pandas算RSI确实快几十行代码加talib就出曲线但指标算完怎么办谁负责在K线收盘一刻把指标值送进策略策略想下单有没有统一的接口抽象回测写一套数据读取逻辑实盘又写一套两边怎么保证一致这些事pandas一概不管。backtrader和vn.py这类完整框架功能全面事件驱动循环、回测引擎、实盘接口都有。但问题在另一方面它们的抽象层次是给“各种策略”设计的为了容纳均线、MACD、布林带、多品种套利等所有场景框架内部非常重。RSI策略作为最轻量的策略类型之一塞进这种重型框架里反而被层层包装裹住了手脚。尤其当你需要接入公司自己的行情源、自己的柜台、自己的风控规则时把这些东西硬塞进通用框架的模型里往往要写不少适配代码比从零写一个薄层还费劲。另一个现实问题是回测与实盘的一致性。很多通用框架为了保证“开箱即用”回测走的是向量化批量计算实盘走的是事件驱动逐tick触发两套代码路径天然不一致。在RSI这种对“信号触发时机”极其敏感的策略上这种不一致几乎是致命的。所以reef从一开始就不想做一个万能量化平台而是只解决一类问题以RSI为核心信号的系统怎么把从行情到订单的整条链路做得干净、稳定、可观测。1.3 先划清楚边界什么该做什么坚决不做做infra框架最容易犯的错是过度设计。既然叫框架就总觉得要支持分布式、支持流式计算、支持所有品种所有周期结果做到最后变成一个大而空的壳谁都用不起来。reef在设计之初就把需求边界写死在文档里核心原则就三条只做“链路”不做“策略”。策略逻辑必须由使用方提供框架只提供注册、调度、执行与隔离能力。只做中低频。目标场景是分钟级、小时级乃至日级K线驱动的信号不追求微秒级高频。一切为可观测性让路。每个信号、每笔委托都要能追踪到完整的生命周期。边界外的东西比如机器学习模型训练、组合优化、资产定价reef一概不管。这样做的效果是框架很薄每个模块都能被工程师完整读透、改动、扩展。范围内范围外行情接入与标准化策略因子研发K线指标计算与缓存机器学习/深度学习模型策略注册、调度与生命周期管理资产组合与资金分配算法信号风控、熔断与权限控制高频交易链路执行网关抽象与委托追踪数据库与存储引擎监控、日志、链路追踪统一账户/结算系统边界清楚之后架构选型变得异常容易。不需要消息队列就用进程内事件总线不需要分布式就用多进程加共享状态所有设计都是“够用就好但够用的标准很高”。2. reef的核心架构拆解从行情到订单的完整链路2.1 一条信号从生成到成交的完整旅程如果把reef比作一条流水线那么一个信号要经过六个工位才最终变成一笔真实委托行情采集器从数据源拉取最新K线转换成统一的内部数据结构。标准化后的K线进入指标计算引擎更新该品种、该周期对应的RSI状态。策略容器在每一个K线闭合事件被唤醒读取最新的指标值运行用户定义的规则产出“信号对象”。信号对象先过风控层是否在交易时段、是否触发熔断、仓位是否超限。通过风控的信号交给执行网关由网关调用具体的交易接口下单。委托回报成交、拒绝、部分成交异步回传与信号ID关联写入追踪日志。听起来很常规但每个环节都有魔鬼细节。比如第1步和第2步之间行情数据的时间戳必须精确到“第几根K线”因为RSI是有状态的指标上一根K线的平滑值会影响下一根如果数据流里有重复推送或乱序推送指标状态就会悄悄飘掉等发现时回测和实盘的差异已经大到完全没法解释。2.2 行情接入层统一数据模型是地基reef的数据模型从一开始就固定下来所有行情源最终都要转换成下面这个结构字段说明symbol品种或合约代码统一格式如btc.usdt.perpexchange交易所或数据源名称timeframeK线周期枚举格式如1m、5m、1htsK线开盘时间戳精确到毫秒open / high / low / close四个价格volume成交量有人可能会问为什么不用现成的数据格式标准。我的经验是宁可自己定义一个极简格式也别直接依赖某个数据厂商的字段风格。因为数据源会换字段会改直接在策略层引用第三方格式等于把外部风险种进了核心链路。reef的做法是数据源适配器负责转换上游爱怎么变都行内部永远是一个稳定模型。技术上reef同时支持主动拉取和被动订阅两种模式。像股票日线这种低频数据每分钟拉一次就够像分钟级K线一般行情源都有订阅推送订阅模式延迟更低。这里有个容易被忽略的细节推送过来的K线在未闭合之前high、low、close都是变动的reef会在K线状态里显式标记是live还是closed指标引擎只对closed状态的数据做结算。这个标记就是回测一致性的第一道保障。2.3 指标计算引擎RSI的口径统一与增量缓存RSI虽然公式简单工程实现上却有两个容易出问题的点平滑算法口径、增量计算状态。先说口径。很多平台默认用简单移动平均SMA来算RSI而经典RSI用的是Wilder平滑本质上是带特定权重的指数平均。同一个14周期RSI用SMA和Wilder算出来的曲线差异不大但拐点可能差一根到两根K线对信号触发时刻影响很大。reef把这套东西收敛了所有指标的平滑算法都由计算引擎统一实现配置文件里显式声明用的是哪种平滑不允许策略层自己再造一个。# reef指标引擎内部的核心思路示意 class RSIState: def __init__(self, period, smoothingwilder): self.period period self.smoothing smoothing self.avg_gain 0.0 self.avg_loss 0.0 self.warmup_remaining period # 温启动期 def update(self, bar): change bar.close - bar.prev_close gain max(change, 0.0) loss max(-change, 0.0) if self.warmup_remaining 0: # 温启动期内用简单平均累积 self.avg_gain (self.avg_gain * (self.warmup_remaining - 1) gain) / self.warmup_remaining self.avg_loss (self.avg_loss * (self.warmup_remaining - 1) loss) / self.warmup_remaining self.warmup_remaining - 1 return None # 正式期用Wilder平滑 self.avg_gain (self.avg_gain * (self.period - 1) gain) / self.period self.avg_loss (self.avg_loss * (self.period - 1) loss) / self.period return rsi_from_avg(self.avg_gain, self.avg_loss)再说增量计算。RSI是有状态的指标它的当前值依赖历史平均值所以必须为每个“品种周期参数”的组合维护一份独立状态。reef的指标引擎为此维护一个状态池K线到了就按索引找到对应状态、更新、写回避免每次全量重算。这个看似不起眼的优化在几百个品种、几十种参数组合下能省掉九成以上的计算量。每次指标值更新还会附带一个版本号策略容器读到版本号变化才知道“有新值可用了”这就把“数据到达”和“策略被唤醒”两件事解耦开避免策略在K线还没闭合时就被半截数据唤醒——那个问题在实盘里非常致命我后面会专门讲。3. 策略工厂让“调参数”和“上实盘”之间不再隔着鸿沟3.1 把RSI策略抽象成四要素RSI策略形态其实非常规整。不管怎么变基本都是这四件事在哪些品种上做、看哪个周期的RSI、满足什么条件进场、满足什么条件离场。reef就按这个抽象来设计策略定义一个策略实例用一段声明式配置就能描述不需要写代码。# 一个简单的RSI策略配置示例 strategy_id: rsi_reversal_btc_1h symbols: - btc.usdt.perp timeframe: 1h params: rsi_period: 14 oversold: 30 overbought: 70 entry_rules: - when: rsi 30 and rsi_cross_up(30) # 超卖回升 action: open_long - when: rsi 70 and rsi_cross_down(70) # 超买回落 action: open_short exit_rules: - when: rsi 55 and position ! 0 # 多头离场 action: close_long - when: rsi 45 and position ! 0 # 空头离场 action: close_short risk: max_position_ratio: 0.05 daily_max_loss_ratio: 0.02把策略做成配置的意义不只是少写代码。配置是可被校验、可被版本化、可被非工程人员review的。我在很多团队里见过一种情况策略逻辑写在两个人共用的Python文件里改来改去最后谁都不知道线上跑的是哪一版。配置化的策略定义配合Git版本管理每次改动都有一个commit记录出了问题可以精确回溯。当然配置化的代价是表达力受限。reef的做法是提供了“声明式规则少量自定义钩子”的组合九成策略用配置搞定剩下的通过注册自定义回调函数来处理但回调函数必须明确声明它会访问哪些数据框架据此做依赖分析和并发调度。3.2 回测与实盘一致性我最看重的一条设计如果只让我保留reef里的一条设计原则那就是“回测和实盘必须走同一条代码路径”。听上去是常识但大量系统做不到就是因为回测为了追求速度用了向量化计算等指标序列全部算完再统一找信号实盘则一笔K线一笔K线往前推边算边判断。两条路径的差异造就了无数“回测很美好实盘全是泪”的故事。reef的做法是回测本质上就是实盘的事件流回放。行情数据按时间顺序一根一根喂给同一个引擎每根K线触发同样的“闭合事件”走同样的指标更新、同样的规则判定、同样的风控检查。唯一的区别是执行网关回测时用模拟撮合实盘时用真实柜台。这样设计必然牺牲一些回测速度但换来的是极高的可信度。因为你在回测里看到的每一笔信号在实盘里会以完全相同的逻辑触发你在实盘里遇到的问题也能用回测数据完整复现出来。这比回测速度重要得多。还有一个常被忽略的细节手续费与滑点模型。RSI策略属于“低胜率、靠盈亏比取胜”的类型如果回测里不真实地加入手续费和滑点很容易得出一个在实盘里根本不成立的结论。reef的模拟撮合模块默认按对手价成交并支持配置固定滑点或比例滑点宁可把回测结果做得保守一点也不要让它跑得太好看。3.3 参数网格研究与资源调度RSI策略的研究工作本质上是参数网格搜索周期取10、12、14、16、18超买超卖阈值取20/80、25/75、30/70循环组合。单品种还好一旦品种扩展到几十个组合数量就是几百上千单线程跑一遍要很久。reef把参数研究做成了一等公民。策略配置里可以声明参数搜索空间框架自动生成组合任务用本地进程池并行跑每个任务的结果写回研究数据库。这里有两个工程细节一是中间结果缓存。比如不同RSI周期组合共享同一个品种的K线数据数据加载、标准化这些只做一次指标计算按参数组合做增量复用避免每次重算全部历史。二是资源隔离。参数研究是计算密集任务实盘是延迟敏感任务两者绝不能混在同一个进程里抢CPU。reef默认把实盘引擎绑定在独占CPU核上研究任务最多用剩余核的80%并且可以通过配置限制并行度防止研究把机器打满导致实盘卡顿。4. 实盘里最容易翻车的地方我的踩坑复盘4.1 K线闭合时刻与信号生成时机差一秒结果天差地别这是我在实盘里踩的最大的一个坑必须放在最前面说。RSI的计算依赖收盘价。这就带来一个天然约束在一根K线还没走完的时候它的收盘价是未知的如果你在K线中途用当时的“最新价”去算RSI算出来的值根本不该被当成这根K线的RSI。很多开源示例代码根本不区分这一点拿最新的close一路算下去回测时因为数据本身就是完整的看不出问题实盘里却会提前触发信号或者永远不触发。reef的做法是强制“闭合事件”机制一根K线只有在收到该周期结束时间戳之后才会被标记为closed并触发指标结算与策略唤醒。举个例子分钟级K线09:30:00这根K线正常情况下到09:30:59的最后一个tick才算闭合reef会等到行情源推送的K线状态变为closed、或者本地时钟越过该K线结束时间且数据完整才正式计算信号。实盘里宁可信号慢一两秒出来也不能用不完整的数据抢跑。因为这个特性reef要求行情源必须支持K线状态标记不支持的话数据接入层就要靠本地时钟做兜底判断同时校验该K线的最后tick时间戳是否已经落入下一个K线区间防止重复触发或漏触发。4.2 数据断流与重连宁可错过不可乱做行情断流是常态不是异常。网络抖动、数据源限流、服务器维护原因千奇百怪。问题不在断流本身而在断流后策略的行为。我见过一种很危险的处理方式断流恢复后数据源会补推历史K线系统收到补推数据后直接把指标引擎状态“校正”一遍然后立刻生成信号。这看起来没问题实际上有两个隐患一是补推数据可能是乱序到达校正时覆盖了已经结算过的指标状态导致后续信号错乱二是断流期间市场可能已经发生了剧烈变化恢复瞬间用“事后数据”生成的信号等于拿着未来信息做事后诸葛亮。reef的默认策略是“宁可错过不可乱做”断流超过阈值后相关品种的指标引擎进入“降级模式”不再生成新信号但已持仓的品种继续执行止损类保护逻辑恢复后先校验数据连续性发现缺口超过规定数值就执行数据重同步重同步完成后手动或按策略规则决定是否恢复交易。这套机制的本质是承认自己有一段时间是“盲”的盲的时候不去做新的判断只做最保守的保护。注意断流期间的“保护逻辑”也要谨慎设计。如果你设定断流超过5分钟就强制平仓那行情源一个普通维护就可能导致你频繁割肉。实际落地时我把保护逻辑分成了两档短断流只暂停开仓长断流或触发行情源故障标记后才考虑减仓。4.3 连续止损后的自我保护熔断机制不能省RSI策略经常在震荡市里反复挨打连续小止损是常态。真正危险的不是单次止损而是连续止损带来的“手感变形”——人会在连续亏损后放大仓位想回本程序虽然不会情绪化但系统的风控参数如果不够刚性同样会越亏越重。reef内置了两级熔断策略级和账户级。策略级熔断指单个策略实例在滚动N个交易日内亏损超过阈值自动停止该策略的开仓动作只保留已持仓的离场逻辑账户级熔断则更刚性整个账户当日亏损比例触及阈值后所有策略只平不开直到人工确认解除。这里有个容易忽略的点熔断阈值应该由谁来定义。我发现如果把阈值写在框架配置里与策略代码混在一起实盘跑一段时间后很容易被“临时调整”得面目全非。reef的做法是把熔断规则放到与行情、指标同一层级的独立风控模块策略层没有权限修改风控参数只能查询当前状态。这样才能保证风控不被策略侧的“我觉得还能扛一下”心态绕过。4.4 全链路追踪让每个信号都有“身份证”实盘系统排查问题最痛苦的是什么是信号说发了但委托端没收到或者委托下了但被柜台拒了你翻日志发现格式不对又不知道是哪一条信号出的问题。归根结底是缺少一条贯穿全链路的关联ID。reef从第一版开始就要求每个信号生成时分配一个全局唯一的signal_idUUID这个ID伴随信号对象走完整个生命周期——风控检查、执行网关、委托回报、成交回报每一环的日志、指标、异常消息都必须带上这个ID。这样排查问题时只需要知道一个信号ID就能用一条命令拉出它经历的每一个环节、每一次状态变更、每一行相关日志。追踪字段内容示例signal_id唯一信号ID全链路关联键strategy_id策略实例IDsymbol品种directionlong / short / closetrigger_time信号生成时间risk_checkpassed / blocked 原因order_sent是否已发送委托order_statusfilled / rejected / partiallatency_ms从信号到委托的耗时这个设计的成本几乎为零收益却是指数级的。我强烈建议任何自己搭交易系统的人哪怕不做框架也要先做这一件事给信号发ID让ID贯穿始终。你会感谢自己的。5. 部署形态与性能表现从单机到多节点的演进5.1 三种典型部署形态reef不强制要求分布式这是刻意设计的。不同阶段的团队资源条件和稳定性要求完全不同一刀切要求所有人上Kubernetes反而是负担。第一种形态是单机部署。策略研发、回测研究、小资金实盘一台8核16G的服务器完全够用。行情数据进内存SQLite存配置和研究结果进程内事件总线连接各模块。优点是部署简单、排查方便缺点是单点故障重启期间信号会缺失。第二种形态是服务化部署适合策略数量多、品种多的团队。把行情接入、指标引擎、策略容器、执行网关拆成独立服务用消息队列连接可以在不重启的情况下单独升级某个模块。这个阶段需要的运维能力开始变高但还不至于上Kubernetes用Docker Compose或简单的systemd管理都能满足。第三种形态是多节点高可用。追求实盘稳定性的团队会把行情接入和策略决策做双活热备主节点故障时备节点在秒级内接管。成本高依赖复杂一般资金规模足够大才需要。部署形态适用阶段优点代价单机研究、小资金简单、直观单点故障服务化多策略并行模块独立升级、扩缩容运维复杂度上升多节点热备大资金实盘高可用、故障秒级切换基础设施成本高我的建议是先单机跑通全链路确认回测实盘一致性没有问题再上服务化热备形态等技术债攒够了再考虑。基础设施的复杂度应该跟着资金规模走而不是跟着想象走。5.2 实测性能与容量参考在测试环境8核16G容器LinuxPython 3.10上reef处理512个品种的1分钟K线流式计算每个品种默认维护3组不同周期参数的RSI状态单节点信号链路P99延迟控制在80ms以内CPU使用率平均约40%内存峰值约2.5G。如果把参数组合扩大到3000组对应大规模参数研究任务单节点能在20分钟左右完成100个品种、24个月1分钟K线的回测任务。这些数字只是参考不能直接对标到你的环境里因为行情源延迟、策略复杂度、日志输出量都会影响结果。但一个总体的结论是稳定的对分钟级中低频策略来说性能从来不是瓶颈瓶颈都在数据质量和链路一致性上。所以当有人跟你说“我们的框架性能很强”时先问回测实盘一致性做得好不好再问性能。5.3 可观测性没有监控的实盘等于裸奔实盘系统上线第一天第一件要做的不是加策略而是加监控。reef默认暴露三类指标信号类信号生成数、信号被风控拦截数、信号到委托的平均延迟。委托类委托成功率、拒绝原因分布、成交耗时。系统类行情推送延迟、指标引擎CPU耗时、内存水位。前两类用来自查交易链路是否正常第三类用来判断系统本身是否健康。加上prometheus和Grafana十分钟就能出一个像样的监控面板。告警规则里我最看重两条信号生成数量突然归零——这往往意味着行情断流或策略容器卡死委托拒绝率突然上升——这往往意味着风控阈值被触发或柜台接口异常。监控不是事后诸葛它的价值在于让异常在影响扩大之前被看见。一次盘中沉默故障如果没人盯着可能一整天都在空转等收盘复盘才发现策略根本没运行。这种故障比亏钱还可怕因为你会误以为系统在正常交易。6. 如果让我重做一遍我会升级这些设计最后聊点实在的体会给想自己搭框架或者正在搭框架的人一些参考。第一数据层要投入最多精力。指标计算、策略逻辑、执行接口都是成熟的真正天天出问题的就是数据质量。重复推送、乱序推送、时间戳越界、补推数据没有标记每一样都能让策略悄悄跑偏。reef后来加了一个“数据体检”模块每次启动前对最近一段行情做完整性校验反复出现异常的数据源在接入层就被标记降级不让脏数据进入指标引擎。这个模块看似不起眼实际上省掉了无数个深夜排查事故的时间。第二回测一致性比回测速度重要一个量级。很多团队花大力气优化回测性能甚至用GPU跑指标计算但回测和实盘走两套代码结果回测再快、再准也没有意义。先把同一条代码路径打通再谈速度优化。性能不够可以通过缓存、并发、更高效的数据结构解决路径不一致的问题最后只有推倒重来一条路。第三不要一开始就上分布式。分布式是复杂性放大器不是解决方案。单机、多进程、事件总线这种朴素架构足够支撑到千万级资金、上百个策略实例的规模。等真正出现“单个进程扛不住”或“需要跨机房容灾”的需求时再演进基础设施跟着需求走反过来是灾难。第四监控和日志要先于业务功能上线。reef第一版连策略容器都还没完全做完信号追踪和监控面板就上线了。因为你知道一个实盘系统一定会出问题问题不可怕可怕的是出问题之后找不到线索。全链路追踪这件事越早做成本越低以后补的代价是你永远补不齐全。第五风控参数要独立于策略代码管理。把“人能决定是否交易”和“程序能决定是否交易”分开看起来有点反直觉但实盘磨几年你就知道程序化的最强优势就是不受情绪影响。风控规则如果可以被策略侧随便改这个优势就被抹掉了。reef里风控模块对策略层是只读的这条设计我至今觉得是框架里最值得保留的决定之一。reef到现在还在迭代但核心设计稳定很久了链路清晰、状态可追踪、边界明确。框架的价值不在复杂在于让复杂的事情变简单、让简单的事情变可靠。如果你的RSI系统也正挣扎在“回测和实盘对不上”“数据断流后策略乱跑”“信号和委托对不上账”这些问题里希望这篇梳理能给你一些参考至少让你知道这些问题有人踩过、也有解法。