K1策略升级:新增市场状态感知+动态过滤门
背景
K1斜度轮动策略跑了有一阵子了——39只ETF候选池,每天算斜度排名,持Top1,满仓30万。信号灯(绿灯/黄灯满仓,红灯禁开)、大盘单日跌超3%禁买、5%回撤止损,这些规则稳了好些天。
但有个问题从一开始就有:策略对市场整体冷暖没有感知。红灯只是沪深300跌破MA120这一个条件,但如果市场整体在阴跌通道里,个别ETF斜度好看,策略还是会开仓——买了以后大盘继续跌,持仓跟着吃面。这轮行情下来尤其明显。
方案
这次给K1加了三个新东西,核心思路就一句:让策略先抬头看天,再低头挑标的。
1. 市场状态判断(每天算一次,缓存复用)
取39只候选池每只过去60日的收盘收益(含当日),算中位数。同时取沪深300同期60日收益做参照。判定逻辑:
- 好市场 = 池中位60日收益 > 0% 且(池中位 − 沪深300收益)> −5%
- 坏市场 = 不满足上面任一条件
本质上就是两问:池子整体赚不赚钱?有没有大幅跑输大盘?两个条件都满足才算好市场。这个计算一天只做一次,结果缓存进state,当天后续调用直接复用,不重复算。
2. 状态切换过滤(Top1买入前拦截)
市场状态决定过滤门的松紧:
- 好市场 → 宽松档:accel > 0 且 5日涨幅 < 13%。牛市保留弹性,别因为涨得猛就下车。
- 坏市场 → 严格档:accel > 0 且 10日涨幅 < 10%。熊市不追高,宁可错过。
其中 accel = 今日25日ln斜率 − 前25日ln斜率(二阶导,判断趋势是否还在加速而不是减速)。ret5 = 今收/5日前收 − 1,ret10 同理。
3. 拦截后不补位
这是跟冷却顺延的关键区别,也是最容易搞混的地方:
- 冷却顺延:Top1在冷却期 → 选Top2(保留不动)
- 状态过滤拦截:Top1不满足档位过滤 → 不开仓,记入skips清单,不向下顺延。空仓等明天。
逻辑很简单:熊市里强行买第二名,多半是换个姿势亏钱。冷却顺延是"这个标的暂时不能买,换一个同类";状态拦截是"这个市场不该买,管你第几名都不买"。
实测
3次完整运行,真实行情数据,39/39拉取全成功:
市场状态: 🔴 坏市场 | 池中位60日=-6.81% | 沪深300同期=-4.48% | 严格档
池子里60日跌了6.81%,沪深300跌了4.48%,判定坏市场。第2、3次运行命中缓存,当天只算一次——这个缓存逻辑也跑通了。今天本身就是红灯禁开仓+坏市场两道锁,策略不开仓符合预期。
顺便修了个老bug
第1次跑的时候发现一个问题:"当前持仓"显示空白,但"今日操作"却说持有。排查下来定位到原代码的坑:持仓表格只在排名前10里找持仓。港股通创新药(sz159570)当天排名第20,直接找不到了。
这个bug应该从策略第一天就存在,只不过之前持仓一直在Top10里没触发。修复很简单——持仓表格改为直接遍历实际持仓,不再依赖排名。修完以后再跑:
当前持仓(Top1):
排名 代码 名称 得分 股数 成本 现价 浮盈 回撤
19 159570 港股通创新药 -0.000025 17,000 1.4695 1.4620 -0.51% 0.51%
今日操作:
📌 持有:港股通创新药(sz159570) → 不变
部署
新逻辑已生效,state完整性验证通过——持仓完好、冷却天数正确、market_state缓存保留。明早开盘cron(11:20/14:40)自动带新规则跑。
核心改动就加在开仓前的拦截逻辑里——k1_strategy.py 的排名循环找到Top1后、下单前,多一层判断:先看市场状态是松是严,再看accel和涨幅过不过档位。所有原有逻辑(信号灯、MA30/MA20筛选、5%止损、冷却顺延)完全不动。
这事值得记一笔,因为它是K1第一次有了"整体市场观"——不再闷头看排名,会先判断环境再决定要不要伸手。说起来简单,做起来也就几十行代码,但这个决策框架的加入,让策略从"纯动量"往"动量+风控"靠了一步。