Quant Trading for Programmers 42: Persist Daily Run Artifacts
· 17 min read · Views --
Last updated on

Quant Trading for Programmers 42: Persist Daily Run Artifacts

Author: Alex Xiang


Quant Trading for Programmers 42: Persist Daily Run Artifacts

Article 41 compressed the daily-run plan into a one-line summary suitable for logs and CLI output.

But logs only answer “what did it look like at that moment?” To debug why a run became blocked, we need more complete evidence: input symbols, failed checks, failure actions, and generation time. That is the role of an artifact.

ZiCode engineer archiving daily-run artifacts

What The Artifact Stores

Article 42 adds app/daily_run_artifacts.py.

@dataclass(frozen=True)
class DailyRunArtifact:
    path: Path
    payload: dict[str, Any]

payload is not a rough serialization of the dataclass. It is written as a stable debugging format.

FieldDescription
trade_dateThe trading day for the run
statusready, dry_run_ready, or blocked
required_symbolsSymbol list requested by this run
failed_checksChecks that did not pass
actionsRemediation action for each failed check
generated_atRequest generation time
executableWhether real execution is allowed

Build JSON Payload

The core function is daily_run_artifact_payload().

def daily_run_artifact_payload(plan: DailyRunPlan) -> dict[str, Any]:
    summary = build_daily_run_summary(plan)
    return {
        "trade_date": summary.trade_date,
        "status": summary.status,
        "dry_run": summary.dry_run,
        "symbol_count": summary.symbol_count,
        "required_symbols": list(plan.request.required_symbols),
        "failed_checks": list(plan.result.failed_checks),
        "actions": [
            {
                "check_name": action.check_name,
                "action": action.action,
                "severity": action.severity,
            }
            for action in plan.failure_actions
        ],
        "action_summary": summary.action_summary,
        "executable": summary.executable,
        "generated_at": plan.request.generated_at.isoformat(),
    }

The tuple fields are deliberately converted into lists. Artifacts are for external readers, so they should not expose Python object habits.

Write The File

The file name is generated by trading day:

def write_daily_run_artifact(plan: DailyRunPlan, *, directory: Path) -> DailyRunArtifact:
    payload = daily_run_artifact_payload(plan)
    directory.mkdir(parents=True, exist_ok=True)
    path = directory / f"daily-run-{payload['trade_date']}.json"
    path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    return DailyRunArtifact(path=path, payload=payload)

In a blocked scenario, the artifact keeps action information like this:

{
  "actions": [
    {
      "check_name": "run_health",
      "action": "inspect_run_health",
      "severity": "blocker"
    }
  ]
}

That is much more useful than seeing only blocked in logs.

Runnable Example

Article 42 continues to use the same command to reproduce artifact persistence:

uv run python -m scripts.chapter_examples paper-command

The output for this article is:

Daily-run artifact command output

Three details in the screenshot are genuinely useful for debugging.

artifact=daily-run-2026-03-07.json shows that the file name is stable and keyed by trading day. On call, nobody needs to guess which file in the log directory belongs to today’s result.

payload_keys shows that the artifact keeps request, status, failed checks, actions, and generation time. It is not a backup of the one-line summary; it is richer runtime evidence.

failed_checks=['data_gaps'] actions=['repair_market_data'] executable=False puts failure cause and remediation direction together. From this line alone, we know the blocker is not a strategy signal issue and not a run-window issue. Market-data gaps blocked execution.

In a production system, I would keep logs, artifacts, and the later runbook separate: logs for first glance, artifact for review evidence, and runbook for next actions. They should reference each other, but not replace each other.

Tests

The tests cover:

  • payload contains the context needed for debugging;
  • written JSON can be read back completely;
  • blocked scenario keeps check_name, action, and severity.

Run:

uv run pytest tests/test_daily_run_artifacts.py tests/test_daily_run_summary.py tests/test_daily_run_plan.py

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

276 passed, 2 warnings

Repository

This article adds:

  • app/daily_run_artifacts.py;
  • DailyRunArtifact;
  • daily_run_artifact_payload();
  • write_daily_run_artifact() and read_daily_run_artifact();
  • tests/test_daily_run_artifacts.py, covering payload and JSON round trip;
  • artifact write/read in scripts/chapter_examples.py; the article screenshot comes from that command output.

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_daily_run_artifacts.py tests/test_daily_run_summary.py tests/test_daily_run_plan.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

An artifact is an evidence file in a production system.

Article 42 writes the daily-run plan into JSON so later commands, alerts, and debugging can all point to the same file instead of each rebuilding their own context.