quantred 2.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. quantred/__init__.py +24 -0
  2. quantred/alphas/__init__.py +53 -0
  3. quantred/alphas/base.py +221 -0
  4. quantred/alphas/combine.py +260 -0
  5. quantred/alphas/horizon.py +153 -0
  6. quantred/alphas/refine.py +300 -0
  7. quantred/alphas/scorers.py +87 -0
  8. quantred/analytics/attribution.py +354 -0
  9. quantred/analytics/bootstrap.py +353 -0
  10. quantred/analytics/charts.py +393 -0
  11. quantred/analytics/excursion.py +185 -0
  12. quantred/analytics/execution_quality.py +180 -0
  13. quantred/analytics/exposure.py +167 -0
  14. quantred/analytics/htmlreport.py +1208 -0
  15. quantred/analytics/information.py +197 -0
  16. quantred/analytics/metrics.py +502 -0
  17. quantred/analytics/performance.py +639 -0
  18. quantred/analytics/reporting.py +1672 -0
  19. quantred/analytics/screening.py +190 -0
  20. quantred/analytics/series_comparison.py +303 -0
  21. quantred/analytics/trade_analytics.py +353 -0
  22. quantred/brokers/alpaca/broker.py +415 -0
  23. quantred/brokers/alpaca/factory.py +33 -0
  24. quantred/brokers/alpaca/market_data.py +166 -0
  25. quantred/brokers/base.py +220 -0
  26. quantred/brokers/dryrun.py +164 -0
  27. quantred/brokers/errors.py +70 -0
  28. quantred/cli.py +8619 -0
  29. quantred/costs/__init__.py +11 -0
  30. quantred/costs/base.py +126 -0
  31. quantred/costs/parametric.py +131 -0
  32. quantred/data/__init__.py +29 -0
  33. quantred/data/compute.py +332 -0
  34. quantred/data/edges.py +81 -0
  35. quantred/data/features.py +120 -0
  36. quantred/data/panel.py +61 -0
  37. quantred/data/scan.py +85 -0
  38. quantred/data/store.py +269 -0
  39. quantred/demo/__init__.py +22 -0
  40. quantred/demo/scanners.py +104 -0
  41. quantred/demo/strategies.py +125 -0
  42. quantred/engine/backtest.py +1206 -0
  43. quantred/engine/barcheck.py +228 -0
  44. quantred/engine/live.py +539 -0
  45. quantred/execution/decision.py +192 -0
  46. quantred/execution/flatten.py +302 -0
  47. quantred/execution/halt.py +165 -0
  48. quantred/execution/ledger.py +758 -0
  49. quantred/execution/live_trader.py +587 -0
  50. quantred/execution/order_id.py +83 -0
  51. quantred/execution/sizing.py +114 -0
  52. quantred/indicators/indicators.py +158 -0
  53. quantred/marketdata/base.py +62 -0
  54. quantred/marketdata/client.py +59 -0
  55. quantred/marketdata/session.py +112 -0
  56. quantred/marketdata/synthetic.py +74 -0
  57. quantred/marketdata/timeframe.py +125 -0
  58. quantred/mcp/__init__.py +6 -0
  59. quantred/mcp/selftest.py +139 -0
  60. quantred/mcp/server.py +2419 -0
  61. quantred/optimization/causality.py +607 -0
  62. quantred/optimization/config_store.py +300 -0
  63. quantred/optimization/optimizer.py +431 -0
  64. quantred/optimization/parallel.py +345 -0
  65. quantred/optimization/param_space.py +309 -0
  66. quantred/optimization/walk_forward.py +1254 -0
  67. quantred/portfolio/allocator.py +135 -0
  68. quantred/portfolio/benchmark.py +109 -0
  69. quantred/portfolio/optimizer.py +786 -0
  70. quantred/portfolio/policy.py +335 -0
  71. quantred/portfolio/posterior.py +208 -0
  72. quantred/research/__init__.py +30 -0
  73. quantred/research/agent.py +868 -0
  74. quantred/research/demo_proposals.py +222 -0
  75. quantred/research/llm.py +311 -0
  76. quantred/research/proposer.py +234 -0
  77. quantred/research/provider_errors.py +242 -0
  78. quantred/research/sandbox.py +340 -0
  79. quantred/risk/__init__.py +60 -0
  80. quantred/risk/base.py +218 -0
  81. quantred/risk/conditional.py +474 -0
  82. quantred/risk/exposures.py +88 -0
  83. quantred/risk/factor.py +119 -0
  84. quantred/risk/sample.py +89 -0
  85. quantred/risk/streaming.py +214 -0
  86. quantred/scanners/base.py +141 -0
  87. quantred/scanners/symbol_scanner.py +148 -0
  88. quantred/services/__init__.py +51 -0
  89. quantred/services/analysis.py +6082 -0
  90. quantred/services/audit.py +574 -0
  91. quantred/services/campaign.py +381 -0
  92. quantred/services/configs.py +282 -0
  93. quantred/services/data.py +172 -0
  94. quantred/services/dryrun.py +206 -0
  95. quantred/services/glossary.py +130 -0
  96. quantred/services/maintenance.py +314 -0
  97. quantred/services/registry.py +351 -0
  98. quantred/services/setup.py +1023 -0
  99. quantred/services/sizing.py +150 -0
  100. quantred/services/smallreal.py +450 -0
  101. quantred/settings.py +571 -0
  102. quantred/store/__init__.py +0 -0
  103. quantred/store/bars.py +660 -0
  104. quantred/store/trials.py +1804 -0
  105. quantred/strategies/base.py +643 -0
  106. quantred/strategies/signals.py +28 -0
  107. quantred/utils/errors.py +48 -0
  108. quantred/utils/extras.py +330 -0
  109. quantred/utils/logging_config.py +33 -0
  110. quantred/utils/numeric.py +46 -0
  111. quantred/utils/streaming.py +474 -0
  112. quantred/utils/timeutils.py +148 -0
  113. quantred-2.4.0.dist-info/METADATA +632 -0
  114. quantred-2.4.0.dist-info/RECORD +117 -0
  115. quantred-2.4.0.dist-info/WHEEL +4 -0
  116. quantred-2.4.0.dist-info/entry_points.txt +8 -0
  117. quantred-2.4.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,1208 @@
1
+ """Self-contained HTML rendering of a research result.
2
+
3
+ Every result in this project otherwise dies in a terminal scrollback. One HTML
4
+ file per run fixes that: openable anywhere, attachable, and honest — because it
5
+ renders the **same** result dict the CLI printed rather than recomputing anything.
6
+ Nothing in this module derives a number; a renderer that computes is a renderer
7
+ that can disagree with the report it claims to be showing.
8
+
9
+ **Self-contained is a hard requirement, not an aspiration.** No web fonts, no CDN
10
+ stylesheets, no remote images, no analytics: opening a report must issue zero
11
+ network requests. That is an offline guarantee and a privacy one at once — a
12
+ report forwarded to someone else must not phone home on their machine. The test
13
+ suite asserts it rather than trusting it.
14
+
15
+ **The medium flatters; the report must not.** Gate failures, memoized results, and
16
+ enabled evidence-gated features render as prominent warnings at the top, mirroring
17
+ the severity the terminal gives them. A pretty report that buries its own verdict
18
+ is worse than the plain-text one it replaced.
19
+ """
20
+
21
+ import base64
22
+ import html
23
+ import json
24
+ from datetime import datetime, timezone
25
+ from typing import Any, Dict, List, Optional, Sequence, Tuple
26
+
27
+ from quantred.analytics.reporting import alphas_heading, capacity_text
28
+
29
+ #: Result kinds this module knows how to render.
30
+ KINDS = ("verdict", "backtest", "walkforward", "info")
31
+
32
+ #: The evidence-gated features that default off. A report where one is enabled says
33
+ #: so loudly: the reader has to know they are looking at a non-default configuration
34
+ #: whose own adoption gate has not cleared on this repo's data.
35
+ _DEFAULT_OFF = {
36
+ "conditional": "conditional risk (Σ_t)",
37
+ "policy": "the aim-in-front-of-the-target trading policy",
38
+ "posterior": "the Black–Litterman posterior",
39
+ }
40
+
41
+
42
+ class ReportKindError(ValueError):
43
+ """The result dict does not match the kind it was asked to render as.
44
+
45
+ Raised loudly and early rather than producing a half-rendered report from a
46
+ payload whose shape was guessed at - the failure mode a result-dict passthrough
47
+ contract invites once two surfaces exchange these objects.
48
+ """
49
+
50
+
51
+ def render_html(result: Dict[str, Any], kind: str, *, extras: Optional[Dict[str, Any]] = None) -> str:
52
+ """Render ``result`` as one self-contained HTML document.
53
+
54
+ ``kind`` is one of :data:`KINDS` and must match the payload: a composite result
55
+ carries its own schema stamp, and a mismatch raises :class:`ReportKindError`
56
+ rather than rendering something misleading.
57
+
58
+ ``extras`` carries display-only material a caller already has in hand but the
59
+ result dict does not (today: an ``equity_curve`` for a backtest, which the
60
+ result dict deliberately omits because it can run to tens of thousands of
61
+ points). It is never required — a report rendered without it degrades to the
62
+ tables, which is exactly what a caller reading the dict alone gets.
63
+ """
64
+ if kind not in KINDS:
65
+ raise ReportKindError(f"Unknown report kind {kind!r}; expected one of {', '.join(KINDS)}")
66
+ _assert_shape(result, kind)
67
+ extras = extras or {}
68
+
69
+ sections = {
70
+ "verdict": _verdict_sections,
71
+ "backtest": _backtest_sections,
72
+ "walkforward": _walkforward_sections,
73
+ "info": _info_sections,
74
+ }[kind](result, extras)
75
+
76
+ return "\n".join(
77
+ [
78
+ "<!doctype html>",
79
+ '<html lang="en"><head>',
80
+ '<meta charset="utf-8">',
81
+ '<meta name="viewport" content="width=device-width, initial-scale=1">',
82
+ f"<title>{esc(_title(result, kind))}</title>",
83
+ f"<style>{_CSS}</style>",
84
+ "</head><body>",
85
+ f"<main><h1>{esc(_title(result, kind))}</h1>",
86
+ _provenance_block(result, kind),
87
+ _warnings_block(result, kind),
88
+ *sections,
89
+ _footer(),
90
+ "</main></body></html>",
91
+ ]
92
+ )
93
+
94
+
95
+ def _assert_shape(result: Dict[str, Any], kind: str) -> None:
96
+ """Fail fast when the payload is not what ``kind`` promises.
97
+
98
+ A composite carries an explicit schema stamp, so it is checked exactly. The
99
+ other kinds predate any stamp, so they are checked on a field only that kind
100
+ produces - crude, but it catches the mistake that matters (a result handed to
101
+ the wrong renderer) without inventing a version scheme for payloads that have
102
+ none.
103
+ """
104
+ if not isinstance(result, dict):
105
+ raise ReportKindError(f"Expected a result dict, got {type(result).__name__}")
106
+ if kind == "verdict":
107
+ from quantred.services.analysis import VERDICT_SCHEMA
108
+
109
+ schema = result.get("schema")
110
+ if schema != VERDICT_SCHEMA:
111
+ raise ReportKindError(
112
+ f"Expected a {VERDICT_SCHEMA} result, got schema={schema!r}. "
113
+ "Pass the object a verdict run returned, unmodified."
114
+ )
115
+ return
116
+ required = {"backtest": "metrics", "walkforward": "folds", "info": "mean_ic"}[kind]
117
+ # A reused walk-forward keeps the OOS aggregate and not the folds; it is a legitimate
118
+ # (reduced) result of this kind, not a mismatched payload.
119
+ if kind == "walkforward" and result.get("memoized") and "oos_aggregate" in result:
120
+ return
121
+ if required not in result:
122
+ raise ReportKindError(
123
+ f"This does not look like a {kind} result: no {required!r} field. "
124
+ "Pass the object the matching run returned, unmodified."
125
+ )
126
+
127
+
128
+ # --------------------------------------------------------------------------- #
129
+ # Escaping and small formatters
130
+ # --------------------------------------------------------------------------- #
131
+ def esc(value: Any) -> str:
132
+ """Escape anything on its way into the document.
133
+
134
+ Symbols, strategy names, config params, and error strings all flow into this
135
+ report, and every one of them is ultimately user input. Everything
136
+ interpolated goes through here - there is no "obviously safe" interpolation.
137
+ """
138
+ return html.escape("" if value is None else str(value), quote=True)
139
+
140
+
141
+ def _num(value: Any, spec: str = "{:.4g}") -> str:
142
+ """Format a number, leaving non-numbers (and absent values) legible as-is."""
143
+ if value is None:
144
+ return "—"
145
+ if isinstance(value, bool):
146
+ return "yes" if value else "no"
147
+ try:
148
+ return spec.format(float(value))
149
+ except (TypeError, ValueError):
150
+ # Plain text, deliberately unescaped: formatters produce values, and
151
+ # escaping happens once, at the point of interpolation. Escaping here too
152
+ # would double-escape every non-numeric cell.
153
+ return str(value)
154
+
155
+
156
+ def _metric_cell(name: str, value: Any) -> str:
157
+ """One metric by name, through the shared unit table so a percent shows its ``%``,
158
+ a probability its three decimals and a flag yes/no - the same text the terminal
159
+ prints. A metric with no declared unit is a ratio and keeps the generic format."""
160
+ from quantred.optimization.walk_forward import UNIT_DECLARED, format_gate_value
161
+
162
+ if value is not None and name in UNIT_DECLARED:
163
+ return format_gate_value(name, value)
164
+ return _num(value)
165
+
166
+
167
+ def _metric_rows(metrics: Dict[str, Any]) -> List[List[str]]:
168
+ """Name/value rows for a metrics dict, with a placeholder zero shown as not measured.
169
+
170
+ A ratio stored as 0.0 beside a flag saying it was unavailable is not a result of
171
+ zero; it is a number that never existed.
172
+ """
173
+ from quantred.analytics.performance import unmeasured_metrics
174
+
175
+ unmeasured = unmeasured_metrics(metrics)
176
+ return [
177
+ [
178
+ _metric_label(k, metrics),
179
+ f"not measured ({unmeasured[k]})" if k in unmeasured else _metric_cell(k, v),
180
+ ]
181
+ for k, v in sorted(metrics.items())
182
+ ]
183
+
184
+
185
+ def _metric_label(name: str, metrics: Dict[str, Any]) -> str:
186
+ """The row label for a metric: an undeflated 'deflated' Sharpe says what it is.
187
+
188
+ ``deflation_applied`` is false for a single trial, where the value is the
189
+ probabilistic Sharpe under another name. The stored key is unchanged; only the
190
+ label is.
191
+ """
192
+ if name == "deflated_sharpe_ratio" and metrics.get("deflation_applied") is False:
193
+ return "PSR, undeflated (1 trial) [deflated_sharpe_ratio]"
194
+ return name
195
+
196
+
197
+ def _pct(value: Any, spec: str = "{:+.2%}") -> str:
198
+ return _num(value, spec)
199
+
200
+
201
+ def _short_date(value: Optional[str]) -> str:
202
+ return (value or "—")[:10]
203
+
204
+
205
+ # --------------------------------------------------------------------------- #
206
+ # Building blocks
207
+ # --------------------------------------------------------------------------- #
208
+ def _card(title: str, body: str, *, tone: str = "") -> str:
209
+ klass = f"card {tone}".strip()
210
+ return f'<section class="{klass}"><h2>{esc(title)}</h2>{body}</section>'
211
+
212
+
213
+ def _table(headers: Sequence[str], rows: Sequence[Sequence[Any]], *, raw: bool = False) -> str:
214
+ """A table. ``raw`` marks cells already escaped by the caller (formatters that
215
+ emit markup, like a pass/fail pill); everything else is escaped here."""
216
+ if not rows:
217
+ return '<p class="empty">Not recorded.</p>'
218
+ head = "".join(f"<th>{esc(h)}</th>" for h in headers)
219
+ body = []
220
+ for row in rows:
221
+ cells = "".join(f"<td>{c if raw else esc(c)}</td>" for c in row)
222
+ body.append(f"<tr>{cells}</tr>")
223
+ return f'<div class="scroll"><table><thead><tr>{head}</tr></thead><tbody>{"".join(body)}</tbody></table></div>'
224
+
225
+
226
+ def _kv(pairs: Sequence[Tuple[str, Any]]) -> str:
227
+ """A definition list. Every key and value is escaped, with no exceptions.
228
+
229
+ There is no markup passthrough here on purpose: a "this string already looks
230
+ like markup" heuristic is indistinguishable from "this user-supplied symbol
231
+ contains a `<`", which is precisely how a symbol list becomes script injection.
232
+ Callers needing markup compose it themselves, outside this helper.
233
+ """
234
+ items = "".join(f"<div><dt>{esc(k)}</dt><dd>{esc(v)}</dd></div>" for k, v in pairs)
235
+ return f"<dl class='kv'>{items}</dl>"
236
+
237
+
238
+ def _pill(passed: Optional[bool]) -> str:
239
+ if passed is None:
240
+ return '<span class="pill neutral">—</span>'
241
+ return '<span class="pill pass">PASS</span>' if passed else '<span class="pill fail">FAIL</span>'
242
+
243
+
244
+ def _pre(lines: Sequence[str]) -> str:
245
+ """Terminal text, verbatim, in a block: the same words the terminal prints, so a
246
+ caveat is one definition shown twice rather than two phrasings that can drift."""
247
+ return f"<pre>{esc(chr(10).join(line.rstrip() for line in lines).strip(chr(10)))}</pre>"
248
+
249
+
250
+ def _warning(text: str, *, level: str = "warn") -> str:
251
+ return f'<div class="banner {esc(level)}">{esc(text)}</div>'
252
+
253
+
254
+ def _chart(png: Optional[bytes], alt: str) -> str:
255
+ """Embed a PNG as a data URI, or explain its absence.
256
+
257
+ A missing chart is a slot with an explanation, never a broken run: the plotting
258
+ dependency is an optional extra, and a text-complete report without pictures is
259
+ strictly better than no report.
260
+ """
261
+ if png is None:
262
+ # Deliberately not the install command: a report is attached and forwarded, and
263
+ # the command names this machine's packs and installer. `init --check` prints it
264
+ # for the reader's own terminal.
265
+ from quantred.settings import DOCS_URL
266
+
267
+ return (
268
+ '<p class="empty">Chart not rendered — the viz extra is not installed. '
269
+ f"See {esc(DOCS_URL)}/usage/extras for how to add it.</p>"
270
+ )
271
+ encoded = base64.b64encode(png).decode("ascii")
272
+ return f'<img alt="{esc(alt)}" src="data:image/png;base64,{encoded}">'
273
+
274
+
275
+ def _try_chart(fn) -> Optional[bytes]:
276
+ """Render a chart, or ``None`` when plotting is unavailable.
277
+
278
+ Only the missing-dependency case degrades silently; a genuine rendering bug
279
+ should not be disguised as "matplotlib isn't installed".
280
+ """
281
+ try:
282
+ return fn()
283
+ except RuntimeError: # the viz extra's own actionable "not installed" error
284
+ return None
285
+
286
+
287
+ # --------------------------------------------------------------------------- #
288
+ # Header blocks (identical for every kind)
289
+ # --------------------------------------------------------------------------- #
290
+ def _title(result: Dict[str, Any], kind: str) -> str:
291
+ strategy = result.get("strategy") or (result.get("inputs") or {}).get("strategy") or "result"
292
+ from quantred.settings import BRAND
293
+
294
+ return f"{BRAND} {kind} — {strategy}"
295
+
296
+
297
+ def _window(result: Dict[str, Any]) -> Dict[str, Any]:
298
+ return (result.get("inputs") or {}).get("window") or result.get("window") or {}
299
+
300
+
301
+ def _provenance_block(result: Dict[str, Any], kind: str) -> str:
302
+ """What a reader needs to not misread this file a month from now.
303
+
304
+ Mandatory, at the top, never a footnote: a report that outlives the context
305
+ that produced it carries its window, universe, cost assumptions, code version,
306
+ and campaign trial count, or it is a number without a claim attached.
307
+ """
308
+ inputs = result.get("inputs") or {}
309
+ provenance = result.get("provenance") or {}
310
+ window = _window(result)
311
+ universe = inputs.get("universe") or result.get("symbols") or inputs.get("candidates") or []
312
+ pairs: List[Tuple[str, Any]] = [
313
+ ("Kind", kind),
314
+ ("Window", f"{_short_date(window.get('start'))} → {_short_date(window.get('end'))}"),
315
+ ("Universe", f"{len(universe)} names: {', '.join(str(s) for s in universe) or '—'}"),
316
+ ("Timeframe", _requested(inputs.get("timeframe") or result.get("timeframe"), result)),
317
+ (
318
+ "Benchmark",
319
+ _requested(
320
+ inputs.get("benchmark") or result.get("benchmark") or result.get("benchmark_requested"),
321
+ result,
322
+ ),
323
+ ),
324
+ ("Cost model", _cost_text(inputs.get("cost"), result)),
325
+ # "Version" rather than "Git SHA": an installed copy has no repository, and
326
+ # the packaged version is the honest answer to "what made this".
327
+ ("Code version", provenance.get("git_sha") or _code_version()),
328
+ ("Generated", provenance.get("generated_at") or datetime.now(timezone.utc).isoformat()),
329
+ ("Campaign trials", _campaign_trials(provenance, result)),
330
+ ]
331
+ if result.get("run_id"):
332
+ pairs.append(("Run id", result["run_id"]))
333
+ if result.get("trial_id"):
334
+ pairs.append(("Trial id", result["trial_id"]))
335
+ return _card("Provenance", _kv(pairs))
336
+
337
+
338
+ def _requested(value: Any, result: Dict[str, Any]) -> str:
339
+ """A header fact, marked "as requested" on a reused result: the trial store kept the
340
+ metrics, not what the run was scored against, so the request is all that is known."""
341
+ if not value:
342
+ return "—"
343
+ return f"{value} (as requested)" if result.get("memoized") else str(value)
344
+
345
+
346
+ def _campaign_trials(provenance: Dict[str, Any], result: Dict[str, Any]) -> str:
347
+ """The terminal's rendering, so a real zero is not "—" and an unrecorded run says so."""
348
+ from quantred.analytics.reporting import campaign_trials_text
349
+
350
+ if "n_trials" in provenance:
351
+ return campaign_trials_text(provenance)
352
+ return "—" if result.get("n_trials") is None else str(result["n_trials"])
353
+
354
+
355
+ def _code_version() -> str:
356
+ """The running code's identifier, for a result dict that carries none."""
357
+ from quantred.services.analysis import code_version
358
+
359
+ return code_version()
360
+
361
+
362
+ def _cost_text(cost: Optional[Dict[str, Any]], result: Dict[str, Any]) -> str:
363
+ if cost is None:
364
+ return "GROSS (no transaction cost charged)" if result.get("gross") else "net of cost (defaults)"
365
+ if cost.get("gross"):
366
+ return "GROSS (no transaction cost charged)"
367
+ return (
368
+ f"{cost.get('commission_bps')}bps commission · impact η={cost.get('impact_eta')} "
369
+ f"· borrow {cost.get('borrow_bps')}bps"
370
+ )
371
+
372
+
373
+ def _warnings_block(result: Dict[str, Any], kind: str) -> str:
374
+ """Everything that should stop a reader, before anything that might please one."""
375
+ banners: List[str] = []
376
+
377
+ if result.get("memoized"):
378
+ banners.append(
379
+ _warning(
380
+ f"REUSED — not re-run. Served from trial {result.get('trial_id', '?')} "
381
+ f"recorded {result.get('trial_ts') or 'at an unknown time'} "
382
+ f"({_age(result.get('trial_ts'))} old). Re-run with --force to re-verify.",
383
+ level="reuse",
384
+ )
385
+ )
386
+
387
+ verdict = result.get("verdict") or {}
388
+ if verdict.get("verdict") == "incomplete":
389
+ banners.append(
390
+ _warning(
391
+ "INCOMPLETE — no verdict. A step failed"
392
+ + (
393
+ f" ({', '.join(verdict.get('failed_steps') or [])})"
394
+ if verdict.get("failed_steps")
395
+ else ""
396
+ )
397
+ + ". Do not act on the sections below.",
398
+ level="fail",
399
+ )
400
+ )
401
+ if verdict.get("verdict") == "infeasible":
402
+ banners.append(
403
+ _warning(
404
+ "INFEASIBLE — no book satisfies the constraints. More dates will not change this; "
405
+ "widen the universe or change the caps explicitly.",
406
+ level="fail",
407
+ )
408
+ )
409
+ failed = [name for name, c in (verdict.get("checks") or {}).items() if not c.get("passed")]
410
+ if failed:
411
+ banners.append(_warning("Gate failures: " + ", ".join(sorted(failed)), level="fail"))
412
+
413
+ gate_report = result.get("gate_report") or {}
414
+ if gate_report and not gate_report.get("promotable"):
415
+ failed_gates = [n for n, c in (gate_report.get("checks") or {}).items() if not c.get("passed")]
416
+ banners.append(
417
+ _warning("NOT PROMOTABLE — failed gates: " + ", ".join(sorted(failed_gates)), level="fail")
418
+ )
419
+
420
+ if kind == "walkforward":
421
+ evidence = result.get("evidence")
422
+ if evidence is None:
423
+ banners.append(
424
+ _warning(
425
+ "Evidence completeness not computed — promotion prerequisites were not assessed "
426
+ f"for this result ({result.get('evidence_unavailable') or 'not computed for this run'}). "
427
+ "`promotable` is the statistical gates only.",
428
+ )
429
+ )
430
+ elif gate_report.get("promotable") and not evidence.get("clear"):
431
+ banners.append(
432
+ _warning(
433
+ "PROMOTABLE is the statistical gates only — evidence completeness: "
434
+ f"{evidence.get('evidence_completeness')}. Not clear for paper.",
435
+ )
436
+ )
437
+
438
+ executability = result.get("executability") or {}
439
+ if executability.get("executable") is False:
440
+ failing = [n for n, c in (executability.get("checks") or {}).items() if not c.get("passed")]
441
+ banners.append(
442
+ _warning(
443
+ "NOT EXECUTABLE at this capital — failed checks: " + ", ".join(sorted(failing)),
444
+ level="fail",
445
+ )
446
+ )
447
+
448
+ if result.get("low_sample"):
449
+ banners.append(_warning("Low sample — too few rebalances to measure an IC with confidence."))
450
+ if result.get("sanity_ceiling_breached"):
451
+ banners.append(
452
+ _warning("Realized IR above 2 on public data — suspect a bug or a leak, not skill.", level="fail")
453
+ )
454
+ leakage = (result.get("diagnostics") or {}).get("leakage_probe")
455
+ if leakage is not None and not leakage.get("passed"):
456
+ banners.append(_warning("Leakage probe FAILED — the result is not trustworthy.", level="fail"))
457
+
458
+ banners.extend(_default_off_banners(result))
459
+ return "".join(banners)
460
+
461
+
462
+ def _default_off_banners(result: Dict[str, Any]) -> List[str]:
463
+ """Name every evidence-gated, default-off feature this run turned on."""
464
+ portfolio = result.get("portfolio") if isinstance(result.get("portfolio"), dict) else result
465
+ banners = []
466
+ for key, description in _DEFAULT_OFF.items():
467
+ value = portfolio.get(key) if isinstance(portfolio, dict) else None
468
+ if value:
469
+ banners.append(
470
+ _warning(
471
+ f"Non-default configuration: {description} is ENABLED ({value}). "
472
+ "It ships off because its own adoption gate does not clear on this "
473
+ "repo's data — this run is not the default path.",
474
+ level="note",
475
+ )
476
+ )
477
+ return banners
478
+
479
+
480
+ def _age(ts: Optional[str]) -> str:
481
+ if not ts:
482
+ return "unknown age"
483
+ try:
484
+ when = datetime.fromisoformat(str(ts).replace("Z", "+00:00"))
485
+ if when.tzinfo is None:
486
+ when = when.replace(tzinfo=timezone.utc)
487
+ seconds = (datetime.now(timezone.utc) - when).total_seconds()
488
+ except ValueError:
489
+ return "unknown age"
490
+ if seconds < 3600:
491
+ return f"{int(max(seconds, 0) // 60)} minutes"
492
+ if seconds < 86400:
493
+ return f"{seconds / 3600:.1f} hours"
494
+ return f"{seconds / 86400:.1f} days"
495
+
496
+
497
+ # --------------------------------------------------------------------------- #
498
+ # Per-kind sections
499
+ # --------------------------------------------------------------------------- #
500
+ def _verdict_sections(result: Dict[str, Any], extras: Dict[str, Any]) -> List[str]:
501
+ from quantred.analytics import charts
502
+ from quantred.optimization.walk_forward import format_gate_value
503
+
504
+ verdict = result.get("verdict") or {}
505
+ checks = verdict.get("checks") or {}
506
+ out = []
507
+
508
+ banner_class = {
509
+ "promotable": "pass",
510
+ "not promotable": "fail",
511
+ "needs more data": "warn",
512
+ "mixed": "warn",
513
+ "incomplete": "fail",
514
+ "infeasible": "fail",
515
+ }.get(verdict.get("verdict"), "warn")
516
+ out.append(
517
+ _card(
518
+ "Verdict",
519
+ f'<p class="verdict {banner_class}">{esc(verdict.get("summary", "unknown"))}</p>'
520
+ + _table(
521
+ ["", "Check", "Value", "Threshold", "Why it is there"],
522
+ [
523
+ [
524
+ _pill(c.get("passed")),
525
+ esc(name),
526
+ esc(format_gate_value(name, c.get("value"))),
527
+ esc(format_gate_value(name, c.get("threshold"))),
528
+ esc(c.get("note", "")),
529
+ ]
530
+ for name, c in sorted(checks.items())
531
+ ],
532
+ raw=True,
533
+ )
534
+ + (
535
+ _chart(
536
+ _try_chart(
537
+ lambda: charts.render_gate_png(
538
+ checks, promotable=bool(verdict.get("promotable")), title="RESEARCH VERDICT"
539
+ )
540
+ ),
541
+ "verdict gate scorecard",
542
+ )
543
+ if checks
544
+ else ""
545
+ ),
546
+ tone=banner_class,
547
+ )
548
+ )
549
+
550
+ steps = result.get("steps") or {}
551
+ notable = [(n, s) for n, s in steps.items() if s.get("status") != "ok"]
552
+ if notable:
553
+ out.append(
554
+ _card(
555
+ "Steps",
556
+ _table(
557
+ ["Step", "Status", "Detail"],
558
+ [[n, s.get("status"), s.get("error") or s.get("reason") or ""] for n, s in notable],
559
+ ),
560
+ )
561
+ )
562
+
563
+ scan = result.get("scan")
564
+ if scan:
565
+ note = (
566
+ "<p>Nothing flagged — the full candidate list was analyzed instead.</p>"
567
+ if scan.get("fell_back_to_candidates")
568
+ else ""
569
+ )
570
+ out.append(
571
+ _card(
572
+ "Scan",
573
+ f"<p>{esc(scan.get('scanner'))}: {esc(scan.get('flagged_count', 0))} of "
574
+ f"{esc(len(scan.get('candidates') or []))} candidates flagged.</p>" + note,
575
+ )
576
+ )
577
+
578
+ source = result.get("combination") or result.get("alphas")
579
+ if source:
580
+ rows = source.get("alphas") or []
581
+ chart = _chart(
582
+ _try_chart(
583
+ lambda: charts.render_bars_png(
584
+ [str(r.get("symbol")) for r in rows[:20]],
585
+ [float(r.get("alpha", 0.0)) for r in rows[:20]],
586
+ title="Alpha forecast by name",
587
+ value_label="annualized residual return",
588
+ )
589
+ ),
590
+ "alphas by name",
591
+ )
592
+ out.append(
593
+ _card(
594
+ "Alphas",
595
+ f"<p>{esc(alphas_heading(result))}</p>"
596
+ + (
597
+ f"<p>{esc(source['low_confidence_reason'])}</p>"
598
+ if source.get("low_confidence") and source.get("low_confidence_reason")
599
+ else ""
600
+ )
601
+ + chart
602
+ + _table(
603
+ ["Symbol", "Alpha", "z", "Residual vol"],
604
+ [
605
+ [r.get("symbol"), _num(r.get("alpha")), _num(r.get("z")), _num(r.get("residual_vol"))]
606
+ for r in rows
607
+ ],
608
+ ),
609
+ )
610
+ )
611
+
612
+ out.append(_portfolio_card(result.get("portfolio")))
613
+ out.append(_information_card(result.get("information")))
614
+ return [s for s in out if s]
615
+
616
+
617
+ def _portfolio_card(portfolio: Optional[Dict[str, Any]]) -> str:
618
+ from quantred.analytics import charts
619
+
620
+ if not portfolio:
621
+ return ""
622
+ if not portfolio.get("feasible"):
623
+ return _card(
624
+ "Portfolio",
625
+ f"<p>NOT FEASIBLE — {esc(portfolio.get('binding_constraint') or portfolio.get('note') or 'unknown')}</p>",
626
+ tone="fail",
627
+ )
628
+ d = portfolio.get("diagnostics") or {}
629
+ weights = portfolio.get("weights") or {}
630
+ chart = _chart(
631
+ _try_chart(
632
+ lambda: charts.render_bars_png(
633
+ list(weights)[:20],
634
+ [float(v) for v in list(weights.values())[:20]],
635
+ title="Proposed weights",
636
+ value_label="portfolio weight",
637
+ )
638
+ ),
639
+ "proposed portfolio weights",
640
+ )
641
+ exposures = portfolio.get("exposures")
642
+ body = (
643
+ "<p class='note'>A proposal, not an order.</p>"
644
+ + _kv(
645
+ [
646
+ ("Names", len(weights)),
647
+ ("Predicted tracking error", _pct(d.get("predicted_tracking_error"), "{:.2%}")),
648
+ ("Target tracking error", _pct(portfolio.get("target_te"), "{:.2%}")),
649
+ ("Expected active return (gross)", _pct(d.get("expected_active_return"))),
650
+ ("Expected active return (net)", _pct(d.get("expected_active_return_net"))),
651
+ ("Predicted IR", _num(d.get("predicted_ir"), "{:+.2f}")),
652
+ ("Transfer coefficient", _num(d.get("transfer_coefficient"), "{:+.2f}")),
653
+ ("Capacity", capacity_text(d) or "n/a"),
654
+ ("Risk model", portfolio.get("risk_model")),
655
+ ]
656
+ )
657
+ + chart
658
+ + _table(["Symbol", "Weight"], [[sym, _pct(w, "{:.2%}")] for sym, w in weights.items()])
659
+ )
660
+ if exposures:
661
+ body += "<h3>Factor exposures</h3>" + _table(
662
+ ["Factor", "Exposure"], [[k, _num(v, "{:+.3f}")] for k, v in sorted(exposures.items())]
663
+ )
664
+ return _card("Portfolio", body)
665
+
666
+
667
+ def _information_card(information: Optional[Dict[str, Any]]) -> str:
668
+ if not information or not information.get("periods"):
669
+ return ""
670
+ r = information
671
+ return _card(
672
+ "Information",
673
+ _kv(
674
+ [
675
+ ("Rebalances measured", r.get("periods")),
676
+ ("Horizon (bars)", r.get("horizon_bars")),
677
+ ("IC (mean)", _num(r.get("mean_ic"), "{:+.4f}")),
678
+ ("IC t-stat", _num(r.get("ic_tstat"), "{:+.2f}")),
679
+ ("Rank IC", _num(r.get("rank_ic"), "{:+.4f}")),
680
+ ("Effective breadth", _num(r.get("breadth_effective"), "{:.0f}")),
681
+ ("Mean correlation ρ̄", _num(r.get("rho_bar"), "{:.2f}")),
682
+ ("Predicted IR", _num(r.get("predicted_ir"), "{:+.2f}")),
683
+ (
684
+ "Realized IR",
685
+ f"{_num(r.get('realized_ir'), '{:+.2f}')} ± {_num(r.get('ir_standard_error'), '{:.2f}')}",
686
+ ),
687
+ (
688
+ f"P(any |t|>2 across {r.get('n_trials', 1)} trials)",
689
+ _num(r.get("multiple_testing_inflation"), "{:.2f}"),
690
+ ),
691
+ ]
692
+ ),
693
+ )
694
+
695
+
696
+ NOT_RECORDED = "not recorded"
697
+
698
+
699
+ def _recorded(value: Any, spec: str = "{:.4g}", suffix: str = "") -> str:
700
+ """A number, or the words ``not recorded`` - never a blank, a dash or a zero.
701
+
702
+ ``_num`` renders None as a dash, which is fine in a dense table but reads as "no
703
+ value" beside a suffix (``—%``) and says nothing about *why*. A run card is the
704
+ place a reader checks capital and cost, so an absent value says so.
705
+ """
706
+ if value is None or value == "":
707
+ return NOT_RECORDED
708
+ return _num(value, spec) + suffix
709
+
710
+
711
+ def _backtest_run_rows(result: Dict[str, Any]) -> List[Tuple[str, str]]:
712
+ """The Run card rows. A reused result lists what it never kept in ``not_recorded``;
713
+ a field named there is shown as such even if a stray value sits under its key."""
714
+ missing = set(result.get("not_recorded") or [])
715
+
716
+ def get(key: str) -> Any:
717
+ return None if key in missing else result.get(key)
718
+
719
+ reused = bool(result.get("memoized"))
720
+ final_label = "Final capital (derived from total return)" if reused else "Final capital"
721
+ final = result.get("final_capital_derived") if reused else get("final_capital")
722
+ request = " (as requested)" if reused else ""
723
+ rows = [
724
+ ("Initial capital" + request, _recorded(result.get("initial_capital"), "{:,.2f}")),
725
+ (final_label, _recorded(final, "{:,.2f}")),
726
+ ("Transaction cost", _recorded(get("total_cost"), "{:,.2f}")),
727
+ ("Cost drag", _recorded(get("cost_drag_pct"), "{:.2f}", "%")),
728
+ ("Trades", _recorded(get("total_trades"), "{:,.0f}")),
729
+ ]
730
+ if reused:
731
+ # Part of the run's identity, so the reused run had the same one; None is a run
732
+ # with no benchmark, not a benchmark that went unrecorded.
733
+ rows.append(("Benchmark" + request, result.get("benchmark_requested") or "none"))
734
+ rows.append(("Not recorded", ", ".join(sorted(missing)) or "nothing"))
735
+ return rows
736
+
737
+
738
+ def _backtest_sections(result: Dict[str, Any], extras: Dict[str, Any]) -> List[str]:
739
+ from quantred.analytics import charts
740
+
741
+ metrics = result.get("metrics") or {}
742
+ equity = extras.get("equity_curve")
743
+ out = []
744
+ if equity:
745
+ out.append(
746
+ _card(
747
+ "Equity",
748
+ _chart(
749
+ _try_chart(lambda: charts.render_equity_png(equity, title="Backtest equity")),
750
+ "backtest equity curve",
751
+ ),
752
+ )
753
+ )
754
+ out.append(_execution_card(result))
755
+ out.append(
756
+ _card(
757
+ "Metrics",
758
+ _table(
759
+ ["Metric", "Value"],
760
+ _metric_rows(metrics),
761
+ ),
762
+ )
763
+ )
764
+ out.append(_card("Run", _kv(_backtest_run_rows(result))))
765
+ out.append(_pnl_by_exit_card(result))
766
+ out.append(_backtest_verdicts_card(result))
767
+ out.append(_universe_card(result, extras))
768
+ return [s for s in out if s]
769
+
770
+
771
+ def _pnl_by_exit_card(result: Dict[str, Any]) -> str:
772
+ """ "Where the P&L came from", as the terminal prints it, including the warning when
773
+ nearly all of the gain is one exit path. A reused result never kept its trades, so the
774
+ card says that instead of vanishing."""
775
+ from quantred.analytics.reporting import format_exit_concentration
776
+
777
+ if result.get("memoized"):
778
+ return _card(
779
+ "Where the P&L came from",
780
+ '<p class="empty">Not recorded for a reused result - its trades were not kept. '
781
+ "Re-run with force to see whether one exit path carries the gain.</p>",
782
+ )
783
+ report = result.get("pnl_by_exit")
784
+ if not report:
785
+ return ""
786
+ block = format_exit_concentration(report)
787
+ return _card("Where the P&L came from", _pre(block.splitlines())) if block else ""
788
+
789
+
790
+ def _backtest_verdicts_card(result: Dict[str, Any]) -> str:
791
+ """The three verdicts, two of them "not assessed here": a backtest is an in-sample
792
+ replay and cannot say the edge was real."""
793
+ from quantred.analytics.reporting import format_backtest_verdicts
794
+
795
+ verdict = result.get("executability")
796
+ if verdict is None:
797
+ return _card(
798
+ "Verdicts",
799
+ _pre(
800
+ [
801
+ "Statistical validation not assessed here - was the edge real, and not overfit (`walkforward`)",
802
+ "Execution viability not recorded for this result",
803
+ "Evidence completeness not assessed here (`walkforward --bootstrap-skill`)",
804
+ ]
805
+ )
806
+ + "<p>A backtest replays one history, in-sample; it is not a validation.</p>",
807
+ )
808
+ return _card(
809
+ "Verdicts",
810
+ _pre(format_backtest_verdicts(verdict))
811
+ + "<p>A backtest replays one history, in-sample; it is not a validation.</p>",
812
+ )
813
+
814
+
815
+ def _universe_card(result: Dict[str, Any], extras: Dict[str, Any]) -> str:
816
+ """Where the universe came from and what that does not guarantee (survivorship).
817
+
818
+ The CLI hands in the terminal's own block (it knows the scanner and the source); a
819
+ payload alone knows only the names, so the fallback is the same block for a supplied
820
+ list - the one case an MCP caller can be in."""
821
+ from quantred.analytics.reporting import format_universe_provenance
822
+
823
+ lines = extras.get("universe_provenance")
824
+ if not lines:
825
+ names = result.get("symbols") or []
826
+ lines = format_universe_provenance(
827
+ candidates=names,
828
+ resolved=names,
829
+ scanner=None,
830
+ scan_clock=None,
831
+ source="the supplied list",
832
+ replayed=False,
833
+ )
834
+ return _card("Universe provenance", _pre([line for line in lines if not line.startswith("===")]))
835
+
836
+
837
+ def _execution_card(result: Dict[str, Any]) -> str:
838
+ """Whether the book that traded resembles the one asked for, check by check.
839
+
840
+ The terminal printed this and the report did not, so a forwarded report of a book
841
+ that could not be traded at its capital read as clean. A result that does not carry
842
+ the check (a reused one) says so rather than leaving the card out.
843
+ """
844
+ from quantred.analytics.performance import format_execution_value
845
+
846
+ verdict = result.get("executability")
847
+ if verdict is None:
848
+ return _card(
849
+ "Execution viability",
850
+ '<p class="empty">Not recorded for this result. Re-run it with force (the '
851
+ "<code>--force</code> flag, or <code>force=true</code> over MCP) to assess whether "
852
+ "this book can be traded at this capital.</p>",
853
+ )
854
+ if verdict.get("executable") is None:
855
+ return _card(
856
+ "Execution viability", _kv([("Not assessed", verdict.get("reason") or "nothing to judge")])
857
+ )
858
+ rows = [
859
+ [
860
+ esc(name),
861
+ esc(format_execution_value(name, check.get("value"))),
862
+ esc(format_execution_value(name, check.get("threshold"))),
863
+ _pill(check.get("passed")),
864
+ esc(check.get("note") or ""),
865
+ ]
866
+ for name, check in (verdict.get("checks") or {}).items()
867
+ ]
868
+ body = _table(["Check", "Value", "Threshold", "Result", "What it means"], rows, raw=True)
869
+ if not verdict.get("executable"):
870
+ body = _warning("Not the book that was validated — see the failing check.", level="fail") + body
871
+ return _card("Execution viability", body, tone="" if verdict.get("executable") else "fail")
872
+
873
+
874
+ def _evidence_card(evidence: Optional[Dict[str, Any]]) -> str:
875
+ """Promotion prerequisites and evidence completeness, beside the statistical verdict.
876
+
877
+ The same block the terminal prints, from the same service output. Absent evidence is
878
+ said to be absent: a card that silently vanished would read as "nothing to check".
879
+ """
880
+ if not evidence:
881
+ return _card(
882
+ "Promotion prerequisites",
883
+ "<p>Not assessed for this result. An unassessed prerequisite is unknown, not passed.</p>",
884
+ tone="warn",
885
+ )
886
+ prereq = evidence.get("prerequisites") or {}
887
+ from quantred.optimization.walk_forward import format_gate_value
888
+
889
+ rows = []
890
+ for name, check in (prereq.get("checks") or {}).items():
891
+ if not check.get("evaluated"):
892
+ rows.append(
893
+ [
894
+ _pill(None),
895
+ esc(name),
896
+ "not evaluated",
897
+ esc(format_gate_value(name, check.get("threshold"))),
898
+ esc(check.get("note")),
899
+ ]
900
+ )
901
+ continue
902
+ rows.append(
903
+ [
904
+ _pill(check.get("passed")),
905
+ esc(name),
906
+ esc(
907
+ format_gate_value(name, check.get("value"))
908
+ if check.get("value") is not None
909
+ else "not evaluated"
910
+ ),
911
+ esc(format_gate_value(name, check.get("threshold"))),
912
+ esc(check.get("note", "")),
913
+ ]
914
+ )
915
+ table = _table(["", "Check", "Value", "Threshold", "Meaning"], rows, raw=True)
916
+ excess = evidence.get("excess_return_by_fold") or {}
917
+ values = excess.get("values_pct") or []
918
+ excess_html = ""
919
+ if values:
920
+ excess_html = (
921
+ "<h3>Excess return by fold (diagnostic)</h3>"
922
+ + _table(
923
+ ["Fold", "Excess return"],
924
+ [[i, f"{v:+.2f}%"] for i, v in enumerate(values)],
925
+ )
926
+ + f"<p>Median {excess.get('median_pct'):+.2f}%, spread {excess.get('spread_pp'):.2f}pp; "
927
+ f"{excess.get('folds_lost')} of {len(values)} folds lost to the benchmark.</p>"
928
+ )
929
+ clear = bool(evidence.get("clear"))
930
+ return _card(
931
+ "Promotion prerequisites",
932
+ _kv(
933
+ [
934
+ ("Statistical validation", evidence.get("statistical_verdict")),
935
+ ("Evidence completeness", evidence.get("evidence_completeness")),
936
+ ("Benchmark", evidence.get("benchmark") or "none"),
937
+ ]
938
+ )
939
+ + table
940
+ + excess_html
941
+ + f"<p>{esc(evidence.get('note', ''))}</p>",
942
+ tone="pass" if clear else "warn",
943
+ )
944
+
945
+
946
+ def _reused_walkforward_sections(result: Dict[str, Any]) -> List[str]:
947
+ """The reduced report for a reused walk-forward: what the trial store kept, and a
948
+ plain list of what it did not. The REUSED banner is the header's; this is the body."""
949
+ promotable = result.get("promotable")
950
+ if promotable is None:
951
+ text, tone = "Promotability was not recorded for the original run", "warn"
952
+ elif promotable:
953
+ text, tone = "Recorded as PROMOTABLE (statistical gates only) by the original run", "warn"
954
+ else:
955
+ text, tone = "Recorded as NOT PROMOTABLE by the original run", "fail"
956
+ not_retained = result.get("not_retained") or [
957
+ "per-fold detail",
958
+ "promotion prerequisites (evidence)",
959
+ "holdout",
960
+ "gate checks",
961
+ ]
962
+ facts = [
963
+ ("Efficiency (OOS / IS)", _recorded(result.get("efficiency"), "{:.3f}")),
964
+ ("Trials total", _recorded(result.get("n_trials_total"), "{:,.0f}")),
965
+ ("Original trial", result.get("trial_id") or NOT_RECORDED),
966
+ ("Original recorded at", result.get("trial_ts") or NOT_RECORDED),
967
+ ]
968
+ return [
969
+ _card(
970
+ "Verdict (from the original run)",
971
+ f'<p class="verdict {tone}">{esc(text)}</p>'
972
+ + _kv(facts)
973
+ + "<p>The individual gate checks were not retained, so no pass/fail table is shown. "
974
+ "Re-run with force to re-verify.</p>",
975
+ tone=tone,
976
+ ),
977
+ _card(
978
+ "Not retained in a reused result",
979
+ "<ul>"
980
+ + "".join(f"<li>{esc(item)}</li>" for item in not_retained)
981
+ + "</ul>"
982
+ + f"<p>{esc(result.get('evidence_unavailable') or '')}</p>",
983
+ tone="warn",
984
+ ),
985
+ _card(
986
+ "Out-of-sample aggregate",
987
+ # The fresh report's rows, so a placeholder zero beside an unavailable flag
988
+ # reads as not measured here too.
989
+ _table(["Metric", "Value"], _metric_rows(result.get("oos_aggregate") or {})),
990
+ ),
991
+ *_reused_holdout_card(result.get("holdout_record")),
992
+ ]
993
+
994
+
995
+ def _reused_holdout_card(holdout: Any) -> List[str]:
996
+ """What the original run recorded of its holdout, in the terminal's words.
997
+
998
+ Found in review: the reused payload carried a recorded holdout and took it off the
999
+ not-retained list, while this report showed it nowhere - neither present nor missing.
1000
+ """
1001
+ if not isinstance(holdout, dict):
1002
+ return []
1003
+ from quantred.analytics.reporting import format_holdout
1004
+
1005
+ return [_card("Holdout (from the original run)", _pre(format_holdout(holdout)))]
1006
+
1007
+
1008
+ def _walkforward_verdicts_card(gate_report: Dict[str, Any], evidence: Optional[Dict[str, Any]]) -> str:
1009
+ """The terminal's "Verdicts" block: statistical validation and evidence completeness,
1010
+ with execution viability named as not assessed here (a walk-forward has no capital)."""
1011
+ from quantred.analytics.reporting import format_verdicts
1012
+ from quantred.services.analysis import evidence_verdicts
1013
+
1014
+ if not gate_report.get("checks"):
1015
+ return ""
1016
+ prereq = (evidence or {}).get("prerequisites")
1017
+ if prereq:
1018
+ statistical, completeness = evidence_verdicts(gate_report, prereq)
1019
+ else:
1020
+ failed = [n for n, c in gate_report["checks"].items() if not c.get("passed")]
1021
+ statistical = "PASS" if gate_report.get("promotable") else f"FAIL - {', '.join(failed)}"
1022
+ completeness = None
1023
+ return _card("Verdicts", _pre(format_verdicts(statistical=statistical, evidence=completeness)))
1024
+
1025
+
1026
+ def _walkforward_sections(result: Dict[str, Any], extras: Dict[str, Any]) -> List[str]:
1027
+ from quantred.analytics import charts
1028
+
1029
+ if result.get("memoized") and "folds" not in result:
1030
+ return _reused_walkforward_sections(result)
1031
+ from quantred.optimization.walk_forward import format_gate_value
1032
+
1033
+ gate_report = result.get("gate_report") or {}
1034
+ checks = gate_report.get("checks") or {}
1035
+ evidence = result.get("evidence")
1036
+ # Passing the six gates is not a clearance while a prerequisite is unmet or unknown:
1037
+ # the colour and the words both say so rather than leaving it to a later section.
1038
+ if not gate_report.get("promotable"):
1039
+ verdict_class, verdict_text = "fail", "NOT PROMOTABLE"
1040
+ elif evidence and evidence.get("clear"):
1041
+ verdict_class, verdict_text = "pass", "PROMOTABLE — evidence clear"
1042
+ else:
1043
+ verdict_class, verdict_text = "warn", "PROMOTABLE (statistical gates only) — evidence NOT clear"
1044
+ out = [
1045
+ _card(
1046
+ "Verdict",
1047
+ f'<p class="verdict {verdict_class}">{esc(verdict_text)}</p>'
1048
+ + _table(
1049
+ ["", "Gate", "Value", "Threshold", "Computed over"],
1050
+ [
1051
+ [
1052
+ _pill(c.get("passed")),
1053
+ esc(n),
1054
+ esc(format_gate_value(n, c.get("value"))),
1055
+ esc(format_gate_value(n, c.get("threshold"))),
1056
+ esc(c.get("basis", "")),
1057
+ ]
1058
+ for n, c in sorted(checks.items())
1059
+ ],
1060
+ raw=True,
1061
+ )
1062
+ + (
1063
+ _chart(
1064
+ _try_chart(
1065
+ lambda: charts.render_gate_png(
1066
+ checks,
1067
+ promotable=bool(gate_report.get("promotable")),
1068
+ title="WALK-FORWARD VERDICT",
1069
+ stats=[
1070
+ ("OOS Sharpe (median)", result.get("median_oos_sharpe", 0.0)),
1071
+ ("Efficiency (OOS / IS)", result.get("median_efficiency", 0.0)),
1072
+ ("OOS trades", result.get("total_oos_trades", 0)),
1073
+ ],
1074
+ )
1075
+ ),
1076
+ "walk-forward gate scorecard",
1077
+ )
1078
+ if checks
1079
+ else ""
1080
+ ),
1081
+ tone=verdict_class,
1082
+ ),
1083
+ _evidence_card(evidence),
1084
+ _walkforward_verdicts_card(gate_report, evidence),
1085
+ _card(
1086
+ "Folds",
1087
+ _table(
1088
+ ["#", "IS window", "OOS window", "IS Sharpe", "OOS Sharpe", "OOS trades", "Trials"],
1089
+ [
1090
+ [
1091
+ f.get("index"),
1092
+ f"{_short_date((f.get('is_window') or {}).get('start'))}→"
1093
+ f"{_short_date((f.get('is_window') or {}).get('end'))}",
1094
+ f"{_short_date((f.get('oos_window') or {}).get('start'))}→"
1095
+ f"{_short_date((f.get('oos_window') or {}).get('end'))}",
1096
+ _num(f.get("is_sharpe"), "{:+.2f}"),
1097
+ _num(f.get("oos_sharpe"), "{:+.2f}"),
1098
+ f.get("oos_trades"),
1099
+ f.get("n_trials"),
1100
+ ]
1101
+ for f in result.get("folds") or []
1102
+ ],
1103
+ ),
1104
+ ),
1105
+ _card(
1106
+ "Out-of-sample aggregate",
1107
+ _table(
1108
+ ["Metric", "Value"],
1109
+ _metric_rows(result.get("oos_aggregate") or {}),
1110
+ ),
1111
+ ),
1112
+ ]
1113
+ if result.get("holdout"):
1114
+ out.append(
1115
+ _card(
1116
+ "Holdout",
1117
+ _table(
1118
+ ["Metric", "Value"],
1119
+ [[k, _metric_cell(k, v)] for k, v in sorted((result["holdout"]).items())],
1120
+ ),
1121
+ )
1122
+ )
1123
+ return out
1124
+
1125
+
1126
+ def _info_sections(result: Dict[str, Any], extras: Dict[str, Any]) -> List[str]:
1127
+ card = _information_card(result)
1128
+ body = [card] if card else []
1129
+ body.append(
1130
+ _card(
1131
+ "How to read this",
1132
+ f"<p>{esc(result.get('note', ''))}</p>",
1133
+ )
1134
+ )
1135
+ return body
1136
+
1137
+
1138
+ def _footer() -> str:
1139
+ from quantred.settings import BRAND
1140
+
1141
+ return (
1142
+ f"<footer><p>Generated by {BRAND}. This file is self-contained: opening it "
1143
+ "issues no network requests. It is a research artifact, not investment advice.</p></footer>"
1144
+ )
1145
+
1146
+
1147
+ def write_html(result: Dict[str, Any], kind: str, path: str, **kwargs: Any) -> str:
1148
+ """Render and write the report; return the path written."""
1149
+ document = render_html(result, kind, **kwargs)
1150
+ with open(path, "w", encoding="utf-8") as fh:
1151
+ fh.write(document)
1152
+ return path
1153
+
1154
+
1155
+ def _json_block(payload: Any) -> str: # pragma: no cover - reserved for a detail view
1156
+ return f"<pre>{esc(json.dumps(payload, indent=2, sort_keys=True))}</pre>"
1157
+
1158
+
1159
+ #: Inline, dependency-free, and light/dark aware. System font stacks only - a web
1160
+ #: font would be an external request, which this document does not make.
1161
+ _CSS = """
1162
+ :root { color-scheme: light dark;
1163
+ --bg:#ffffff; --fg:#1b1f24; --muted:#6b7280; --line:#e5e7eb; --card:#ffffff;
1164
+ --pass:#16a34a; --fail:#dc2626; --warn:#b45309; --accent:#2563eb; }
1165
+ @media (prefers-color-scheme: dark) { :root {
1166
+ --bg:#0f1216; --fg:#e6e8eb; --muted:#9aa3ad; --line:#2a2f36; --card:#161a20;
1167
+ --pass:#4ade80; --fail:#f87171; --warn:#fbbf24; --accent:#60a5fa; } }
1168
+ * { box-sizing: border-box; }
1169
+ body { margin:0; background:var(--bg); color:var(--fg); font:15px/1.55 -apple-system,
1170
+ BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
1171
+ main { max-width: 60rem; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
1172
+ h1 { font-size:1.6rem; margin:0 0 1.25rem; }
1173
+ h2 { font-size:1.05rem; margin:0 0 .75rem; letter-spacing:.02em; text-transform:uppercase;
1174
+ color:var(--muted); }
1175
+ h3 { font-size:.95rem; margin:1.25rem 0 .5rem; }
1176
+ .card { background:var(--card); border:1px solid var(--line); border-radius:10px;
1177
+ padding:1.1rem 1.25rem; margin:0 0 1rem; }
1178
+ .card.pass { border-left:4px solid var(--pass); }
1179
+ .card.fail { border-left:4px solid var(--fail); }
1180
+ .card.warn { border-left:4px solid var(--warn); }
1181
+ .banner { border-radius:8px; padding:.8rem 1rem; margin:0 0 .75rem; font-weight:600;
1182
+ border:1px solid var(--line); }
1183
+ .banner.fail { background:color-mix(in srgb, var(--fail) 12%, transparent); border-color:var(--fail); }
1184
+ .banner.warn, .banner.reuse { background:color-mix(in srgb, var(--warn) 14%, transparent);
1185
+ border-color:var(--warn); }
1186
+ .banner.note { background:color-mix(in srgb, var(--accent) 10%, transparent); border-color:var(--accent); }
1187
+ .verdict { font-size:1.2rem; font-weight:700; margin:.25rem 0 1rem; }
1188
+ .verdict.pass { color:var(--pass); } .verdict.fail { color:var(--fail); }
1189
+ .verdict.warn { color:var(--warn); }
1190
+ .scroll { overflow-x:auto; }
1191
+ table { border-collapse:collapse; width:100%; font-size:.9rem; }
1192
+ th, td { text-align:left; padding:.4rem .6rem; border-bottom:1px solid var(--line); white-space:nowrap; }
1193
+ th { color:var(--muted); font-weight:600; }
1194
+ td:last-child, th:last-child { white-space:normal; }
1195
+ dl.kv { display:grid; grid-template-columns:repeat(auto-fit,minmax(15rem,1fr)); gap:.4rem 1.5rem; margin:0; }
1196
+ dl.kv div { display:flex; justify-content:space-between; gap:1rem; border-bottom:1px solid var(--line);
1197
+ padding:.3rem 0; }
1198
+ dt { color:var(--muted); } dd { margin:0; font-variant-numeric:tabular-nums; text-align:right; }
1199
+ .pill { font-size:.72rem; font-weight:700; padding:.12rem .45rem; border-radius:999px; }
1200
+ .pill.pass { background:var(--pass); color:#fff; } .pill.fail { background:var(--fail); color:#fff; }
1201
+ .pill.neutral { background:var(--muted); color:#fff; }
1202
+ img { max-width:100%; height:auto; display:block; margin:1rem 0; border-radius:6px; }
1203
+ .empty, .note { color:var(--muted); font-style:italic; }
1204
+ code { font-family:ui-monospace, SFMono-Regular, Menlo, monospace; font-size:.85em; }
1205
+ pre { overflow-x:auto; background:var(--card); padding:.75rem; border-radius:6px; }
1206
+ footer { color:var(--muted); font-size:.82rem; margin-top:2rem; border-top:1px solid var(--line);
1207
+ padding-top:1rem; }
1208
+ """