5.3 risk 与运行时


5.3 risk 与运行时

本节摘要:生命周期最后两环。risk 部分是两道闸门:单笔层(止损、止盈、最大单笔仓位)在 intent 产生的瞬间校验,组合层(总敞口上限、同向集中度、回撤熔断)在折算成目标仓位后校验;拦截不是丢弃那么简单——按「降级、拆分、拒绝」三档处置,并且每次拦截都落日志,因为「被拦掉的交易」也是回测报告的一部分。运行时部分是三个钩子的分工:开盘前预热数据、定时钩子做再平衡与日报、收盘后对账与状态落盘;凡是「不是针对当前 K 线做决策」的事,一律离开 on_bar。本节末尾把断线重连、部分成交、重启恢复三个实战场景过一遍,附钩子判定器与高频排查表,策略至此才算完整。

学习目标

  • 配置单笔与组合两层风控,并说出各自的校验时机。
  • 按「降级、拆分、拒绝」三档设计拦截处置与日志。
  • 把对账、日报、预热安放进正确的运行时钩子。
  • 处理三个实战场景:断线重连、部分成交、重启恢复。

一、两道闸门:单笔层与组合层

intent 的风控路径(拦截点示意) intent_open("BTC", long) │ ▼ ┌─────────────────────────┐ 不过 → 处置三档之一 + 拦截日志 │ 单笔层:止损距离可接受? │ │ 止盈比例合理? │ │ 单笔仓位 ≤ 上限? │ └───────────┬─────────────┘ ▼ 通过 ┌─────────────────────────┐ 不过 → 处置三档之一 + 拦截日志 │ 组合层:总敞口 ≤ 上限? │ │ 同向集中度 ≤ 上限? │ │ 回撤中?熔断开关状态? │ └───────────┬─────────────┘ ▼ 通过 → 运行时下单(回测撮合 / paper / 实盘) ​

两层的分工用一句话记:单笔层管「这一笔会不会要命」,组合层管「很多笔加起来会不会要命」。参数示例(全部示意值,须按品种与账户规模重估):单笔风险不超过权益的 1%、总敞口不超过权益的 100%、同方向不超过总敞口的 60%、组合回撤超过 10% 触发熔断(暂停开新仓、只允许平仓)。度量口径见《量化投资方法论》第 14 章《风险控制与绩效度量》。

二、拦截处置:降级、拆分、拒绝

处置 含义 适用
降级 把仓位缩到限额内放行 超出单笔上限但方向仍有效
拆分 拆成多笔分批建仓 流动性或风控上限约束
拒绝 丢弃 intent 熔断期间的开仓、止损无效的单

两条配套纪律:其一,处置规则要确定(同一场景永远同一处置),否则回测无法复现实盘行为;其二,拦截日志与成交日志同级保存——第 6.1 节回测报告里「信号出现但被拦截」的统计就来自这里,它也是排查「策略变慢变钝」的第一现场。

# risk_gates.py —— 两层风控的最小实现(示意) def pass_risk(intent, target_qty, account_state, limits): # 单笔层 if target_qty > limits["max_per_order"]: target_qty = limits["max_per_order"] # 处置:降级 log("降级", intent, reason="单笔上限") # 组合层 projected = account_state["gross_exposure"] + target_qty * intent.price if projected > limits["max_gross"]: if account_state["circuit_breaker"]: log("拒绝", intent, reason="熔断期间禁止开仓") return None allowed = (limits["max_gross"] - account_state["gross_exposure"]) / intent.price if allowed <= 0: log("拒绝", intent, reason="总敞口已满") return None log("降级", intent, reason="总敞口约束") target_qty = allowed return target_qty ​

三、三个钩子的分工

on_start(开盘前):预热历史 K 线让指标出值(4.2 节的分位数需要 120 根历史)、校验配置、声明本交易日将用的数据源。原则:凡是为「今天的第一根 K 线就能决策」做的准备都在这里。

on_timer(定时):再平衡检查、日报生成、波动率估计的定期更新。原则:周期性但不随 K 线走的动作放这里,避免 on_bar 越来越胖。

on_stop(收盘后):对账(本地状态与运行时核对)、状态落盘(5.1 节的最小状态集合)、次日参数生效检查。原则:一天结束前把「重启也不丢」的东西写完。

一句话分工:on_bar 只做决策,其余一切进钩子。判断某个逻辑放哪,就问「它需要当前这根 K 线吗」——不需要,就不属于 on_bar。

「放哪」的判断可以写成一个小函数当团队速查(示意):

# hook_placement.py —— 「这段逻辑放哪个钩子」判定器(纯标准库) def place(needs_bar, is_periodic, is_lifecycle): """三个问句依次回答 True / False(示意判定顺序)""" if needs_bar: return "on_bar:逐 K 线决策" if is_periodic: return "on_timer:周期性维护" if is_lifecycle: return "on_start / on_stop:启动准备或收尾" return "重新考虑:可能不属于策略,交给运行时或外部任务" if __name__ == "__main__": print(place(True, False, False)) # 止损判定 print(place(False, True, False)) # 波动率估计更新 print(place(False, False, True)) # 指标预热 print(place(False, False, False)) # 发通知 ​

最后一行是判定器的隐藏价值:它逼你承认有些逻辑根本不该住在策略里——通知、扩容、外部对账这类事,属于平台与运维层。策略类保持「只做决策」,是它能在三种运行时里不改一行跑起来的前提之一。

四、三个实战场景

场景一,断线重连。行情断流期间 K 线缺失,重连后运行时会补发(第 3.3 节的数据回填),策略侧要保证幂等(5.1 节):补发的旧 K 线重放不产生重复 intent。切忌在策略里自己写「断线就立刻平仓」——这是组合层熔断的职责(配合第 10 章的应急开关),策略层自作主张会让回测与实盘行为分叉。

场景二,部分成交。实盘中 intent 折算的订单可能只成交一部分。处置原则:成交事实以运行时为准,策略下一次 on_bar 拿到的 position 已经反映部分成交;不要在策略里追踪「我还有多少没成交」——那是重放不幂等的状态。

场景三,重启恢复。trading-worker 重启(第 2 章)后 on_start 重新预热,策略从落盘的最小状态与运行时持仓恢复。恢复后第一件事不是交易,是对账:落盘状态、运行时持仓、交易所实际持仓三方核对(第 8.3 节),对得上才继续。

重启恢复后的核对清单(示意,对得上才继续交易):

核对项 左侧 右侧
持仓 落盘的最小状态 运行时 position
敞口 策略视角的组合敞口 风控视角的组合敞口
挂单 本地订单表待处理记录 交易所(或 paper 记账)侧在途单
时间 本地时钟与数据时间戳 数据源时间口径(3.1 节)

五、常见问题与排查

疑问 排查方向
怎么知道 intent 被拦了 拦截日志与成交日志同级保存,先查它;报告里的拦截统计同源
熔断触发后怎么解除 复位是人工确认动作,不建议做成自动解除(示意纪律)
部分成交后仓位对不上 以运行时 position 为准;策略自记成交量即不幂等
on_timer 迟迟不触发 查运行时定时配置与第 2 章节拍器链路(beat 投递、worker 领取)
降级和拆分怎么选 流动性约束选拆分,限额约束选降级;规则要事先写死

本节要点回顾

  • 两道闸门:单笔层管这一笔,组合层管总和;拦截按降级、拆分、拒绝三档处置并落日志。
  • 钩子分工:开盘前准备、定时维护、收盘后对账落盘;on_bar 只做决策。
  • 断线靠数据回填加幂等,部分成交以运行时为准,重启后先对账再交易。
  • 放哪三问:需要当前 K 线吗、周期性吗、生命周期收尾吗——都不是就移出策略。
  • 本章策略骨架至此完整,下一章进回测考场。

策略会做了、也会自我保护了。但它到底行不行,要交给数据说话——第 6 章回测与实验,把这份代码放进历史行情里检验。


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