本节摘要:拿到 typed Choice 之后的工作是消费它:approve 什么时候照单放行、reduce 的系数怎么落到订单、veto 与弃权怎么区分处理——本节给一张消费策略表和一段骨架代码。然后是本章最重要的设计问题:决策服务不可用时怎么办。QuantDinger 的默认答案是 fail-open——放行不卡死,让交易主干在决策服务故障时继续运转;本节解释这个选择的可用性逻辑,同时划出应当改为 fail-closed 的场景清单(大额、新策略首日、极端行情),并给出延迟预算的分配方法。一切的前提是一个认知:门是增强件,不是依赖件——它应该让系统更好,而不是让系统多一种死法。
Choice 附带置信度信息(Jev 的输出特征,详见《Jev 决策编程》第 03 章《三种问题类型》),消费策略就是"在什么置信度下执行什么动作":
| Choice | 置信度姿态 | 消费动作 | 说明 |
|---|---|---|---|
| approve | 置信度高于阈值 | 照单放行 | 低置信度的同意可按弃权处理(见下) |
| approve | 置信度低于阈值 | 按弃权处理 | 宁可不用门,不要门硬给意见 |
| veto | 置信度高于阈值 | 拦截并记录 | 理由字段入决策日志 |
| veto | 置信度低于阈值 | 拦截但标记为弱否决 | 观察期单独统计弱否决的事后对错 |
| reduce | 任意可信置信度 | 按系数缩量后发出 | 系数设下限,防止缩到零星碎单 |
骨架代码(示意逻辑,接口字段以官方文档为准):
# consume_choice.py —— Choice 消费骨架(示意逻辑) from dataclasses import dataclass APPROVE_MIN_CONF = 0.7 # 同意所需的最低置信度(示意值) REDUCE_FLOOR = 0.25 # 缩量系数下限,低于则并入否决(示意值) @dataclass class Choice: decision: str # approve / veto / reduce confidence: float # 置信度 scale: float # 仅 reduce 时有效 def consume(choice: Choice, order_qty: float) -> float: """返回最终下单数量;0 表示本单不发""" if choice.decision == "approve": return order_qty if choice.confidence >= APPROVE_MIN_CONF else order_qty * 0.5 if choice.decision == "reduce": factor = max(choice.scale, REDUCE_FLOOR) return order_qty * factor if choice.confidence >= APPROVE_MIN_CONF else 0.0 return 0.0 # veto:拦截
两个设计细节。其一,代码里"低置信度的同意"折半放行而不是照单全收:对拿不准的意见,折中使用比二选一更稳。其二,缩量系数设下限:把一张单缩到最小步长以下,产出的碎单只贡献手续费与对账复杂度——低于下限就该干脆否决。这些数值全是示意值,正确的取值来自你影子期(9.1 节)的分布统计,而不是抄任何人的默认值。
fail-open 的定义:决策服务超时、报错、不可达时,决策门当作"通过"处理,订单照发。QuantDinger 采用这一默认设计(官方文档口径),逻辑如下:
fail-open 的可用性权衡 决策服务故障时: fail-open:订单继续发出 ──▶ 系统退化为"无门的交易系统" fail-closed:订单全部拦截 ──▶ 系统退化为"停止交易的系统" 隐含判断:对已通过第06~08章全链路验证的策略, "无门运行"的风险低于"停摆运行"的风险
这段话的要点是"隐含判断"四个字:fail-open 不是说门不重要,而是说在故障瞬间,两个坏选项里选了伤害较小的那个。它的代价真实存在——故障窗口内的订单失去了语境审查,所以故障本身必须被发现:fail-open 生效时要触发告警(第 11 章)并记录故障窗口,事后把窗口内的订单拉出来人工复盘。fail-open 加告警,才是完整的默认策略;只抄 fail-open 不配告警,是把容错做成隐瞒。
故障窗口的复盘清单(告警触发后照单执行):
| 步骤 | 动作 | 产出 |
|---|---|---|
| 1 | 从告警记录确定窗口起止时间 | 故障窗口时间线 |
| 2 | 拉出窗口内发出的全部订单 | 订单清单与各自的 Context 快照 |
| 3 | 逐单补做一次语境审查(人工或重放) | 哪些单在有门时可能被拦或被缩 |
| 4 | 复盘故障根因与恢复耗时 | 事件记录,供第 11 章指标复盘 |
| 5 | 按第 3、4 步结论调整分层配置 | 是否把该场景升级为 fail-closed |
第 3 步是这套流程的灵魂:fail-open 放行的单不能放任不管,事后补审把"故障期间失去的审查"补回来一部分;更重要的是,它是分层配置的数据来源——哪些场景在故障时真的出过事、哪些从没出过,靠一次次的窗口复盘积累,第 5 步的调整才会越来越有据,而不是拍脑袋把阈值越调越紧。
fail-open 的隐含判断对"已充分验证的策略"成立,以下场景它不成立,应改为 fail-closed(订单拦截):
| 场景 | 为什么不该 fail-open | 配置建议 |
|---|---|---|
| 单笔金额大 | 一单失控的代价高于停摆代价 | 按单笔规模分层:超阈值即 fail-closed |
| 新策略首日 | "已验证"前提不成立 | 新接入策略默认 fail-closed |
| 极端行情 | 正是门存在理由的时段 | 波动指标越限联动切换 |
| 决策门近期高否决率 | 门在拼命拦,说明语境恶化 | 连续多单否决后自动升级 |
实践形态是分层配置:默认 fail-open,按单笔规模、策略接入时长、市场状态三层条件局部升级为 fail-closed(配置能力以官方文档为准)。这比全局 fail-closed 可用,比全局 fail-open 安全。
一句话记住分界:fail-open 赌的是"门坏了没关系",fail-closed 赌的是"门拦住的一切都该拦"——你的系统该在哪一段下什么注,取决于那段订单的失配成本。
决策门在下单路径上,它的耗时直接叠加进 7.2 节的信号滑出。预算方法:先测量无门链路的下单延迟分布,再给门分配一个不超过总预算某个比例的份额(示意做法:十分之一到五分之一),超时即按 fail 策略处理:
| 环节 | 预算份额(示意) | 说明 |
|---|---|---|
| 信号到订单构造 | 大头留给原有链路 | 决策门不该是延迟主因 |
| 决策门调用(含 Context 组装) | 约一成到两成总预算 | timeout_ms 据此设定 |
| 超时处理 | 触发 fail 策略 | fail-open 放行+告警,或分层升级 |
两个配套动作。其一,timeout_ms 一旦设定就进监控:门的 P99 耗时逼近超时值之前就要告警,而不是等超时雪崩。其二,影子期同时测延迟分布,把 9.1 节"影子模式跑一至两周"的观察项加上延迟这一条——分布不稳的引擎配置(例如过大的上下文字段)先修再上线。
| 问题 | 排查方向 | 要点 |
|---|---|---|
| 影子期数据怎么变成阈值 | 统计各 choice 的置信度分布 | 阈值取分布的稳定分界,不抄默认值 |
| timeout_ms 设多少合适 | 无门链路延迟分布加预算份额 | 见本节预算表;P99 逼近超时前告警 |
| fail-open 窗口内的单要不要撤 | 逐单补审结果 | 有据地留或撤,不搞一刀切全撤 |
| 弱否决统计有什么用 | 观察期单独统计事后对错 | 弱否决的正确率是门校准的输入 |
| 缩量后订单仍被风控拦 | 缩量系数与最小下单量关系 | 下限要高于最小步长,防碎单 |
| 分层条件越加越多记不住 | 分层逻辑成文 | 写进配置注释与值班手册,评审变更 |
这张表的第二行常被反过来做:先设一个"看起来合理"的超时值,再让链路去适应它。正确方向是无门链路的实测分布在前、预算份额在后——超时值是推导出来的,不是设定的。第六行则是分层策略的长期隐患:四类 fail-closed 场景叠加三层条件之后,没人能凭记忆说清"这笔单此刻走的是哪条 fail 路径",所以分层逻辑必须成文并随配置一起评审,排查时才能按图索骥。
门装好了、消费策略定了、故障预案有了,最后一个问题是:门后面的引擎用谁的。下一节把选项摆开——默认的 Jev 托管最省事,协议同构的 Kev 与 Laya 可以自托管换来自主与数据不出域,选型表与切换验证清单都在那里。