本节摘要:本节展开生命周期的前两环。intents 部分给四种意图的语义与用法:开仓(建立敞口)、平仓(清零敞口)、调仓(增减现有敞口)、观望(显式不动作)——「观望也是意图」是容易被忽略的设计,它让每根 K 线的决策都有记录,回测复盘时才能区分「没信号」与「有信号没触发」。sizing 部分按三档进阶:固定手数(原型期的 1 手起步)、波动率反比(同一风险预算下,波动大的标的少买)、波动率目标仓位(把「每根 K 线愿意亏多少」反推成仓位)。三档共用一个原则:仓位是风险的函数,不是信号的函数——信号只回答方向,风险预算回答数量。示例代码把 4.2 节的收缩事件接成完整策略,末尾附超参台账与高频疑问。
| intent | 语义 | 典型触发 | 常见误用 |
|---|---|---|---|
| 开仓(intent_open) | 从零建立敞口 | 空仓时信号出现 | 已持仓再开仓(应走调仓) |
| 平仓(intent_close) | 敞口清零,含止损止盈 | 止盈条件、持有期满 | 与「调仓减半」混用 |
| 调仓(intent_resize) | 增减现有敞口 | 再平衡、金字塔加码 | 想清仓时只调到零附近 |
| 观望(intent_hold) | 显式维持现状 | 条件不满足的每根 K 线 | 省略不写,决策无痕 |
「观望也是意图」值一页篇幅:回测复盘最常见的死胡同是「这根 K 线为什么没交易」——如果观望不落痕,答案永远丢失。显式 hold 让每根 K 线都有一条决策记录,第 6 章的回测报告才能统计「信号出现但被风控拦截」与「信号根本没出现」这两类完全不同的事件。
落到纸面上,留痕长这样(示意):
同一策略相邻三根 K 线的决策记录 bar N-2 intent_hold 条件不满足 ──▶ 有记录:没信号 bar N-1 intent_open 收缩+突破 ──▶ 有记录:信号与 sizing 依据 bar N intent_hold 持仓中,止损未触发 ──▶ 有记录:继续持有 复盘问题「这根为什么没交易」在记录里直接可查,不靠记忆与猜测
第一档,固定手数。每次开仓买固定数量(例如 0.01 BTC,示意值)。它的唯一优点是简单,适合原型验证;致命伤是风险不均——同一手数在低波动与高波动标的上的每笔风险差几倍,组合层面等于把风险交给运气。
第二档,波动率反比。仓位与波动率成反比,把 4.2 节的 ATR 直接用起来:
# sizing_by_vol.py —— 第二档:波动率反比仓位(示意实现) def size_by_volatility(equity, risk_fraction, atr, price, min_qty=0.001): # risk_fraction:单笔愿意亏掉的权益比例(示意值,如 0.01) # 逻辑:价差按 2 倍 ATR 估算止损距离(示意),仓位=风险额/止损距离 stop_distance = 2.0 * atr risk_amount = equity * risk_fraction qty = risk_amount / stop_distance if stop_distance > 0 else 0.0 return max(qty, min_qty) if price > 0 else 0.0
第二档的关键转变:仓位不再问「信号多强」,而是问「止损距离多远、愿意亏多少」。同样的风险预算,黄金与山寨币拿到截然不同的手数——这是第一档做不到的公平。
第三档,波动率目标仓位。不再盯单笔,而是盯组合的目标波动率:按近期已实现波动率反推整体敞口,再分配到各标的。它解决了「三个标的都用第二档、总风险仍是三倍」的叠加问题,代价是要维护组合层的波动估计。三档的关系是包含而非替代:第三档内部仍用第二档分配单笔,方法论详见《量化投资方法论》第 13 章《仓位管理:ATR、凯利与风险预算》。
第三档的核心一步写成函数(示意实现,非投资建议):
# sizing_by_target_vol.py —— 第三档:波动率目标缩放(示意实现) def target_vol_scale(realized_vol, target_vol, floor=0.0, cap=2.0): """按已实现波动率与目标波动率之比缩放整体敞口""" if realized_vol <= 0: return 1.0 # 波动率测不出来时不动,交组合层兜底 scale = target_vol / realized_vol return max(floor, min(cap, scale)) # 上下限截断,防极端值打出荒谬仓位
上下限截断值得强调:波动率估计在数据匮乏或市场骤变时可能给出极端读数,无截断的缩放会顺势打出极端仓位——第三档的复杂度不是白来的,它的每个自由度都需要第 6 章的检验来背书。
把 4.2 节的收缩事件接进骨架,配上第二档 sizing:
# squeeze_breakout.py —— 收缩突破策略(第 4.2 节指标 + 5.2 节 sizing) from quantdinger.strategy import BaseStrategy # 基类与 ctx 接口写法示意,以官方文档为准 class SqueezeBreakout(BaseStrategy): params = {"risk_fraction": 0.01, "atr_mult": 2.0} def on_start(self, ctx): self.sq = ctx.register_indicator("indicators_volatility") def on_bar(self, ctx, bar): pos = ctx.position(bar.symbol) # 仓位是运行时的事实,不自己记 atr = self.sq.atr(bar) if pos.quantity == 0: if self.sq.squeeze(bar) == 1 and bar.close > self.sq.prev_high(bar, n=20): qty = self._qty(ctx, bar, atr) # sizing 在 intent 之后运行时折算 ctx.intent_open(symbol=bar.symbol, direction="long") elif bar.close <= pos.entry_price - self.params["atr_mult"] * atr: ctx.intent_close(symbol=bar.symbol) # 止损:平仓意图 else: ctx.intent_hold(symbol=bar.symbol) # 观望也留痕 def _qty(self, ctx, bar, atr): equity = ctx.account().equity return max((equity * self.params["risk_fraction"]) / (self.params["atr_mult"] * atr), 0.001)
三处值得注意:仓位从运行时查询(5.1 节状态最小化);sizing 的数量在 intent 之后由运行时按同样参数折算(示例里 _qty 仅示意策略侧如何给出依据);止损写成平仓意图而不是直接发单——「止损单的挂法」属于运行时与 5.3 节风控的职责。
本节出现了四个超参:收缩分位数(4.2 节)、止损倍数、风险比例、突破回看窗口。它们全部集中在 params 声明——这不是洁癖,而是第 6 章的入场券:walk-forward 与过拟合检验(第 6.2 节)的对象就是这些参数。散落在代码里的魔法数字会让一切检验失效。
把四个超参列成台账(示意结构):
| 超参 | 来源章节 | 当前值(示意) | 检验归属 |
|---|---|---|---|
| 收缩分位数 | 4.2 节 | 0.2 | walk-forward 稳健性 |
| 止损 ATR 倍数 | 本节 | 2.0 | walk-forward 与成本压力测试 |
| 单笔风险比例 | 本节 | 0.01 | 组合层风控约束(5.3 节) |
| 突破回看窗口 | 本节 | 20 | PBO 审查的参数维度之一 |
台账的意义在第 6.2 节会显形:PBO 与 Deflated Sharpe 要求你如实申报「试了多少」——没有台账,申报只能靠回忆,而回忆总是偏乐观。
| 疑问 | 处置 |
|---|---|
| sizing 算出来是 0 | 检查量纲:权益、价格、ATR 是否同币种同单位;再看最小数量约束(示意) |
| 已持仓又出信号怎么办 | 走调仓语义,不要再开仓(对照语义表的误用列) |
| 观望意图会不会淹没日志 | 它是决策记录不是异常;报告层按需聚合即可 |
| 一定要用第三档吗 | 原型期第一档、单标的第二档、组合层才有第三档的事 |
| 止损为什么写成平仓意图 | 挂单方式是运行时与风控的职责(5.3 节),策略只表达意图 |
意图与仓位都会了,但这一路还没人拦着——下一节补上风控这两道闸门,以及运行时钩子里的收尾工作。