5.1 Strategy API V2 总览


5.1 Strategy API V2 总览

本节摘要:Strategy API V2 是 QuantDinger 策略的骨架(官方模块口径)。本节先看一个最小策略类的全貌——它继承什么、覆写哪几个方法、每个方法在什么时机被调用;再把生命周期画成一张图:数据到达、产生 intent、sizing 定仓、risk 校验、订单落地、钩子收尾。最重要的分界线在 intent 与订单之间:intent 只说「我想以某方向调整某标的的敞口」,不说「下一张什么类型的单」——把成交细节交给运行时,同一份策略代码才能在回测、paper、实盘三种运行时里不改一行地跑。本节最后立两条纪律:状态最小化与幂等,它们决定策略重启后能不能恢复;配套状态自查脚本与五条排查速查表,把纪律落成工具。

学习目标

  • 说出策略类的骨架方法与各自被调用的时机。
  • 用生命周期图定位一个 bug 应该在哪一环排查。
  • 解释 intent 与订单的分界为什么是回测实盘一致性的支点。
  • 应用状态最小化与幂等两条纪律。

一、最小策略类全貌

# my_strategy.py —— Strategy API V2 最小骨架(接口写法示意,以官方文档为准) from quantdinger.strategy import BaseStrategy # 基类名示意 class SqueezeBreakout(BaseStrategy): params = {"atr_window": 14, "qtl_low": 0.2, "risk_per_bar": 0.001} # 超参集中声明 def on_start(self, ctx): # 开盘前钩子:预热数据、注册指标(第 4 章产物在此接入) self.squeeze = ctx.register_indicator("indicators_volatility") self.state = {"in_position": False, "entry_bar": None} def on_bar(self, ctx, bar): # 每根 K 线到达:产生 intent(本节核心,5.2 展开) if not self.state["in_position"] and self.squeeze.value(bar) == 1: ctx.intent_open(symbol=bar.symbol, direction="long") # 只表达意图 elif self.state["in_position"] and self.take_profit_hit(bar): ctx.intent_close(symbol=bar.symbol) # 平仓也是意图 def on_timer(self, ctx, tick): # 定时钩子:再平衡、日报,不塞进 on_bar(5.3 展开) pass def on_stop(self, ctx): # 收盘后钩子:状态落盘、对账(5.3 展开) ctx.persist(self.state) ​

骨架只有四个覆写点:启动、每根 K 线、定时、停止。策略作者的战场在 on_bar——它每根 K 线被调用一次,拿到的是已经过第 3 章数据层质检的 bar 与第 4 章算好的指标值。

二、生命周期:一个 intent 的完整路径

on_bar 里的一次完整流转(写法示意) on_bar(bar) │ 1. 指标更新(第 4 章:一处定义的指标在此出值) │ 2. 判定条件 → ctx.intent_open(...) ← intent:方向+标的,无价格无数量 │ 3. sizing:按算法把 intent 折成目标仓位 ← 5.2 节 │ 4. risk 校验:单笔限额→组合限额→熔断 ← 5.3 节;任一不过则降级或丢弃 │ 5. 运行时下单:回测撮合 / paper 记账 / 实盘单 ← 三种运行时只在这一步不同 ▼ on_stop / on_timer 收尾:状态落盘、对账、日报 ​

第 5 步是理解整个 API 的钥匙:回测的撮合器、paper 的记账器、实盘的适配器(第 8 章)实现同一个接口。策略代码停在第 4 步之前,所以三种运行时共用一份代码——这就是「intent 是唯一能共享的东西」的含义。

排查 bug 时按图索骥:信号没出现查第 1~2 环(指标与条件,回第 4 章);仓位不对查第 3 环;订单没发出去查第 4 环(风控拦截日志);成交价不对查第 5 环(回测撮合假设,第 6.1 节)。

三、intent 与订单:分界线画在哪

维度 intent(策略的词汇) 订单(运行时的词汇)
表达 方向、标的、紧迫度 类型、价格、数量、有效期
谁关心 策略作者 回测撮合器/paper 记账/交易所适配器
回测实盘差异 无 全部差异集中于此
例子 「做多 BTC 敞口」 「买 0.03 BTC,限价,只挂 5 分钟」

这条分界线的工程收益是三级复用:同一策略进回测(第 6 章)、进 paper(第 7 章)、进实盘(第 8 章);成本是策略作者要克制——不要在 on_bar 里琢磨「挂单还是市价」,那是运行时与 5.3 节风控的事。

四、两条纪律:状态最小化与幂等

trading-worker 会重启(第 2 章),策略必须活过重启。两条纪律:

其一,状态最小化。策略自己保存的状态越少,恢复越可靠。仓位、成交、余额这些事实由运行时持有并按需查询,策略只留无法重建的最小集合(如「本轮是否持仓」)。判断标准:写下你的每个状态字段,问一句「重启后能不能从运行时查回来」——能,就删掉。

其二,幂等。on_bar 可能被重放(恢复时补跑漏掉的 K 线),同一段数据跑两遍必须得到同样的 intent。落地做法:判定条件只用「当前 bar 及之前」的数据(不要偷看下一根);有状态字段时先判再改;外部调用(查余额、查持仓)放在判定之后、动作之前,避免重放时产生两次副作用。

状态最小化可以脚本化自查(示意实现):

# state_audit.py —— 策略状态最小化自查(纯标准库) RUNTIME_QUERYABLE = {"position", "equity", "balance", "open_orders"} # 运行时可查(示意集合) def audit_state(state_fields): """对策略自持状态逐字段判级:可删 / 需解释 / 必须保留""" report = [] for field in state_fields: if field in RUNTIME_QUERYABLE: report.append((field, "可删:运行时持有,重启后查得回")) elif field.endswith("_cache"): report.append((field, "可删:缓存可由历史数据重建")) else: report.append((field, "必须保留:写下重启后如何恢复它")) return report if __name__ == "__main__": for field, verdict in audit_state(["position", "entry_bar", "atr_cache", "regime"]): print(f"{field}: {verdict}") ​

判级为「必须保留」的字段要能回答两问:它何时被写入(on_stop 落盘了吗)、重启后从哪里读回(on_start 恢复了吗)。答不上任何一问,它就不是状态,是隐患。

幂等的自检问句压缩成三行:

自检问 通过标准
判定是否只用当前 bar 及之前的数据 是
同一根 K 线重放两次,intent 是否相同 相同
外部调用(查持仓、查余额)的位置 在判定之后、动作之前

五、常见问题与排查

把「按生命周期图定位 bug」的对应关系补全成速查表:

症状 先查哪一环 去哪章
on_bar 迟迟不被调用 数据没到:闭合 K 线落库了吗 第 3 章
指标值一直是 None 预热不足:窗口没攒够 第 4 章与 5.3 节 on_start
有信号但没成交 风控拦截或运行时未消费 5.3 节拦截日志、第 2.3 节
重启后仓位「失忆」 状态过重或没走 persist 本节第四部分自查脚本
回测与实盘行为不同 先怀疑数据口径,再怀疑代码 第 3 章质检、第 6.1 节撮合假设

本节要点回顾

  • 骨架四个覆写点:on_start / on_bar / on_timer / on_stop,战场在 on_bar。
  • 生命周期五步:指标→intent→sizing→risk→订单;策略停在第 4 步之前。
  • intent 与订单的分界线,让回测、paper、实盘共用一份策略代码。
  • 状态最小化与幂等是策略活过重启的两条纪律。
  • 排查速查:数据、预热、拦截、状态、口径——五个症状对应五条去路。

骨架有了,接下来填前两环:intent 到底有哪几种说法,sizing 怎么把「想做多」折成具体仓位。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U