程序员量化交易实战 44:执行闸门
原创 · 约 12 分钟阅读 · 阅读 --
Last updated on

程序员量化交易实战 44:执行闸门

作者: Alex Xiang


程序员量化交易实战 44:执行闸门

古董级程序员,大厂出来后一直在创业公司,现在仍活跃在一线做 AI 相关的开发。这个专栏会把一个 A 股量化平台从 0 到 1 拆开写:数据、策略、回测、模拟盘、提醒和生产化,尽量用真实代码和真实运行结果说话。更完整的更新也会同步到微信公众号「字与码」。

第 43 篇把每日运行包装成命令响应,但它还没有解决一个更敏感的问题:什么时候允许真实执行。

在模拟盘系统里,dry_run_ready 很容易被误解成“ready”。如果命令层只看 exit code,dry run 可能被错误地接到真实执行流程。第 44 篇把这个判断集中成执行闸门。

ZiCode 工程师检查每日运行执行闸门

执行决策

第 44 章新增 app/execution_guard.py

@dataclass(frozen=True)
class ExecutionDecision:
    allowed: bool
    reason: str
    required_actions: tuple[str, ...]

allowed 是机器判断,reasonrequired_actions 是给人和告警系统看的。

plan 状态allowedreasonrequired_actions
readytrueready
dry_run_readyfalsedry_rundisable_dry_run
blockedfalsewarningblocker失败动作列表

判断逻辑

核心函数如下:

def decide_execution(plan: DailyRunPlan) -> ExecutionDecision:
    if plan.result.status == "ready":
        return ExecutionDecision(allowed=True, reason="ready", required_actions=())
    if plan.result.status == "dry_run_ready":
        return ExecutionDecision(allowed=False, reason="dry_run", required_actions=("disable_dry_run",))
    if plan.failure_actions:
        return ExecutionDecision(
            allowed=False,
            reason=plan.action_summary,
            required_actions=tuple(action.action for action in plan.failure_actions),
        )
    return ExecutionDecision(allowed=False, reason="not_ready", required_actions=("manual_review",))

注意这里没有让 dry_run_ready 通过。它代表检查通过,但仍处于演练模式。

稳定日志格式

执行决策也有一行格式:

def execution_decision_line(decision: ExecutionDecision) -> str:
    actions = ",".join(decision.required_actions) if decision.required_actions else "none"
    return f"allowed={str(decision.allowed).lower()} reason={decision.reason} actions={actions}"

warning 场景输出:

allowed=false reason=warning actions=inspect_archive

这一行适合放在 artifact、告警或命令响应之后,告诉值班的人为什么没执行。

接到可运行示例

本章继续使用同一个命令查看执行闸门:

uv run python -m scripts.chapter_examples paper-command

本章对应的输出如下:

执行闸门命令输出

这次输出里,allowed=false 是最核心的判断。前面第 43 篇的命令已经用 exit_code=1 告诉调度器这次运行失败;执行闸门再明确告诉真实执行层:不要下单,不要更新模拟盘账户,不要触发后续交易动作。

reason=blocker 来自失败动作的严重级别。它和 required_actions=('repair_market_data',) 放在一起,能让人判断这是必须先修复的数据问题,而不是可以忽略的 warning。

执行闸门的价值在边界上。CLI、Web 后台、定时任务以后都可能触发每日运行,但它们不应该各自判断 status 字符串。统一经过 decide_execution(),才能避免 dry run 被误当成 ready,也能避免 blocked 状态下继续产生真实副作用。

测试

本章测试覆盖:

  • ready 计划允许执行。
  • dry-run-ready 计划不允许执行,并要求 disable_dry_run
  • blocked 计划返回失败动作。
  • 日志行格式稳定。

运行命令:

uv run pytest tests/test_execution_guard.py tests/test_daily_run_command.py tests/test_daily_run_plan.py tests/test_failure_policy.py

本批次补充 paper-command 后,全量测试通过:

276 passed, 2 warnings

本章更新与代码仓库

本章更新内容:

  • 新增 app/execution_guard.py
  • 新增 ExecutionDecision
  • 实现 decide_execution()
  • 实现 execution_decision_line()
  • 新增 tests/test_execution_guard.py,覆盖 ready、dry-run-ready、blocked 和日志格式。
  • scripts/chapter_examples.py 中接入执行闸门输出,复现 blocked 时的 required actions。

代码仓库:

https://github.com/ax2/zi-quant-platform

本章代码:

git clone https://github.com/ax2/zi-quant-platform.git
cd zi-quant-platform
git checkout chapter-41-45-paper-command
uv sync --extra dev
uv run python -m scripts.chapter_examples paper-command
uv run pytest tests/test_execution_guard.py tests/test_daily_run_command.py tests/test_daily_run_plan.py tests/test_failure_policy.py tests/test_chapter_examples.py

第 41-45 篇共用 tag chapter-41-45-paper-command。当前全量测试通过:276 passed,只有既有 FastAPI deprecation warning。

本篇小结

第 44 篇把真实执行前的最后一层判断单独抽出来。

这个模块很小,但边界重要。以后无论 CLI、定时任务还是 Web 后台触发每日运行,都应该先拿 ExecutionDecision,而不是各自判断 status 字符串。