KB / operations
Report Lifecycle
Last verified
Every report cycle — whether hot (5 min), market (15 min), full (anchor), extended, or overnight — flows through the same four-stage orchestrator in app/engine.py:generate_report_logic(). The orchestrator is thin; the substance lives in the four helpers it delegates to.
Stage 1 — Fetch
fetch_all_sources(profile) instantiates the full source registry, filters to the active profile, and runs each source on the thread pool (run_in_executor). For every source:
- The fetch is gated by the per-source release lock so the static cycle and the future release-aware scheduler can’t race on the same
_source_cache[name]slot. - The fresh result is merged against the on-disk cache (
.source_cache.json). Missing-but-known fields stay at their cached value; empty collections don’t overwrite good data. A transient fetch failure doesn’t wipe yesterday’s good number. - A
source_runsrow records the outcome — see source health for the ok/fail semantics, including the “empty dict counts asok=False” rule.
Three auxiliary fetches run alongside: gamma profile (Schwab option chain), Fed Watch (CME rate probabilities), and rolling correlations (a SQL query against daily_signals history, not an external call). Stashed under underscore-prefixed keys so format_report finds them without collision.
Diagnostic pruning fires here: source_runs older than RETENTION_SOURCE_RUNS_DAYS (default 30) are deleted. signal_transitions and news_items retention is indefinite unless RETENTION_TRANSITIONS_DAYS / RETENTION_NEWS_DAYS are set explicitly.
Stage 2 — Format
format_report(template, aggregated_data, prev_ctx) is the deterministic, side-effect-free part of the cycle. It:
- Builds the template context dict from raw source payloads — column ordering, derived signals, change markers (vs
prev_ctx), bridge text, regime classification, alignment categories, and the health score. - Renders
app/templates/report.mdvia Jinja2 ({{ field }},numfilter — see Law 6 in the repo CLAUDE.md). Thenumfilter returns"N/A"for missing or non-numeric values so failed sources don’t leak$0.00/0.00%into the rendered report. - Stashes the canonical
signalsdict on the in-process_engine_sig_inputhandoff for the next stage to consume. This is the Law 4 contract — capture reads from thesource_rawparallel store with the in-process handoff as fallback, never re-parses the rendered markdown. (Stage 3 sunset, 2026-06-17 — the legacy.signals_snapshot.jsonsidecar was retired; the canonical priority chain is now_READ_PRIORITY = ("source_raw", "handoff").)
Stage 3 — Persist (report files)
write_report_files(report_content, timestamp) writes the rendered markdown twice:
report.md(the “latest” pointer — what the legacy/reportendpoint serves).report-{timestamp}.md(an archive file). Files stay flat on disk; day/week organization is derived from filenames at query time by the API.
Failures here log CRITICAL but never raise — a report file write failing must not kill the next stage’s database update.
Stage 4 — Capture
capture_signals_hook(timestamp, profile) is the database-write stage:
- Read the latest
source_rawrows (or fall back to the in-process handoff dict) and persist the canonical signals intodaily_signals(one row per trading date, upserted) andintraday_signals(one row per cycle). - Compute and write the health score via a post-hoc UPDATE so the persisted score reflects the full
sig_input. - Emit any new
signal_transitionsrows by diffing the current classifier state against the previous snapshot. See event feed for the classifier inventory and dedup rules. - Evaluate the 63 alert definitions in
alert_thresholds.jsonand write any newly-fired or newly-resolved rows intoalerts. - Record predictions into
base_rate_predictionsfor the calibration loop.
The whole stage is wrapped in a broad try/except — a capture failure logs CRITICAL but never raises into the looper. The next cycle gets a clean attempt.
Failure modes
| Failure | Behavior |
|---|---|
| Single source raises | source_runs records ok=False, cache holds previous value, cycle continues |
Single source returns {} | source_runs records ok=False with error="empty result", cache holds previous value |
| Format raises | Cycle fails loudly; no report file written; next cycle retries |
Both source_raw and handoff miss | Capture logs CRITICAL and aborts the cycle (P0 — no silent degradation); next cycle retries |
| Capture raises | Logs CRITICAL; report file is on disk but DB row is from previous cycle |
| Report file write fails | Logs CRITICAL but capture still runs — DB row stays consistent with the in-memory render |
See also
- Data sources — the inventory the fetch stage iterates.
- Schedule — when each profile fires.
- Source health — the
source_runstable and how outcomes escalate. - Event feed — what stage-4 capture writes into
signal_transitions. - Code:
app/engine.py:generate_report_logicand its four helpers (fetch_all_sources,format_report,write_report_files,capture_signals_hook).