“能不能做个指标,把风险挡住?”这是我接手这个项目时,需求方给出的全部信息。没有明确策略逻辑,没有预期收益,甚至没有说清“挡住”是指止损、减仓还只是提示。项目代号就叫“RSI-Kekkai”,Kekkai在日语里是“结界”的意思——需求方想要的,是一个能守住账户回撤的屏障。
从一句模糊需求到最终跑通回测、完成参数验证、接入模拟盘环境,整个过程花了三周。这篇博文想完整记录这段经历:我是怎么把“挡住风险”翻译成RSI指标规则的,怎么设计“结界”的工程结构,又是怎么让它变成一个可验证、可被追问、可迭代的量化闭环。如果你也接过这种“一句话需求”,或者正在设计自己的趋势/震荡判别边界,这篇文章应该对你有用。
1. 那句模糊需求背后,其实藏着三个待解的问题
1.1 原话、潜台词与真正的问题
需求方的原话是:“最近行情上下乱跳,能不能搞个东西,跌多了提醒我,至少别再深套了。”
这句话拆开看,有三个层次。第一层是“跌多了提醒我”,这指向超卖识别;第二层是“别再深套”,这指向止损或仓位控制;第三层是“上下乱跳”,这说明需求方默认当前市场处于震荡而非单边趋势。三者叠加,RSI(相对强弱指标)几乎是天然答案——它本身就是衡量涨跌动能强弱、标注超买超卖区的经典指标。
但仅仅确认用RSI还远远不够。真正的问题是:当RSI进入超卖区,系统要做什么动作?是弹一个提示让用户自己决定,还是自动降低仓位?提示的阈值是标准的30,还是要根据不同品种调?如果行情继续下跌,RSI持续钝化在超卖区,系统怎么避免反复发出错误的“到底了”信号?这三个问题不解决,需求就依然是模糊的。
1.2 把业务语言翻译成规则语言
和需求方来回对了三轮之后,我把需求收敛成三条可验证的规则:
- 当RSI(14)低于30时,视为进入“结界警戒区”,系统输出减仓信号;
- 当RSI(14)从30以下回升至35以上时,视为“结界解除”,系统输出恢复仓位信号;
- 若RSI在低于15的极值区停留超过3根K线,强制触发一次仓位复查流程,不自动加仓。
这三条规则看起来简单,但每一条都对应着需求背后的潜台词。“回升至35才解除”是为了避免在30附近反复横跳产生大量无效信号;“极值区停留超过3根K线”是为了应对RSI钝化——这是RSI策略最容易出问题的环节。
这一轮澄清花的时间不长,但价值极高。需求方看到这三条规则时,立刻提出了一个问题:“那如果它一直钝化怎么办?”这说明他真的理解了方案边界——这就是从模糊需求走向可验证闭环的第一步:不是翻译需求,而是把需求逼成一个可以被证伪的系统。
2. “结界”的工程化拆解:为什么RSI适合做屏障,以及它的三个致命短板
2.1 为什么选RSI而不是MACD或布林带
做风控屏障,最重要的是对“极端状态”的反应速度和可解释性。RSI天然满足这两点:它的取值范围固定,落在0到100之间,天然是一个“有无越界”的判定器;它的计算方式决定了它对近期价格变化敏感,比MACD的两条线更快反映动能反转。
相比之下,MACD的边界是动态变化的——金叉死叉的参考线本身在移动,作为“边界”来用,缺少一个稳定锚点;布林带虽然也能给出通道边界,但它的上下轨是统计带宽,当行情单边加速时带宽会迅速扩大,出现“价格越跌、下轨越远离价格”的尴尬情况,根本挡不住下跌。
RSI的固定区间、固定阈值,让“破界”和“回归”这两个动作有了明确定义。这也是“Kekkai”这个概念能落地的原因——结界必须先有边界,边界必须稳定。
2.2 RSI计算的细节,以及一个常被忽略的参数陷阱
RSI的标准计算方式不复杂:取N周期内的涨幅平均值与跌幅平均值,算出相对强弱值,再映射到0到100区间。
N = 14 up = 涨幅中大于0的部分的均值(N周期) down = 跌幅中大于0的部分的均值(N周期) RS = up / down RSI = 100 - 100 / (1 + RS)实操中有一个细节极其关键:跌跌幅的均值计算,有的库用SMA(简单移动平均),有的用Wilder平滑法。两者在长周期的RSI数值上差异不大,但在短周期上,Wilder平滑会让RSI变化更平缓,减少跳变。
我最终选用了Wilder平滑法,原因单纯:它让RSI曲线更稳,边界突破次数少了,误报率明显下降。
但真正让我在测试中差点翻车的是另一个问题——RSI的计算窗口。很多人默认用14,因为这是最经典的取值,但它不是万能的。我拿某只日线级别走势测试时,14周期的RSI经常跌到20以下,导致“结界警戒区”频繁触发,信号多到没法用。后来换用21周期,信号频率立刻降下来了。
参数选择没有标准答案,但有一个原则:让“边界内波动”覆盖掉噪音,让“破界”只留给真正的极端行情。阈值和周期必须匹配你观察的品种波动特性,不能拿来就用。
2.3 钝化、钝化、还是钝化
RSI做风控屏障,最怕的是钝化。钝化是指价格持续单边下跌,RSI长期趴在超卖区,信号一直显示“超卖、该反弹了”,但价格还在跌。这时候如果系统机械地执行“进入超卖就买入/加仓”,就是灾难。
我设计的应对方式是引入“结界状态机”。这个状态机有四个状态:
- 安全区:RSI在30以上,不触发任何动作;
- 警戒区:RSI跌破30,记录入场信号,但只提示不操作;
- 极值区:RSI跌破15,且停留超过3根K线,强制触发仓位复查流程;
- 回归确认:RSI回升到35以上,且连续2根K线站稳,才解除警戒。
这个状态机的核心思路是:把RSI从“单一信号源”降级为“状态判定器”。我不再依赖RSI每一次穿越阈值都产生交易动作,而是依赖它在不同区间的停留状态来调整系统行为。钝化阶段系统不会反复给矛盾信号,而是锁定在极值区,直到出现真正的反转确认。
3. 从指标到闭环:数据层、信号层、验证层的完整实现
3.1 数据层:清洗K线时踩的坑
项目第一步是准备行情数据。我用的是某交易平台的分钟线数据来做参数调试,日线数据做回测验证。这里有个典型的坑:不同数据源的K线时间戳对齐方式不同,有的是K线开始时间,有的是K线结束时间。如果混用,RSI计算会出现偏移一根K线的问题,信号整体错位。
我用了一个最简单的办法统一处理:清洗时强制把时间戳归一化为“K线开始时间”,并把数据按时间序列升序排列,遇到重复时间戳直接去重。这些操作看起来基础,但直接影响RSI计算的正确性。
清洗脚本长这样:
def clean_kline(df): df = df.sort_values("timestamp") df = df.drop_duplicates(subset="timestamp", keep="last") df["rs_basis"] = df["close"].diff() return df3.2 信号层:状态机的代码实现
核心逻辑就是上面说的四状态状态机。实际写下来,比预想中要简洁:
def kekkai_state(rsi_value, prev_state, hold_count): # 状态递进:安全 -> 警戒 -> 极值 -> 回归确认 if rsi_value < 15: current_state = "extreme" elif rsi_value < 30: current_state = "alert" elif rsi_value > 35: current_state = "safe" else: # 30到35之间,维持上一状态,避免频繁切换 current_state = prev_state if current_state == "extreme": hold_count += 1 if hold_count >= 3: trigger_action("仓位复查") elif current_state == "alert": hold_count = 0 return current_state, hold_count这段代码里有一个关键设计——“30到35之间维持上一状态”。这就是所谓的“滞回区间”,用工程术语说叫hysteresis。它避免了RSI在30附近来回穿越时,系统不断在安全区和警戒区之间反复切换,产生大量无效信号。
3.3 验证层:怎么让“可验证”三个字落地
闭环的最终闭环,是验证。验证不是跑一次回测看收益率曲线就完了,而是要让每一步都能被追问、被复现。我给这个项目搭了三层验证:
第一层是信号正确性验证。随机抽取20个历史时间点,人工确认历史价格走势,核对系统在这些时间点输出的信号是否符合规则。这步慢,但能抓出代码逻辑和数据对齐问题。
第二层是参数敏感性验证。把RSI周期从14改为13、16、21,把阈值从30改为25、28、32,观察信号数量和收益率变化。好的策略应该对参数微调不敏感。如果换个参数结果就剧烈变化,那说明策略逻辑本身是在“过度拟合”历史数据,而不是在捕捉真实规律。
第三层是样本外验证。拿前80%的数据做参数调试,后20%数据用来做最终验证,后者完全不参与参数选择过程。样本外表现才接近真实水平。
这三层验证跑完,项目才算真正达到了“可验证的闭环”。没有这个过程,策略就只是“看起来能赚钱的曲线”而已。
4. 回测结果怎么看:一个反直觉的真相和两条铁的纪律
4.1 别盯着收益率曲线,先看最大回撤和信号胜率
第一次跑完回测,收益率曲线很漂亮,年化收益亮眼。如果只看收益率,这个项目当场就能验收。但我把最大回撤拉出来一看,心里凉了半截——回撤几乎全部集中在RSI钝化区间,策略在震荡行情下表现尚可,在单边下跌行情中形同虚设。
这其实暴露了一个本质问题:RSI本质上是动量指标,“钝化”是它的天然盲区。任何以RSI为核心的风控系统,都必须接受一个事实:在极端单边行情中,它能做的是止损,而不是预测反转。
因此我调整了回测考核指标,优先看三组数据:最大回撤是否被控制在阈值以内、信号胜率是否稳定在合理区间、单笔止损是否能覆盖RSI钝化期的连续亏损。收益率排在这三者之后。
4.2 纪律一:先有止损逻辑,再谈信号逻辑
很多人在设计RSI策略时,先想的是“RSI低于30我就买”,这是典型的信号驱动思路。但做风控屏障,思路必须反过来:先定义什么情况下无条件撤退,再定义什么情况下可以进场。
我给这个项目加的底线规则是:如果收盘价跌破此前20个交易日的最低价,无论RSI处于什么区域,强制清仓该标的的信号。这条规则完全不依赖RSI,它的作用是防止RSI钝化期出现极端损失。
4.3 纪律二:信号必须可回溯、可审计
项目第一次上线模拟盘时,出现过一次信号延迟问题——系统在盘中触发了警戒信号,但需求方盘后复盘时完全找不到对应的历史标记。排查下来,是信号记录表中只保存了最新状态,没有保存每个信号产生的完整上下文。
修复方法是:每产生一个信号,强制记录这个信号发生时RSI的取值、当时价格的相对位置、行情数据源时间戳、以及触发规则编号。这样每一个“结界动作”都能被回溯。
5. 从“能跑”到“敢用”:模拟盘阶段的三次修正
5.1 修正在盘中发现的时间偏差
接入模拟盘后,第一个问题是信号时间与预期不符。系统发出警戒信号的时间,比需求方主观感受的“危险时刻”晚了大约两小时。查下来,问题出在RSI计算时用了未复权的收盘价,而数据源在盘中推送的是实时价,两者在除权除息日会产生明显偏差。
修正方案是对数据源做前复权处理,同时在计算RSI时明确使用复权价。这个坑在回测阶段完全体现不出来,因为回测数据已经过处理,但实时数据不会。
5.2 修正二:模拟盘和回测的最大差异,是延迟和滑点
回测可以假设信号产生的同时就按收盘价成交,但模拟盘必须面对现实:信号产生后,人工确认、下单、成交之间有时间差。在快市环境下,这个时间差足以让实际成交价偏离信号价好几个档位。
我给信号加了一个“确认延迟”参数,默认设为5分钟,在回测中也加入同样的延迟假设,让测试环境更接近真实。这步修正直接过滤掉了很多在纯回测中看起来不错、其实无法稳定实现的策略变体。
5.3 修正三:把“提示”升级成“动作”,才算闭环
项目前两周,系统只会输出提示信号,实际操作由需求方自行判断。结果需求方因为最初几天行情反弹了,连续两次忽略警戒信号。直到一次深度回调吃了亏,他才回来说:“提示没用,得让它直接控制仓位。”
这是整个项目中最重要的一次需求变更。我把“提示型系统”升级为“动作型系统”:当警戒区信号触发时,系统自动计算当前持仓的建议减仓比例,并输出可直接执行的指令;只有当需求方人工确认后,系统才恢复安全区状态。
这一步让“RSI-Kekkai”从一个“看板”变成了“半自动风控装置”。从用户反馈来看,这个变更比之前所有功能叠加起来都更有价值。
6. 项目复盘:三个判断和两个遗憾
6.1 判断一:状态机比报警器更接近真实需求
最初版的设计就是简单的“跌破30报警”,做成之后发现没人重视它。原因很简单:报警器只会说“危险”,但不说“怎么办”。状态机的价值不在于更智能,而在于它给出了行为路径——进了警戒区该做什么,进了极值区该做什么,什么时候解除警戒。它把“风险感知”变成了“风险管理”。
6.2 判断二:模糊需求不可怕,怕的是没人把它逼成可证伪的问题
这个项目的前两天,我们几乎没有写代码。所有时间都花在把“挡住风险”变成“跌破30提示、回升35解除、极值区停留3根复查”这样的具体规则上。一旦规则具象化,后面的开发就只是执行了。
6.3 遗憾一:没有在实盘单边行情中经受真正考验
项目验证期结束时,市场整体处于震荡格局,“结界”最害怕的钝化场景只出现过一次,且持续时间很短。它还没有经历过连续多日阴跌的极限测试。这是一个未完成的验证项,后续如果遇到类似的行情,我会把完整的复盘记录补上。
6.4 遗憾二:没有做多标的组合层面的屏障协同
目前每个标的的“结界”是独立运作的,但一个账户同时持有多只标的时,真正的风险是系统性共振——所有标的同时进入警戒区。现在系统没有在组合层面做“总警戒”判定。这是我下一版最想补上的功能:当持仓中超过40%的标的进入警戒区时,触发组合级风控动作。
7. 给后来者的三句话
第一句,做量化风控,永远先把防守逻辑写在进攻逻辑前面。RSI能告诉你哪里是墙,但先要决定好撞墙之后怎么活着回来。
第二句,任何指标都需要状态化,不要指望单次穿越能代表全部含义。RSI钝化是常态,不是异常,系统设计要围绕常态运转,而不是围绕理想情况设计。
第三句,多花时间在数据清洗和信号回溯上。整个项目中让我最狼狈的,不是策略逻辑,而是除权日的价格偏移和不同数据源之间的时间戳对齐。
这个项目最后交付的是一个能跑、能查、能调整的半自动风控闭环。它不完美,但在“从模糊需求到可验证闭环”这件事上,该走的路径都走通了。如果你正在做类似的事,希望这篇记录能帮你少踩几个坑——尤其是那个除权价格的坑,真的太隐蔽了。