name: hl-quant description: 当用户要用启发式学习优化量化交易策略时使用:诊断策略弱点、提出有经济含义的假设、只修改策略文件、用固定回测评分器评估,并按严格门槛决定是否接受。触发词包括“启发式探索”“启发式学习”“HL循环”“策略调参”“回测改进”“优化策略分数”“heuristic exploration”“heuristic learning”。
hl-quant:量化策略的启发式探索 Skill
用固定回测评分器反复评估策略候选:先诊断弱点,再提出一个有经济含义的假设,只改策略文件,最后用同一评分口径决定是否接受。
本 skill 是「启发式探索」通用框架在量化交易领域的实例化。完整框架(核心启发、固定评估器范式、模块结构)见随 skill 分发的 references/framework.md;要把同一方法迁移到性能/转化率/错误率等其它可度量问题,也从该文档入手。下文示例里的指标名、阈值、参数均为教学用的虚构值,不代表任何真实生产策略。
什么时候使用
- 策略表现弱:Sortino / Sharpe 偏低、回撤偏大、胜率偏低。
- 需要提高策略 score、收益或风险调整后表现。
- 需要解释策略为什么失败,而不是只知道分数低。
- 需要判断某个参数变化是真改进,还是过拟合。
什么时候不要用
- 已有固定参数空间,只需要纯数值搜索:优先用 Optuna / grid search。
- 策略逻辑已经冻结,只是在做部署。
- 还没有固定评估器:先建立评估器,再做 HL。
核心循环
Probe → Diagnose → Propose → Patch → Evaluate → Replay → Decide → Compress
- Probe:运行固定评估器,记录基线 score、关键指标和归因材料。
- Diagnose:解释弱点来自哪里,例如入场太早、离场太快、震荡中反复打脸、仓位过度集中、 regime 错配。
- Propose:每轮只提一个有经济含义的假设,例如“放慢均线可以过滤噪声并持有趋势更久”,而不是“MA=17 分数最高”。
- Patch:只修改策略文件。不要改评估器、数据源、成本模型或评分公式。
- Evaluate:用同一评估器重跑,比较 score 和关键指标。
- Replay:检查 golden case、实盘约束和已知失败场景没有退化。
- Decide:通过严格门槛才接受,否则拒绝并回到 Diagnose。
- Compress:删除无触发、负贡献、重复或解释不清的规则,保持策略小而可解释。
固定评估器与唯一可编辑程序
候选策略必须走同一条评估管线:
同一数据 → 同一回测引擎 → 同一成本模型 → 同一评分公式 → 一个 score
只有策略文件可以改。如果为了提分去改评估器,候选之间就不再可比,整个循环会变成自欺。
如果认证失败、数据缺口、引擎崩溃或缓存不可读,停止并报告阻塞。不要靠修改基线绕过问题。
Score 口径
本仓库示例采用 Sortino ratio(索提诺比率) 作为主 score:
score = Sortino
Sortino 只惩罚下行波动,比 Sharpe 更贴近“上行波动不是坏事、下行波动才是风险”的交易直觉。Sharpe、收益、回撤、胜率和交易笔数仍是验收门槛的一部分。
接受门槛
候选必须同时满足以下条件。右列是开箱即用的默认容忍带,在 run 开始前写入 run_config 即可,避免临场心证:
| 指标 | 默认门槛 |
|---|---|
| score / Sortino | 严格高于基线(硬门槛,不容忍) |
| 总收益 | ≥ 基线 − 5% |
| 年化收益 | ≥ 基线 − 5% |
| Sharpe | ≥ 基线 − 5% |
| 最大回撤 | ≤ 基线 + 2pp |
| 胜率 | ≥ 基线 − 3pp |
| 交易笔数 | ≥ 50(见下) |
唯一需要你先确认的是交易笔数下限:它和回测区间长度、股票池大小强相关。默认给 50,覆盖多数全市场年级别回测;如果你的区间明显更短、股票池更小(笔数天然偏少),或反过来很大,按规模上调/下调后固定下来,不要直接套 50。其余几项的 5% / 2pp / 3pp 容忍带可直接沿用。
score 变高但关键指标塌掉,拒绝。一次幸运交易带来的高 Sortino,不等于策略真的变好。
反过拟合纪律
每个改动必须有经济含义
| 改动 | 经济解释 | 结论 |
|---|---|---|
| 均线从 5/10 放慢到 10/20 | 过滤日间噪声,持有趋势更久 | 可接受 |
| 增加价格高于 60 日均线的趋势过滤 | 避免下行趋势里逆势买入 | 可接受 |
| 某震荡指标死区 31-32 | 为什么偏偏是 31-32,没有市场解释 | 拒绝 |
| 止损从 10% 调到 8.2% | 8.2% 太精确,像曲线拟合 | 拒绝 |
禁止连续变量上的窄坑过滤
不要在 RSI、量比、资金流、动量等连续变量上挖 1-2 个点宽的死区(下面用一个虚构的振荡指标 OSC 举例):
# 过拟合:为什么是 43-44,而不是 42-43?
ENTRY_OSC_DEAD_ZONE_MIN = 43.0
ENTRY_OSC_DEAD_ZONE_MAX = 44.0
更稳健的变化应该是有经济含义的连续区间,或规则级开关:
# 可解释:OSC 40-50 表示一段有市场含义的“中性犹豫区”
ENTRY_OSC_DEAD_ZONE_MIN = 40.0
ENTRY_OSC_DEAD_ZONE_MAX = 50.0
禁止未来函数
- 入场信号只能使用已完成的 bar。
- 成交必须发生在下一根 bar 的开盘,或另一个预先固定的成交规则。
- 不要用同日未完成的 high / low / amplitude 过滤当日入场。
保证样本量
高 score 但只有 1-2 笔交易,通常是运气,不是稳健 edge。交易次数太少时,即使 score 更高,也不能直接接受。
训练 / 验证纪律
如果有股票池或时间切分:
- 训练集:可以用于搜索。
- 验证集:每轮都可以评估,但不能用来指导搜索方向;可以用验证退化来拒绝过拟合,不能为了最大化验证分数去调参。
- 最终 holdout:候选冻结后只跑一次。holdout 输给基线,不部署。
泛化健康度参考:
| 指标 | 健康 | 警告 | 拒绝 |
|---|---|---|---|
| train score - validation score | < 0.5 | 0.5 ~ 1.0 | > 1.0 |
| train return / validation return | 0.7 ~ 1.3 | 0.5 ~ 0.7 或 1.3 ~ 2.0 | < 0.5 或 > 2.0 |
进入拒绝区的候选,即使训练集硬门槛通过,也视为过拟合。
改进泛化的做法
- 优先结构变化,而不是精调阈值:增加 regime filter 往往比把阈值从 7.2% 调到 7.8% 更可泛化。
- 每轮只动一个变量:否则失败无法归因,成功也难复现。
- 接受后压缩:删掉不触发、重叠、解释不清的规则。
- 回放 golden case:真实失败场景要固定成回归样例,防止实盘约束退化。
- 不要追最高分:停在多指标都变好且交易笔数健康的候选,而不是追 1-2 笔交易堆出来的最高 score。
示例流程
# 1. Probe:跑基线
python backtest.py
# score 0.7545, return +5.46%, Sharpe 0.698, Sortino 0.755, drawdown 7.10%, 12 trades
# 2. Diagnose
# 5/10 均线太灵敏,震荡中频繁翻转,只吃到同期指数涨幅的一小段。
# 3. Propose
# 放慢到 10/20:过滤噪声,持有趋势更久。这是灵敏度的连续调整,不是窄坑。
# 4. Patch
# 修改 strategy.py:SHORT_WINDOW=10, LONG_WINDOW=20
# 5. Evaluate
python backtest.py
# score 2.0615, return +17.33%, Sharpe 1.839, Sortino 2.062, drawdown 6.71%, 7 trades
# 6. Decide
# score 严格提高,关键指标不退化,交易笔数仍可接受 → ACCEPT
# 7. Compress
# 没有冗余规则,策略保持最小。
常见错误
| 错误 | 修正 |
|---|---|
| 为了提分修改评估器 | 只改策略文件 |
| 在连续变量上加窄死区 | 使用有经济含义的连续区间或规则开关 |
| 只看 score 接受候选 | score 是主指标,但关键指标也要过门槛 |
| 用验证集调参 | 验证集只用于检查泛化,不用于指导搜索 |
| 高 score 但只有 1-2 笔交易 | 要求足够样本量 |
| 入场信号使用未来信息 | 只用已完成 bar |
| 不断叠规则不压缩 | 接受后删除无效和重叠规则 |
| 一轮改多个变量 | 每轮一个假设,保证归因清楚 |
| 忽略 train / validation 分化 | 分化过大即拒绝 |