Quant Trading for Programmers 44: Execution Guard
· 16 min read · Views --
Last updated on

Quant Trading for Programmers 44: Execution Guard

Author: Alex Xiang


Quant Trading for Programmers 44: Execution Guard

Article 43 wrapped the daily run as a command response, but it did not solve a more sensitive question: when is real execution allowed?

In a paper-trading system, dry_run_ready is easy to misread as “ready.” If the command layer only looks at exit code, a dry run may accidentally be connected to the real execution flow. Article 44 centralizes that decision into an execution guard.

ZiCode engineer checking the daily-run execution guard

Execution Decision

Article 44 adds app/execution_guard.py.

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

allowed is the machine decision. reason and required_actions are for humans and alerting systems.

Plan statusallowedreasonrequired_actions
readytruereadyempty
dry_run_readyfalsedry_rundisable_dry_run
blockedfalsewarning or blockerfailure-action list

Decision Logic

The core function is:

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",))

The important part is that dry_run_ready does not pass. It means checks passed, but the system is still in rehearsal mode.

Stable Log Line

The execution decision also has a one-line format:

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}"

A warning case prints:

allowed=false reason=warning actions=inspect_archive

This line is suitable for artifacts, alerts, or command responses. It tells the on-call engineer why execution did not happen.

Runnable Example

This article continues to use the same command:

uv run python -m scripts.chapter_examples paper-command

The output for this article is:

Execution-guard command output

Here, allowed=false is the core decision. Article 43 already used exit_code=1 to tell the scheduler that the run failed. The execution guard now tells the real execution layer: do not place orders, do not update the paper-trading account, and do not trigger downstream trading actions.

reason=blocker comes from the severity of the failure action. Together with required_actions=('repair_market_data',), it tells humans that a data problem must be fixed first, not ignored as a warning.

The value of the execution guard lies at the boundary. CLI, web admin, and scheduled jobs may all trigger daily runs later, but they should not each parse the status string independently. Everything should pass through decide_execution() so dry runs are not mistaken for ready states, and blocked states do not produce real side effects.

Tests

The tests cover:

  • ready plan allows execution;
  • dry_run_ready does not allow execution and requires disable_dry_run;
  • blocked plan returns failure actions;
  • log-line format remains stable.

Run:

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

After adding paper-command in this batch, the full test suite passed:

276 passed, 2 warnings

Repository

This article adds:

  • app/execution_guard.py;
  • ExecutionDecision;
  • decide_execution();
  • execution_decision_line();
  • tests/test_execution_guard.py, covering ready, dry-run-ready, blocked, and log format;
  • execution-guard output in scripts/chapter_examples.py, reproducing required actions for a blocked run.

Repository:

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

Code for this chapter:

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

Articles 41-45 share the tag chapter-41-45-paper-command. The current full suite passes with 276 passed, with only existing FastAPI deprecation warnings.

Summary

Article 44 extracts the final judgment before real execution into its own module.

The module is small, but the boundary matters. Whether a daily run is triggered by CLI, scheduled job, or web admin later, it should first obtain an ExecutionDecision instead of checking the status string itself.