先把原话贴出来:“我想搞一个自动筛选股票的RSI工具,得能自己验证。”这就是整个项目的起点,连标点符号都算上不到三十个字。当时的项目代号先定为Kekkai,灵感来自某部经典动画里的结界设定:信号只有突破屏障才有资格进入候选池。差不多两周之后,我把它做成了一条从原始行情到回测报告的完整流水线,也就是标题里写的“可验证的闭环”。
写这篇文章,是因为我意识到这种经历太典型了:绝大多数人拿到一句模糊需求,第一反应是直接开干,结果干到一半发现到处都是坑。RSI-Kekkai这个项目好就好在它足够小,小到一个人两周能做完,但又足够完整,几乎覆盖了从需求分析到验证交付的全部环节。无论你是想给自己搭一套量化研究工具,还是纯粹想看看“一句模糊的需求到底怎么变成能运行的东西”,这篇文章都值得读完。我会把拆解思路、指标原理、系统设计、验证闭环和踩坑记录全部摊开讲,尽量让没有量化背景的人也能看懂。
1. 从模糊需求到项目定位:把“一句话”拆成能落地的任务
1.1 原始需求到底模糊在哪里
先看看最初的需求文本里有什么:自动筛选、股票、RSI工具、能自己验证。四个词,四个信息点。但真正动手的时候会发现,这四个点全是“悬空的”。
自动筛选,筛选什么标的?全市场四千多只股票,还是某个指数成分股?筛选出来的结果是什么形态——一个列表、一条信号、还是一份报告?RSI工具,这里的RSI到底是指经典的14周期相对强弱指标,还是某个变体?参数要不要可调?能自己验证,“验证”这个词最含糊——你是指验证代码算得对不对,还是验证这个筛选策略能不能赚钱?这两个“验证”差着十万八千里。
信息不足不可怕,可怕的是意识不到信息不足。很多人拿到这种需求直接翻资料开始写代码,写到一半才发现方向偏了。我的做法是先用一段话把项目边界钉死:做一个针对日线行情数据的RSI筛选工具,输入某交易所的日线行情,输出满足超卖反转条件的候选股票列表,并且能通过历史回测验证该筛选规则的绩效表现。这一句话看起来很简单,但每个词都在消除歧义。
接下来是第二层拆解。既然要做回测验证,那数据结构和输出口径就得先定。数据结构我选了按股票代码分文件的本地CSV形式,每列固定为日期、开盘、收盘、最高、最低、成交量。输出口径也不复杂:每轮筛选结束后,在报告里附带当轮的行情窗口、规则参数、标的池大小和绩效指标。不要小看这个“输出口径”,它决定了后期验证闭环能不能做起来——没有完整的上下文记录,你根本没法复现一次实验结果。
1.2 我如何定义“可验证”这个关键词
项目成败的关键,其实就压在“可验证”三个字上。我当时的理解是:任何输出都必须能被第三方复现,必须经得起反向核对。于是立了三条原则。
第一,数据可追溯。所有计算基于的原始数据必须留档,哪怕后来数据源更新了,旧文件也保留一份,回测到哪个日期就能拿出来复核。第二,结果可复现。同样一批参数跑同一段历史数据,每次得到的结果必须完全一致,不允许有任何随机性混入主流程。第三,行为可回测。这条最重要,筛选规则的每一次触发、每一条候选记录,都必须映射到历史行情上的具体时点,能算得出来在这段历史里按规则操作到底会怎样。
这三条原则其实就架出了闭环的形状:原始数据进入指标计算,指标产生信号,信号被摄入策略规则,策略规则驱动回测引擎,回测引擎输出绩效评估,评估结果反馈给规则调整。看起来像数据流图,但本质上是一圈“带反馈回路”的流程。这也是我坚持不直接用现成平台的原因——我想看到每一环的细节,想控制每一个判断点,而不是把数据丢给黑盒。
1.3 技术选型背后的思考
语言选Python,理由没什么新鲜的:pandas、numpy这些库几乎就是为处理行情数据长出来的,生态里还有数不清的辅助工具。不过我没有直接用现成的量化回测框架,而是自己搭了一个轻量引擎。这样做有代价,要处理很多框架帮你处理好的细节,比如撮合逻辑、手续费模型、信号延迟,但收益也在这里——每一个细节都变成了你能掌控的东西。
数据源方面,我选了通过本地CSV库按日期批量拉取,再用脚本做一套数据校验。校验内容不复杂,检查日期连续性、是否有未来数据、是否有异常跳空,但这一步很值得做。行情数据这个事,看起来简单,实际上脏数据比想象中常见。我遇到过某天数据整体偏移、遇到过停牌股在CSV里留下不该有的假阳线,如果没有校验步骤,后面算出来的结果就等于垃圾进垃圾出。
核心计算用pandas的rolling和ewm组合,信号生成则走规则引擎,后面会细讲。这个选型思路的整体逻辑是:项目足够小,不用上重框架;但要做成可验证闭环,必须把上下文记录、数据留档、规则版本化这些“重流程”做扎实。轻工具、重流程,原则就是一切服务于“可复现”。
2. RSI指标的原理拆解与参数设定:先懂指标,再谈策略
2.1 RSI的计算原理:从收盘价变动到动量测量
RSI全称是相对强弱指标,由一位技术分析师在上世纪70年代末提出,用来度量一段时间内价格变动的内部强度。这个“强度”不玄乎,核心就是拿上涨幅度和下跌幅度做对比。
公式分两步。第一步,把每日收盘价与前一日的差算出来,大于零记为上涨,小于零记为下跌。第二步,在给定的回看周期里(比如14天),分别计算平均上涨幅度和平均下跌幅度,然后套入RSI公式:
RS = 平均上涨幅度 / 平均下跌幅度
RSI = 100 - 100 / (1 + RS)
折算之后,RSI的取值被压缩在0到100之间。当上涨幅度占绝对优势时RS趋向极大,RSI就向100靠近;反之则向0靠近。日常交易里最常听到的解读是:RSI高于70算超买,低于30算超卖。这里的逻辑是,极端读数意味着当前动量很难持续,价格大概率要回归。
关键点在于“平均”这两个字。很多人在这一步犯迷糊,觉得平均上涨就是用(总上涨量 / 14)。传统RSI用的不是简单平均,而是Wilder提出的平滑移动平均。它的特点是赋予最近的价格变动更高的权重,对突变更敏感。简单平均和Wilder平滑会在后续数据上产生明显差异——尤其是在趋势反转的早期阶段,Wilder平滑能更快给出转向提示。
2.2 参数与阈值的选择逻辑
RSI的参数主要由回看窗口和超买超卖阈值两部分组成。经典回看窗口是14,这是原始论文里作者反复测试后的默认值,也是市面上所有行情软件打开就能看到的默认值。理论上讲,窗口越短,指标越灵敏,但噪音也越大;窗口越长,指标越平滑,但反应滞后。
以14为主窗口的前提是日线级别。如果把周期换成周线或月线,14根K线对应的实际时间跨度就完全不同,参数的意义也会跟着变。我做了一组参数对比实验:用7、14、28三个窗口分别跑同一批股票的五年日线数据,结果很有意思。7周期RSI触达30以下的次数是14周期的两倍多,但后续出现反转的概率明显更低;28周期虽然触发次数少,但每次触发的含金量高,反转的成功率最高。这也说明一个道理:参数没有绝对好坏,只有匹配不匹配你的交易周期和风险偏好。
超买超卖阈值同样需要斟酌。经典阈值是70/30,但震荡市里价格来回扫,70/30可能触发太频繁;行情一旦走单边强趋势,RSI在80以上待很久也是正常的,过早卖出反而吃不到主升浪。我最终采用的方案是:主策略用14周期搭配80/20阈值,同时把7周期和28周期的RSI作为辅助过滤条件。三窗口同时满足条件才触发信号。这个设计是为了过滤掉那些单一窗口的虚假翻转,后面在回测环节确实体现出了差异。
2.3 从指标到信号的三种典型形态
RSI指标本身只是计算结果,它要变成“可验证的策略规则”,还需要转换成语义明确的信号形态。这个转换决定了策略的收益结构,值得花力气想清楚。
超买超卖翻转是最基础的形态:RSI从下方向上穿越20,视为超卖反转信号;从上方向下穿越80,视为超买反转信号。我用的是穿越而不是触碰,因为触碰在震荡行情里会出现连续的伪信号,而穿越代表动能方向已经实际发生了改变。
背离信号要复杂一些。大致含义是:价格创新低,但RSI没有再创新低,说明下跌动能衰竭。技术分析教材把这个形态叫底背离,是反转的前兆。背离信号在趋势末端很有价值,但识别规则写起来比穿越麻烦,需要额外记录前高点/前低点。我在初版策略里没有把背离做成硬性过滤,只把它作为候选池排序时的加权因子,这样既保留它的价值,又不让它破坏主规则的简洁性。
第三种是多周期共振,本质上是对超买超卖信号的再确认。当14周期RSI触发超卖时,再去看看7周期和28周期是不是也处于低位区域。我设定的条件是三个窗口的RSI值同时小于35,才认定共振成立。这种方式牺牲了一部分触发频率,换来了更高的信号质量。
| 信号形态 | 核心逻辑 | 适用场景 | 主要风险 |
|---|---|---|---|
| 超买超卖翻转 | RSI穿越20/80阈值 | 震荡行情高抛低吸 | 强趋势中反复止损 |
| 背离信号 | 价格极值与RSI极值不一致 | 趋势末端抄底逃顶 | 背离后继续背离 |
| 多周期共振 | 多窗口RSI同处极端区 | 确认信号质量 | 触发频率过低 |
这三种信号形态我最终都落到了规则表里,但主策略只启用了多周期共振这一套。另外两种形态被我留在了工具里,作为一种手工分析辅助面板。原因前面也说了,回测下来共振形态的收益风险比最稳,主策略优先要稳定。
3. Kekkai的系统设计与核心实现:从数据到信号,再串成策略
3.1 整体架构:模块分层与数据流
系统设计一开始就是按模块拆的,四个层次:数据层、指标层、信号层、策略验证层。每层只干一件事,层与层之间用明确的接口对接。这个结构的最大好处是单点可测——你可以把任何一层的输入固定住,去验证输出对不对,不用把整条流水线跑完才知道哪里崩了。
数据层负责读取CSV、清洗、对齐日期、插值停牌日。输出是一张标准的DataFrame,列名统一为date、open、close、high、low、volume。指标层接收清洗后的行情,按参数配置计算RSI,并输出RSI值序列。信号层读取RSI序列,应用参数阈值和规则模板,生成布尔型的信号列表。策略验证层则负责把信号翻译成模拟交易记录、汇总绩效并产出报告。
数据流是单向的:数据层到指标层到信号层再到策略验证层,不允许反向调用。比如你想在信号层里再改RSI的参数,那就回指标层的配置去调,而不是在信号层里写临时计算。这个规矩帮了大忙,它保证了整个流程的所有中间产物都能被随时序列化存档,复现实验时只需要重放存档即可。
3.2 指标计算模块的实现细节
RSI计算代码不复杂,但有几个细节极容易踩坑。先看核心代码:
import pandas as pd import numpy as np def compute_rsi(close: pd.Series, period: int = 14) -> pd.Series: delta = close.diff() gain = delta.clip(lower=0) loss = -delta.clip(upper=0) avg_gain = gain.ewm(alpha=1/period, min_periods=period, adjust=False).mean() avg_loss = loss.ewm(alpha=1/period, min_periods=period, adjust=False).mean() rs = avg_gain / avg_loss rsi = 100 - (100 / (1 + rs)) return rsi第一行对收盘价做差分,得到每日涨跌量。clip操作把涨跌拆成两组正数序列。这里的核心是ewm,它的alpha取值是1/period,正好对应Wilder平滑里对旧数据的衰减系数。min_periods=period确保前period-1个数据点不会被填上什么奇怪的值,而是保持为NaN。这段代码跑5000行数据,耗时在毫秒级,性能完全不是瓶颈。
很多人以为算RSI就是照着公式敲一遍,但有个前提容易被忽略:你在用收盘价差分时,必须确认序列已经按日期排序,且没有重复日期行。我在数据层就做了一次去重和排序,指标层还要再加一道校验,防御性编程在这里很实用。另一个坑是avg_loss可能出现0值,比如连续多日全部上涨。此时rs变成inf,rsi会被推成100。真实行情里很少见,但测试样例里一定会出现,必须在代码里单独处理。我把avg_loss兜底为一个极小值,比如1e-10,这样rsi计算不会崩。
3.3 规则引擎:把交易规则从策略里拆出来
最开始我设想的是写一个筛选函数,硬编码几个条件。但后来发现信号层一旦要支持回测,不同的参数组合、不同的阈值灵活性要求非常高,硬编码风格到最后会变成一团乱麻。于是我把规则从代码里拆了出来,做成了声明式规则表。
规则表的每一行包含规则名、逻辑类型、标的字段、阈值上下限、生效布尔值。策略引擎来读取这张表,逐条判断当前RSI序列是否满足全部条件。这样策略的每一次调整都不需要改代码,只需要改配置。更重要的是,配置本身可以被存档、被版本化,回测时能把规则的版本号对齐到每一轮实验。
# 规则配置示例 rules = [ {"name": "rsi_oversold", "field": "rsi_14", "operator": "above", "value": 20}, {"name": "rsi_resonance", "field": "rsi_7", "operator": "above", "value": 35}, {"name": "rsi_resonance2", "field": "rsi_28", "operator": "above", "value": 35}, {"name": "volume_filter", "field": "volume_avg_20", "operator": "above", "value": 100000}, ]这个设计直接引出了另一个好处:新规则可以通过简单拼接来验证。比如在共振条件之外追加一个成交量过滤,我只需要在规则表里加一行,然后把生效布尔值从False改成True,重新跑一遍回测就行了。这个流程折腾起来不肉疼,因为全部改动都在配置层面,可验证性也天然保留了。
3.4 交易信号与回测闭环
信号生成之后,还不能直接说“这个策略赚钱”。我搭建的是一个极简回测引擎,把信号映射成模拟成交记录。关键点在于成交价格的处理:信号在收盘后生成,但执行只能在下一个交易日的开盘。如果回测里用收盘价去成交,就是典型的前视偏差,等于拿未来价格做决策,结果当然漂亮得像作弊。
持仓周期我设成了两种:一种是信号触发后持有固定天数(比如10个交易日),另一种是持有到反向信号出现为止。模拟成本包括双边手续费和千分之一的滑点假设,这些成本在回测里反复出现,会显著影响最终的收益评估。
绩效汇总输出五类指标:区间累计收益、最大回撤、夏普比率、交易胜率、交易次数。每轮回测都会生成一份JSON格式的存档文件,里面包含本轮的回测区间、标的池、规则版本、参数配置和绩效结果。这一步是闭环的最末端,也是最容易被忽略的一环。没有这个存档,前面做的一切验证努力都白费了,因为你根本无法回答“你上次跑出的那个结果,是在什么条件下跑出来的”。
4. 验证闭环的搭建:如何证明策略“可验证”而不是“撞运气”
4.1 三类测试框架
“能运行”和“可验证”完全是两个层次的评价。我在项目里搭了三层测试,逐层递进。
第一层是样例级调试。固定输入一批造好的迷你行情数据,手算RSI值,和代码输出比对。这层测试的目的是确保指标计算本身没有bug。第二层是全历史回测。用完整的历史数据跑一遍主流程,把结果存档,看策略的整体表现。第三层是样本内外划分测试。把历史数据按时间切成两段,前一段用于调参,后一段完全不参与参数调整,作为盲测集。只有后备一段的绩效也说得过去,这个策略才算初步经受住了考验。
这三层测试加起来,成本不高,但价值极高。样例级测试保证算得对,回测保证能跑通,样本内外划分保证不是纸上富贵。很多刚上手研究量化的人会跳过前两层直接上回测,发现问题后根本分不清是bug的问题、数据的问题还是策略逻辑的问题,排查只能靠猜。
4.2 逐一解析验证环节的关键点
回测环节最容易犯的错误,我在5.2里会详细展开,这里先讲一个最关键的思维模式:任何回测,本质上是在检验“如果我在过去某一天按规则操作,结果会不会和预期一致”。
这句话意味着,回测里所有的输入必须严格限定在当时已知的信息。具体到RSI策略:筛选信号收盘后触发,那你模拟下单的价格就应该是第二天的开盘价,而不是当天收盘价。手续费和滑点的假设,只能基于当下能估算的流动性水平,不能用未来才知道的真实成交价格来反推。
样本内外切分也有讲究。切分点不能选在某个市场事件的附近,比如牛熊转折的中段,不然两边的数据分布差异会极大,导致内样本调出的参数在外样本完全失效。我用的切分比例大约是前65%调参、后35%盲测,并且在两段之间预留了20个交易日做缓冲,防止信号跨段产生边界效应。
4.3 用数据说话:一次典型回测的结构
拿某一次回测举例,标的池是某个行业板块的30只股票,区间是近两年半的日线数据,规则用的是14周期RSI超卖共振,持仓周期10天。回测结果大概是这样的:
| 指标 | 数值 |
|---|---|
| 区间累计收益 | 27.4% |
| 基准指数同期收益 | 12.8% |
| 最大回撤 | -8.9% |
| 夏普比率 | 0.91 |
| 交易胜率 | 54.2% |
| 交易次数 | 48 |
单看累计收益,这个策略好像跑赢了基准。但真正要关注的是最大回撤和夏普比率。最大回撤只有9%左右,说明策略在波动控制上做得还可以;夏普0.9虽然不算顶尖,但在日线策略里已经属于风险收益比可接受的范围。交易次数48笔属于中低频,胜率54%意味着它不是一个靠单次爆发赚钱的策略,而是一个靠赔率累积的稳健策略。
只看这些数字还是会出错——你得把它放到上下文里读。这个标的池本身是某个板块,而这两年多该板块整体就在上涨,策略的超额收益也就只有十几个点。单独跑一只股票,这样的结果说明不了什么。所以在报告里我额外附上了基准收益和每笔交易的明细记录,要把装在套子里的运气也拆开看。
5. 踩坑实录与排查技巧:三天两夜的死磕记录
5.1 最隐蔽的坑:RSI计算中的“零分母”与起始段漂移
第一轮结果出来时,我发现某些个股的RSI序列在连续上涨后直接变成100,然后卡在100不下来。先以为是代码问题,查了半天才发现是avg_loss归零导致的除零异常,RS函数里分母是0,算出来的RS是无穷大,RSI就被顶到100。这个现象在数学上很好解释,但在真实回测里很坑——连续涨停或一字板较多的股票经常发生。
修复方式是在分母上做保护,把avg_loss小于1e-10的值强制替换为1e-10。这个操作对RSI的正常计算区间几乎没有影响,因为正常情况下下跌均值不会小到这种程度,纯粹是用来防除零崩溃。
另一个更隐蔽的问题是起始段的漂移。RSI的初始值高度依赖第一根K线的涨跌方向,如果拿一段行情的开头几天来算,RSI会有一段明显的“热身期”,这和我们常说的“新数据驱动的计算需要时间收敛”是一个道理。处理方案是在回测时给指标计算预留一个足够长的预热窗口,把需要计算的起始日期往前推两个月,信号正式生效的日期从预热结束之后才开始。这样热身期的影响就被隔离在了正式结果之外。
5.2 前视偏差的典型翻车现场
我犯过的最大错误是信号生成和成交时间错位。最初版本里,我在回测主循环里当天生成信号、当天以收盘价成交。当时没觉得有问题,因为很多入门教程都这么写。但某一轮回测跑出来的收益高得离谱,高到我自己都不敢信。
排查时我把每一笔成交明细打出来看,发现有个股票的买入信号出现在某月10日,但成交价格却用的是10日的收盘价——而这根收盘K线本身就是形成信号的数据之一。通俗点说,你是在看完考试答案之后再去填答题卡,分数当然高。
修复方式很简单,把执行时间整体往后挪一个交易日。信号在T日收盘后生成,模拟订单挂在T+1日开盘执行。改完之后重新跑回测,收益立刻回归正常。这个翻车现场让我养成了一个习惯:每次回测日志里必须同时打印信号时间戳和成交时间戳,肉眼检查这两列是否存在同日期的情况。
5.3 过拟合防护与参数敏感度检查
策略做完之后,很容易陷入参数寻优的泥潭。把RSI的窗口从12调到18,把阈值从75/30改到80/25,每次微调都可能让回测结果更好一点点。但这里有个陷阱——你调整参数的过程本身就在“拟合”历史数据,参数寻优做到极致,就是对历史数据做一次高维插值。
我没有走自动寻优的路,而是做了一张参数敏感度表。以RSI主窗口为横轴、超卖阈值为纵轴,跑一个参数网格,把每个格子上回测的夏普比率填进去。如果答案是块平滑的高原,那参数稳定性是可靠的;如果是一根陡峭的尖峰,稍微偏一点绩效就崩,那这个策略就是过拟合的形态。
结果很关键。我发现某个参数组合在网格角上有一格夏普1.4的极值,但周围四个格子都跌回0.7以下。这就是典型过拟合尖峰。后来我把参数锁定在了高原区的中心位置,夏普稳定在0.8到0.9。坦白说,这个收益不如单点极值漂亮,但它是个把“运气”拆出来后的真实状态。我宁愿要一个平庸但可信的结果,也不要一个漂亮但不可复现的幻觉。
6. 从工具到结论:这个闭环最终给我带来了什么
项目做完,最大的收获反而不是RSI策略本身,而是“把模糊需求变成可验证闭环”的整套方法。我总结出三个可复用的经验:第一,需求再模糊,也要先定义“验证标准”再动手,否则做出来的东西只是自娱自乐;第二,模块分层和规则配置化这些“不性感”的设计,恰恰是让系统能持续演进的前提;第三,存档永远比你以为的更重要,规则版本、参数配置、结果数据缺一不可。这三个经验后来被我反复用到其他项目和日常分析里,屡试不爽。
最后分享一个实用的小细节:我在策略配置里埋了一个version字段,每次回测存档时自动把配置文件的内容哈希也写进去。这样回看历史结果时,只要哈希不同,就知道规则数据已经变了。你甚至可以在此基础上加一个简单的对比脚本,自动告诉你“这次改动让回测结果变好还是变坏”。这个习惯看起来微不足道,但在需要长期迭代的策略研究里,它比任何花哨的功能都更能救命。