本节摘要:策略规则回答"条件是否满足",回答不了"此时此地是否合适"。本节讲 QuantDinger 预交易决策门的两个核心构件:Decision Context V2 与 AI Decision Filter。前者是下单前组装的判断上下文——当前持仓、待发信号、市场状态、近期绩效、限额余量,把"当下"浓缩成决策服务可读的一份材料;后者消费这份材料,返回类型化的 Choice:同意(原单发出)、否决(本单拦截)、缩小规模(按系数打折后发出)。本节给出整体架构图、上下文字段表、三种 Choice 的语义与统计口径,以及启用配置示例。决策服务的判断型问题形式化,对应《Jev 决策编程》第 03 章《三种问题类型》中的判断题类型,本节只讲交易场景的落地。
第 05 章的策略模块里已经有 risk 约束、有 sizing 规则,为什么还需要一道门?因为它们的职责不同:
| 模块 | 判断方式 | 回答的问题 | 例子 |
|---|---|---|---|
| 策略 intents | 机械规则 | 触发条件满足了吗 | 均线金叉 |
| sizing | 公式计算 | 该下多大的量 | 按波动率倒数的仓位 |
| risk | 硬约束 | 这单违规了吗 | 敞口不得越限 |
| 决策门 | 语境判断 | 此时此地发这单合适吗 | 持仓已重+今晨信号连续被拒+市场波动异常 |
第四行的问题写不进规则,因为它需要把多个分散的语境信息放在一起权衡——这正是《Jev 决策编程》第 03 章《三种问题类型》里"判断题"的特征:给定状态,输出带置信度的分类判断,而不是可枚举的规则匹配。决策门不是替代前三者,是站在它们全部通过的下游做最后一道语境审查。
Decision Context V2 是决策门的名字里"Context"的实体:下单前由 trading-worker 组装的一份结构化材料(字段构成以官方文档口径为准)。典型字段:
| 字段组 | 内容 | 回答什么 |
|---|---|---|
| 持仓快照 | 当前各标的持仓、成本、未实现损益 | 已经背了多少风险 |
| 待发信号 | 本单的方向、标的、数量、策略标识 | 正要做什么 |
| 市场状态 | 波动水平、流动性指标、近段价格行为 | 环境是否异常 |
| 近期绩效 | 该策略近段滚动表现、被门拦截的历史 | 策略是否处在失效期 |
| 限额余量 | 风控限额的已用与剩余 | 还有多少空间 |
组装原则有三条。其一,快照而非全量:给判断者的是当下的浓缩状态,不是整段历史——历史信息以"近期绩效"这类聚合形式进入。其二,字段与决策相关:上下文不是越大越好,塞进无关字段会稀释判断质量。其三,可追溯:每次组装的上下文应可落盘(第 11 章可观测层),否则三个月后你无法回答"当时门看到了什么才放行这单"。
决策门整体架构 trading-worker 决策服务 交易所适配器 ┌─────────────────┐ ┌──────────────┐ ┌─────────────┐ │ 策略产出 intents │ │ AI Decision │ approve │ 真实/虚拟 │ │ risk/sizing 通过 │─Context──▶ │ Filter(Jev) │──────▶────│ 订单(第08章) │ │ 组装 Context V2 │ │ │ reduce │ │ │ │◀─Choice───│ typed Choice │──────▶────│ 缩量后发出 │ └─────────────────┘ └──────┬───────┘ veto └─────────────┘ │ 拦截并记录 ▼ 决策日志(可审计)
架构里有一个容易忽视的细节:被否决的订单也要完整记录(信号、上下文、choice、理由)。否决记录是第 07 章 signal-only 观察的重要材料——门的拦截分布本身是策略质量的镜子。
决策门的输出是类型化结果(typed Choice),不是自由文本:
| Choice | 语义 | 下游动作 | 统计口径 |
|---|---|---|---|
| approve(同意) | 语境无碍,放行 | 原单发出 | 同意率 |
| veto(否决) | 语境不支持此单 | 本单拦截、记录 | 否决率、否决理由分布 |
| reduce(缩小规模) | 方向可取但规模过大 | 按系数缩量后发出 | 缩量率、平均缩量系数 |
三种结果的统计口径值得在观察期建立起来。健康的门,否决率应该在个位数到两成之间(示意经验值,取决于策略风格):否决率趋近零说明门形同虚设或策略已足够保守,趋近一半以上说明策略与门的判断标准脱节——两种情况都要查,而不是简单调门的松紧。
特别说明 reduce 的边界:缩量保护的是"规模失配"(仓位与语境不匹配),不修补"方向错误"——方向错误应该换来 veto。把 reduce 当成万能折扣码,等于让门退化成一个打折器。
(示意结构,字段名以官方文档为准。)
# decision_gate.yaml —— 预交易决策门配置(示意,字段以官方文档为准) decision_gate: enabled: true engine: jev # 默认引擎;替换选项见 9.3 节 context_version: v2 scope: pre_trade # 门的位置:下单前 on_unavailable: fail_open # 服务不可用时的策略,9.2 节展开 timeout_ms: 300 # 延迟预算(示意值),9.2 节展开 record: context: true # 上下文落盘,供审计 choice_log: true # choice 与理由落盘 observe_only: false # true=只记录不影响订单(影子模式起步建议)
启用节奏建议分两步走。第一步,observe_only 打开跑一至两周(量级建议):门只记录 choice、不改订单,与无门基线对比,确认门的判断分布符合预期、延迟可接受。第二步,关掉影子模式真正生效,先在 paper 或小资金实盘观察。直接全量生效的问题在于:你无法区分"策略表现变化"来自市场还是来自门,影子期就是控制变量。
影子期的产出是一份决策日志,统计工具让分布可见(纯标准库):
# choice_stats.py —— 决策日志的 choice 分布统计(影子期观察工具) from collections import Counter def gate_stats(rows): """rows: [{strategy, choice, reason}],choice 取 approve/veto/reduce""" stats = {} for r in rows: s = stats.setdefault( r["strategy"], {"n": 0, "choice": Counter(), "veto_reason": Counter()}) s["n"] += 1 s["choice"][r["choice"]] += 1 if r["choice"] == "veto": s["veto_reason"][r["reason"]] += 1 out = {} for name, s in stats.items(): out[name] = { "total": s["n"], "veto_rate": round(s["choice"]["veto"] / s["n"], 3), "reduce_rate": round(s["choice"]["reduce"] / s["n"], 3), "top_veto_reasons": s["veto_reason"].most_common(3), } return out
读这份报告的顺序有三条:先看 total 是否积累到可判读的量级(样本不够时一切比率免谈,与第 06 章回测的笔数直觉同源);再看 veto_rate 落在哪个区间,对照上节的示意经验值;最后才看 top_veto_reasons——理由聚类是调松紧之前的事,理由结构没变而只调松紧,等于不看病换药。
| 现象 | 优先排查方向 | 处理要点 |
|---|---|---|
| 影子期否决率长期为零 | 门是否真的在工作、策略是否本就保守 | 查决策日志条数;对照无门基线有无差异 |
| 启用后信号延迟明显变大 | Context 组装耗时、timeout 设置 | 按 9.2 节延迟预算重新分配 |
| reduce 后出现碎单 | 缩量系数未设下限 | 检查消费侧下限逻辑(9.2 节) |
| 上下文落盘占用过大 | 字段塞进了无关大对象 | 回组装原则二:字段与决策相关 |
| 多策略共用一份门配置 | 各策略的否决率基线不同 | 按策略分别统计、分别判读 |
这张表的第一行最值得警惕:零否决率看起来"门很满意",实际上有两种完全相反的解释——门形同虚设,或者策略确实保守到门无事可做。区分办法是抽几笔门放行的订单人工复评:人工也觉得都该放行,是后者;人工能挑出该拦的,是前者。判读门与判读策略一样,不能只看比率不看样本。
门给出的 Choice 是一份"意见书",不是"命令行"—— approve 也好、reduce 也好,最终怎么落到订单上仍由你的消费策略决定。下一节讨论消费的细节:置信度阈值怎么设、弃权怎么处理,以及那个最重要的设计问题:决策服务不可用时,交易是停是走。