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,1672 @@
1
+ """Human-readable rendering of research results.
2
+
3
+ Kept separate from metric *computation* so the numbers can be consumed
4
+ programmatically (e.g. by the optimizer, or rendered to another medium) without
5
+ any formatting concerns. Nothing here computes: a renderer that derives a number
6
+ is a renderer that can disagree with the report it is summarizing.
7
+ """
8
+
9
+ import logging
10
+ from datetime import datetime, timezone
11
+ from typing import Any, Dict, List, Optional, Sequence
12
+
13
+ from quantred.analytics import metrics as m
14
+ from quantred.analytics import performance
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ #: The capital range the capacity search bisects over. Here, below the service that
20
+ #: searches it, so the words describing a bound cannot drift from the bound.
21
+ CAPACITY_SEARCH_FLOOR = 1e3
22
+ CAPACITY_SEARCH_CEILING = 1e12
23
+
24
+
25
+ def capacity_text(diagnostics: Dict[str, Any]) -> Optional[str]:
26
+ """Words for a portfolio's capacity result, or ``None`` when none was computed.
27
+
28
+ A number only when one was solved for; otherwise which bound the search hit, so a
29
+ search sentinel never reads as a measured capital.
30
+ """
31
+ if "capacity_status" not in diagnostics:
32
+ return None
33
+ status = diagnostics["capacity_status"]
34
+ capital = diagnostics.get("capacity_capital")
35
+ if status == "measured" and capital is not None:
36
+ return f"≈ ${capital:,.0f} (where √-impact erases the alpha)"
37
+ return {
38
+ "no_edge": "not applicable - expected gross alpha is not positive, so there is no capital at which "
39
+ "it survives cost",
40
+ "no_priced_holdings": "not measured - no holding had the price and volume history the cost model needs",
41
+ "below_search_floor": "not measured - the alpha does not cover √-impact cost even at the smallest "
42
+ f"capital searched (${CAPACITY_SEARCH_FLOOR:,.0f})",
43
+ "above_search_ceiling": "not measured - the alpha still covers √-impact cost at the largest capital "
44
+ f"searched (${CAPACITY_SEARCH_CEILING:,.0f})",
45
+ }.get(status, f"unknown ({status})")
46
+
47
+
48
+ def small_aware(value: Any, spec: str) -> str:
49
+ """``format(value, spec)``, except a nonzero value that the spec would print as zero.
50
+
51
+ ``+0.0000`` and ``-0.00`` read as "exactly nothing" for a value that is merely
52
+ small; those are shown in scientific notation instead (a percent spec shows the
53
+ percent).
54
+ """
55
+ if value is None:
56
+ return "n/a"
57
+ text = format(value, spec)
58
+ if value != 0 and not any(ch in "123456789" for ch in text):
59
+ scale = 100.0 if spec.endswith("%") else 1.0
60
+ return f"{value * scale:.1e}" + ("%" if scale == 100.0 else "")
61
+ return text
62
+
63
+
64
+ # (metric key, label, format spec), grouped into report sections.
65
+ _SECTIONS = [
66
+ (
67
+ "Returns",
68
+ [
69
+ ("total_return", "Total Return", "{:.2f}%"),
70
+ ("cagr", "CAGR", "{:.2f}%"),
71
+ ("buy_hold_return", "Buy & Hold (universe)", "{:.2f}%"),
72
+ ("benchmark_buy_hold_return", "Buy & Hold (benchmark)", "{:.2f}%"),
73
+ ("annualized_volatility", "Annualized Volatility", "{:.2f}%"),
74
+ ],
75
+ ),
76
+ (
77
+ "Risk-adjusted",
78
+ [
79
+ ("sharpe_ratio", "Sharpe Ratio", "{:.2f}"),
80
+ ("probabilistic_sharpe_ratio", " Probabilistic Sharpe", "{:.2f}"),
81
+ ("deflated_sharpe_ratio", " Deflated Sharpe", "{:.2f}"),
82
+ ("sortino_ratio", "Sortino Ratio", "{:.2f}"),
83
+ ("calmar_ratio", "Calmar Ratio", "{:.2f}"),
84
+ ("martin_ratio", "Martin Ratio (UPI)", "{:.2f}"),
85
+ ("information_ratio", "Information Ratio", "{:.2f}"),
86
+ ("treynor_ratio", "Treynor Ratio", "{:.2f}"),
87
+ ],
88
+ ),
89
+ (
90
+ "Drawdown & tail",
91
+ [
92
+ ("max_drawdown", "Max Drawdown", "{:.2f}%"),
93
+ ("max_drawdown_duration", "Max DD Duration (days)", "{:.0f}"),
94
+ ("ulcer_index", "Ulcer Index", "{:.2f}"),
95
+ ("recovery_factor", "Recovery Factor", "{:.2f}"),
96
+ ("var_95", "VaR (95%)", "{:.2f}%"),
97
+ ("cvar_95", "CVaR (95%)", "{:.2f}%"),
98
+ ("var_99", "VaR (99%)", "{:.2f}%"),
99
+ ("exposure", "Time in Market", "{:.1f}%"),
100
+ ],
101
+ ),
102
+ (
103
+ "Trades",
104
+ [
105
+ ("total_trades", "Total Trades", "{:.0f}"),
106
+ ("win_rate", "Win Rate", "{:.2f}%"),
107
+ ("profit_factor", "Profit Factor", "{:.2f}"),
108
+ ("payoff_ratio", "Payoff Ratio", "{:.2f}"),
109
+ ("expectancy", "Expectancy", "${:.2f}"),
110
+ ("sqn", "System Quality (SQN)", "{:.2f}"),
111
+ ("kelly_criterion", "Kelly Fraction", "{:.2f}"),
112
+ ("max_consecutive_wins", "Max Consecutive Wins", "{:.0f}"),
113
+ ("max_consecutive_losses", "Max Consecutive Losses", "{:.0f}"),
114
+ ("avg_trade_duration", "Avg Trade Duration (h)", "{:.2f}"),
115
+ ("mae_pct", "Avg MAE", "{:.2f}%"),
116
+ ("mfe_pct", "Avg MFE", "{:.2f}%"),
117
+ ("avg_win", "Average Win", "${:.2f}"),
118
+ ("avg_loss", "Average Loss", "${:.2f}"),
119
+ ("largest_win", "Largest Win", "${:.2f}"),
120
+ ("largest_loss", "Largest Loss", "${:.2f}"),
121
+ ],
122
+ ),
123
+ (
124
+ "Benchmark-relative",
125
+ [
126
+ ("alpha", "Alpha (per period)", "{:.5f}"),
127
+ ("beta", "Beta", "{:.2f}"),
128
+ ("r_squared", "R-squared", "{:.2f}"),
129
+ ],
130
+ ),
131
+ ]
132
+
133
+
134
+ # Flat view for backward compatibility / simple consumers.
135
+ _ROWS = [row for _, rows in _SECTIONS for row in rows]
136
+
137
+
138
+ def _execution_lines(execution: Optional[Dict[str, Any]]) -> List[str]:
139
+ """The gap between the book the sizer asked for and the one that could be traded.
140
+
141
+ Silent by construction until now: whole-share rounding just happens, and it scales
142
+ with how small the account is. Shown whenever anything was lost, and shown with
143
+ both numbers beside their thresholds rather than as a verdict on its own - the same
144
+ rule the promotion gates follow.
145
+ """
146
+ if not execution:
147
+ return []
148
+ verdict = performance.execution_verdict(execution)
149
+ if verdict["executable"] is None or (verdict["executable"] and not execution.get("rounding_drag_pct")):
150
+ return []
151
+
152
+ mark = "(!)" if not verdict["executable"] else "(i)"
153
+ lines = [
154
+ "",
155
+ f"--- Execution & cost {mark} ---",
156
+ f"{'Intended notional':28}${execution.get('requested_notional', 0.0):,.2f}",
157
+ f"{'Filled notional':28}${execution.get('filled_notional', 0.0):,.2f}",
158
+ ]
159
+ for name, check in verdict["checks"].items():
160
+ flag = "PASS" if check["passed"] else "FAIL"
161
+ # Unit per check, from the module that declares the check - not one format
162
+ # applied to all of them. `book_breadth` carries a position count and rendered
163
+ # as "1.00%", which is a different claim from the one the check makes.
164
+ value = performance.format_execution_value(name, check["value"])
165
+ threshold = performance.format_execution_value(name, check["threshold"])
166
+ lines.append(f" [{flag}] {name:22}{value} vs {threshold}")
167
+ lost = execution.get("positions_rounded_to_zero", 0) + execution.get("positions_below_min_notional", 0)
168
+ if lost:
169
+ lines.append(
170
+ f"{' entries never opened':28}{lost} "
171
+ f"({execution.get('positions_rounded_to_zero', 0)} rounded to zero, "
172
+ f"{execution.get('positions_below_min_notional', 0)} below min notional)"
173
+ )
174
+ if not verdict["executable"]:
175
+ lines.append(" Not the book that was validated - see the failing check above.")
176
+ return lines
177
+
178
+
179
+ def _leg_lines(legs: Optional[Dict[str, Any]]) -> List[str]:
180
+ """Each side of a long/short book, separately.
181
+
182
+ Shown only when both sides actually traded: a long-only run has nothing to
183
+ decompose, and a table with an empty short row is noise that teaches readers to
184
+ skip the block.
185
+
186
+ The question it exists to answer is what a net figure structurally cannot: a
187
+ near-zero net beta is either genuinely small exposure on both sides, or a large
188
+ long beta cancelling a large short one. Same number, opposite risk.
189
+ """
190
+ if not legs or not all(legs.get(side, {}).get("trades") for side in ("long", "short")):
191
+ return []
192
+ lines = [
193
+ "",
194
+ "--- Legs (diagnostic; no thresholds) ---",
195
+ f" {'leg':7}{'return':>9}{'vol':>8}{'max DD':>9}{'beta':>8}{'corr':>7}{'trades':>8}{'cost':>11}",
196
+ ]
197
+ for name in ("long", "short"):
198
+ leg = legs[name]
199
+ beta, corr = leg.get("beta"), leg.get("benchmark_correlation")
200
+ lines.append(
201
+ f" {name:7}{leg['return_pct']:>+8.2f}%{leg['volatility_pct']:>7.3f}%"
202
+ f"{leg['max_drawdown_pct']:>8.2f}%"
203
+ f"{'n/a' if beta is None else format(beta, '.3f'):>8}"
204
+ f"{'n/a' if corr is None else format(corr, '.2f'):>7}"
205
+ f"{leg['trades']:>8}{leg['cost']:>11,.0f}"
206
+ )
207
+ betas = [legs[name].get("beta") for name in ("long", "short")]
208
+ if all(b is not None for b in betas) and min(abs(b) for b in betas) > 0.3:
209
+ lines.append(
210
+ " Both legs carry real market exposure - a small net beta here is two "
211
+ "exposures cancelling, not an absence of them."
212
+ )
213
+ return lines
214
+
215
+
216
+ def format_universe_provenance(
217
+ *,
218
+ candidates: Sequence[str],
219
+ resolved: Sequence[str],
220
+ scanner: Optional[str],
221
+ scan_clock: Optional[str],
222
+ source: str,
223
+ replayed: bool,
224
+ ) -> List[str]:
225
+ """Where this run's universe came from, and what that does and does not guarantee.
226
+
227
+ A 61-name large-cap list is not "the market", and a report that leaves the universe
228
+ in the background invites it to be read as one. The awkward line is the last: a
229
+ hand-supplied candidate list is *today's* names applied to history, so anything that
230
+ left the list - delisted, acquired, collapsed - is already absent from every
231
+ backtest run over it.
232
+
233
+ That is stated rather than measured, because measuring it needs point-in-time
234
+ membership data this project does not ingest. Saying "this is survivorship-prone by
235
+ construction" is honest; computing a survivorship number from a list that has none
236
+ would not be.
237
+ """
238
+ lines = [
239
+ "",
240
+ "=== Universe provenance ===",
241
+ f" {'candidates':16}{len(candidates)} names from {source}",
242
+ ]
243
+ if scanner and scanner != "none":
244
+ clock = f" as of {scan_clock}" if scan_clock else ""
245
+ lines.append(f" {'scanner':16}{scanner}{clock}")
246
+ lines.append(f" {'resolved':16}{len(resolved)} of {len(candidates)} names")
247
+ else:
248
+ lines.append(f" {'scanner':16}none - candidates traded as-is")
249
+ lines.append(f" {'universe':16}{'replayed from config' if replayed else 'resolved this run'}")
250
+ lines.append(
251
+ f" {'survivorship':16}a hand-supplied list is today's names applied to history; "
252
+ "membership was not point-in-time"
253
+ )
254
+ return lines
255
+
256
+
257
+ def format_verdicts(
258
+ *,
259
+ statistical: Optional[Dict[str, Any]] = None,
260
+ execution: Optional[Dict[str, Any]] = None,
261
+ evidence: Optional[Dict[str, Any]] = None,
262
+ ) -> List[str]:
263
+ """The three facts about a candidate, side by side and separately labelled.
264
+
265
+ They were already three verdicts that never collapse into one another, but each was
266
+ printed by a different command at a different moment, so nothing ever showed a
267
+ reader all three. That is how a backtest replay reads as "approved" when it only
268
+ means "this saved config runs and its history looks good".
269
+
270
+ A verdict this command could not assess is printed as **not assessed here**, with
271
+ the command that would assess it - the same rule the prerequisites follow, because
272
+ an unknown rendered as a blank is an unknown a reader fills in optimistically.
273
+ """
274
+ rows = [
275
+ ("Statistical validation", statistical, "was the edge real, and not overfit", "walkforward"),
276
+ ("Execution viability", execution, "can this book be traded at this capital", "backtest"),
277
+ (
278
+ "Evidence completeness",
279
+ evidence,
280
+ "what has actually been checked",
281
+ "walkforward --bootstrap-skill",
282
+ ),
283
+ ]
284
+ lines = ["", "=== Verdicts ==="]
285
+ for label, verdict, question, command in rows:
286
+ if verdict is None:
287
+ lines.append(f" {label:24}not assessed here - {question} (`{command}`)")
288
+ continue
289
+ lines.append(f" {label:24}{verdict}")
290
+ lines.append(" Three separate facts. Clearing one says nothing about the others.")
291
+ return lines
292
+
293
+
294
+ def format_backtest_verdicts(executability: Dict[str, Any]) -> List[str]:
295
+ """A backtest can speak to execution and to nothing else; this says so.
296
+
297
+ ``executability`` is :func:`performance.execution_verdict`'s answer. One definition
298
+ for the terminal and the HTML report, so the "not assessed here" lines that stop a
299
+ replay reading as approval cannot be missing from either.
300
+ """
301
+ executable = executability.get("executable")
302
+ summary = {
303
+ None: "UNKNOWN - nothing was attempted",
304
+ True: "PASS",
305
+ False: f"FAIL - {executability.get('reason')}",
306
+ }[executable]
307
+ return format_verdicts(execution=summary)
308
+
309
+
310
+ def format_backtest_report(
311
+ metrics: Dict[str, float],
312
+ initial_capital: float,
313
+ final_capital: Optional[float],
314
+ title: str = "Backtest Results",
315
+ execution: Optional[Dict[str, Any]] = None,
316
+ legs: Optional[Dict[str, Any]] = None,
317
+ ) -> str:
318
+ """Render metrics as an aligned, fixed-width text block, grouped by section."""
319
+ # A reused result may carry no final capital at all; absent is not zero.
320
+ final = f"${final_capital:,.2f}" if final_capital is not None else "not recorded"
321
+ lines = [f"=== {title} ===", f"{'Capital':28}${initial_capital:,.2f} -> {final}"]
322
+ if metrics.get("low_sample"):
323
+ lines.append(
324
+ f"{'(!) low sample':28}fewer than {performance.LOW_SAMPLE_TRADES} trades - treat ratios with caution"
325
+ )
326
+ if not metrics.get("benchmark_available", True):
327
+ lines.append(f"{'(i) no benchmark':28}alpha/beta/information-ratio unavailable")
328
+ elif not metrics.get("treynor_available", True):
329
+ lines.append(
330
+ f"{'(i) beta near zero':28}Treynor unavailable (|beta| < "
331
+ f"{m.MIN_ABS_BETA_FOR_TREYNOR:g}; excess return per unit of beta is not "
332
+ f"meaningful for a book with no market exposure)"
333
+ )
334
+ lines.extend(_execution_lines(execution))
335
+ lines.extend(_leg_lines(legs))
336
+ undeflated = not metrics.get("deflation_applied", True)
337
+ for section, rows in _SECTIONS:
338
+ section_lines = []
339
+ for key, label, fmt in rows:
340
+ if key == "deflated_sharpe_ratio" and undeflated:
341
+ # One trial deflates against nothing, so the row says what it is rather
342
+ # than borrowing the name of a correction that was not applied.
343
+ # Named for what the number is - a probability, not a Sharpe ratio, which
344
+ # the earlier "Sharpe (undeflated)" read as.
345
+ label = " PSR, undeflated (1 trial)"
346
+ if key in metrics and _is_probability(key):
347
+ section_lines.append(f"{label:28}{_prob_cell(metrics[key], key)}")
348
+ elif key in metrics:
349
+ value = fmt.format(metrics[key]) if metrics[key] != float("inf") else "inf"
350
+ section_lines.append(f"{label:28}{value}")
351
+ if section_lines:
352
+ lines.append(f"--- {section} ---")
353
+ lines.extend(section_lines)
354
+ lines.append("=" * (len(title) + 8))
355
+ return "\n".join(lines)
356
+
357
+
358
+ #: How to get back what a reused backtest never kept, in both spellings: a reader of the
359
+ #: payload may be on either surface. Here rather than in the service that fills the
360
+ #: payload, because this layer renders it and must not import upward.
361
+ BACKTEST_RECOVERY_NOTE = "Re-run with --force (CLI) or force=true (MCP) to recompute them."
362
+
363
+
364
+ def format_not_recorded(payload: Dict[str, Any]) -> str:
365
+ """One line naming what a reused result never kept, and how to get it back.
366
+
367
+ Empty for a fresh result, so a caller can print it unconditionally.
368
+ """
369
+ missing = payload.get("not_recorded") or []
370
+ if not missing:
371
+ return ""
372
+ return f"Not recorded for this reused result: {', '.join(missing)}. {BACKTEST_RECOVERY_NOTE}"
373
+
374
+
375
+ def log_backtest_report(
376
+ metrics: Dict[str, float],
377
+ initial_capital: float,
378
+ final_capital: float,
379
+ execution: Optional[Dict[str, Any]] = None,
380
+ legs: Optional[Dict[str, Any]] = None,
381
+ ) -> None:
382
+ """Log the rendered report at INFO.
383
+
384
+ ``legs`` is forwarded. It was accepted here and dropped on the way to the
385
+ formatter, so the long/short decomposition - the block that distinguishes "no
386
+ exposure" from "two exposures cancelling" - was unreachable from ``backtest``
387
+ while being fully covered by tests that called the formatter directly. A test
388
+ that renders *through* this function is what catches that.
389
+ """
390
+ logger.info(
391
+ "\n%s",
392
+ format_backtest_report(metrics, initial_capital, final_capital, execution=execution, legs=legs),
393
+ )
394
+
395
+
396
+ def _parse_iso(text: str) -> datetime:
397
+ """An ISO timestamp, on every supported Python.
398
+
399
+ ``datetime.fromisoformat`` before 3.11 refuses a ``Z`` suffix and fractional seconds
400
+ of other than three or six digits, so a stamp such as ``03:00:00.5+00:00`` printed
401
+ as raw UTC text on 3.10 and as New York time on 3.11. pandas reads both.
402
+ """
403
+ import pandas as pd
404
+
405
+ try:
406
+ return datetime.fromisoformat(text.replace("Z", "+00:00"))
407
+ except ValueError:
408
+ try:
409
+ return pd.Timestamp(text).to_pydatetime()
410
+ except (ValueError, TypeError) as exc:
411
+ raise ValueError(str(exc)) from exc
412
+
413
+
414
+ def _age_str(ts: Optional[str]) -> str:
415
+ """A human ``"N days"``/``"N hours"`` age for an ISO timestamp; ``"unknown"``
416
+ when it can't be parsed - never let a formatting failure block the notice."""
417
+ if not ts:
418
+ return "unknown age"
419
+ try:
420
+ when = _parse_iso(ts)
421
+ if when.tzinfo is None:
422
+ when = when.replace(tzinfo=timezone.utc)
423
+ delta = datetime.now(timezone.utc) - when
424
+ except ValueError:
425
+ return "unknown age"
426
+ seconds = delta.total_seconds()
427
+ if seconds < 0:
428
+ return "0 days"
429
+ if seconds < 3600:
430
+ return f"{int(seconds // 60)} minutes"
431
+ if seconds < 86400:
432
+ return f"{seconds / 3600:.1f} hours"
433
+ return f"{seconds / 86400:.1f} days"
434
+
435
+
436
+ def format_cached_notice(
437
+ row: Dict[str, Any], *, current_accounting: Optional[int] = None, vintage_available: bool = False
438
+ ) -> str:
439
+ """A prominent, unmistakable "this is a memo, not a fresh verification" banner.
440
+
441
+ Every command that can serve a memoized trial (``backtest``/``optimize``/
442
+ ``walkforward``, CLI and MCP alike) renders this immediately above its report
443
+ so the reused case looks visually distinct everywhere, not just wherever it
444
+ was first implemented. Always names the original run's id and timestamp/age -
445
+ never let a stale number pass as freshly checked.
446
+
447
+ ``vintage_available`` is ``True`` only when *this* lookup was itself keyed on
448
+ a bar-cache data-vintage stamp and still matched - which, by construction,
449
+ means the match's underlying data vintage is identical to the stored trial's
450
+ (that's what made the dedup hash match). In that case the data caveat below is
451
+ replaced with a positive statement rather than dropped silently; a run without
452
+ the bar cache (``--cache``/``--offline``) still gets the original caveat, since
453
+ for it nothing has actually changed.
454
+ """
455
+ ts = row.get("ts")
456
+ lines = [
457
+ f"=== REUSED — trial {row.get('id', '?')} from {ts or 'unknown time'} ({_age_str(ts)} old) ===",
458
+ "Not re-run: an identical trial already exists in the trial store. Pass --force/--rerun to re-verify.",
459
+ ]
460
+ row_accounting = row.get("accounting")
461
+ if current_accounting is not None and row_accounting is not None and row_accounting != current_accounting:
462
+ lines.append(
463
+ f"(!) stored under accounting v{row_accounting}, engine is v{current_accounting} — "
464
+ "its metrics are NOT comparable to a fresh run"
465
+ )
466
+ if vintage_available:
467
+ lines.append(
468
+ "(i) data vintage confirmed: the bar cache's fetch stamp for this exact window matches the "
469
+ "original run's — this reuse is vintage-safe, not just same-inputs"
470
+ )
471
+ else:
472
+ lines.append(
473
+ "(i) reused on 'same requested inputs' only — not yet guaranteed identical underlying data "
474
+ "(no data-vintage stamp; re-run with --cache/--offline for a vintage-safe reuse); a rare vendor "
475
+ "correction/backfill since the original run could differ"
476
+ )
477
+ return "\n".join(lines)
478
+
479
+
480
+ def format_verdict_report(result: Dict[str, Any]) -> str:
481
+ """Render a composite research result as one consolidated text report.
482
+
483
+ Pure formatting over the composite object: every number here was produced by
484
+ the step that owns it. The verdict banner is printed **last and loudest** -
485
+ the sections above it are evidence, and a report that buries its own conclusion
486
+ under a pretty table is worse than no report.
487
+ """
488
+ inputs = result.get("inputs") or {}
489
+ window = inputs.get("window") or {}
490
+ universe = inputs.get("universe") or []
491
+ provenance = result.get("provenance") or {}
492
+ lines = [
493
+ "",
494
+ f"=== Research verdict: '{inputs.get('strategy', '?')}' "
495
+ f"{_short_date(window.get('start'))}..{_short_date(window.get('end'))} ===",
496
+ f" universe: {len(universe)} names ({_symbol_summary(universe)})",
497
+ f" timeframe {inputs.get('timeframe', '?')} | benchmark {inputs.get('benchmark', '?')} | "
498
+ f"cost {_cost_summary(inputs.get('cost'))}",
499
+ ]
500
+ if provenance:
501
+ lines.append(
502
+ f" provenance: git {provenance.get('git_sha') or 'unknown'} | "
503
+ f"campaign trials {campaign_trials_text(provenance)} | "
504
+ f"bar requests: {_fetch_summary(provenance.get('bar_requests'))}"
505
+ )
506
+
507
+ lines.extend(_verdict_step_lines(result))
508
+ lines.extend(_verdict_scan_lines(result))
509
+ lines.extend(_verdict_alpha_lines(result))
510
+ lines.extend(_verdict_portfolio_lines(result))
511
+ lines.extend(_verdict_information_lines(result))
512
+ lines.extend(_verdict_banner_lines(result))
513
+ return "\n".join(lines)
514
+
515
+
516
+ def campaign_trials_text(provenance: Dict[str, Any]) -> str:
517
+ """A verdict's campaign trial count as printed, on every surface that prints it.
518
+
519
+ A run that was not recorded reports the family without itself, and says so; a count
520
+ the store could not supply reads as unavailable rather than as zero.
521
+ """
522
+ if "n_trials" not in provenance:
523
+ return "?"
524
+ count = provenance["n_trials"]
525
+ text = "unavailable" if count is None else str(count)
526
+ return text + (" (this run not counted)" if provenance.get("this_run_counted") is False else "")
527
+
528
+
529
+ def _short_date(value: Optional[str]) -> str:
530
+ return (value or "?")[:10]
531
+
532
+
533
+ def _symbol_summary(symbols, limit: int = 8) -> str:
534
+ listed = list(symbols)[:limit]
535
+ suffix = f", +{len(symbols) - limit} more" if len(symbols) > limit else ""
536
+ return ", ".join(str(s) for s in listed) + suffix if listed else "none"
537
+
538
+
539
+ def _cost_summary(cost: Optional[Dict[str, Any]]) -> str:
540
+ if not cost:
541
+ return "unknown"
542
+ if cost.get("gross"):
543
+ return "GROSS (no transaction cost charged)"
544
+ return (
545
+ f"{cost.get('commission_bps')}bps commission, impact η={cost.get('impact_eta')}, "
546
+ f"borrow {cost.get('borrow_bps')}bps"
547
+ )
548
+
549
+
550
+ def format_offline_scan_notice(as_of) -> str:
551
+ """Say plainly that an offline scan saw only what the cache already holds.
552
+
553
+ A scan picks a universe at a clock. Offline it reads local Parquet and nothing
554
+ else, so the selection is exactly as current as the cache and no more - and
555
+ nothing errors when coverage ends before that clock: the newest cached bar simply
556
+ becomes "the latest", and a universe chosen from stale bars is indistinguishable
557
+ from one chosen from fresh ones. That is precisely the case that has to announce
558
+ itself.
559
+ """
560
+ return (
561
+ f"OFFLINE: universe resolved at {as_of.isoformat()} from cached bars only - "
562
+ "as current as the cache and no more. `cache status` shows the coverage."
563
+ )
564
+
565
+
566
+ def _fetch_summary(stats: Optional[Dict[str, Any]]) -> str:
567
+ """How much of the "one shared fetch" claim actually held, as measured.
568
+
569
+ This counts *in-run sharing*, not provider access. ``fetches`` is the number of
570
+ requests that missed this run's own memo and reached the data client underneath -
571
+ and on an ``--offline`` run that client is the local bar cache, so the old wording
572
+ ("hit the provider") reported a network round trip on a run that made none. A
573
+ provenance line that overstates where data came from is worse than no line, since
574
+ provenance is the one thing a reader cannot check for themselves.
575
+ """
576
+ if not stats:
577
+ return "not measured"
578
+ fetches, requests = stats.get("fetches", "?"), stats.get("requests", "?")
579
+ return f"{fetches} of {requests} reached the data client, the rest shared within this run"
580
+
581
+
582
+ def _verdict_step_lines(result: Dict[str, Any]) -> list:
583
+ """One line per step that did not simply succeed - silence means everything ran."""
584
+ steps = result.get("steps") or {}
585
+ notable = [(name, s) for name, s in steps.items() if s.get("status") != "ok"]
586
+ if not notable:
587
+ return []
588
+ lines = ["", " Steps:"]
589
+ for name, state in notable:
590
+ detail = state.get("error") or state.get("reason") or ""
591
+ marker = "(!)" if state.get("status") == "failed" else " -"
592
+ lines.append(f" {marker} {name}: {state.get('status')}{f' — {detail}' if detail else ''}")
593
+ for miss in result.get("cache_miss") or []:
594
+ lines.append(f" Fix the cache first: {miss}")
595
+ return lines
596
+
597
+
598
+ def _verdict_scan_lines(result: Dict[str, Any]) -> list:
599
+ scan = result.get("scan")
600
+ dropped = result.get("dropped_symbols") or []
601
+ tail = (
602
+ [f" (!) dropped for having no bars: {', '.join(dropped)} — the book was judged without them"]
603
+ if dropped
604
+ else []
605
+ )
606
+ if not scan:
607
+ return tail
608
+ lines = [
609
+ "",
610
+ f" Scan ({scan.get('scanner')}): {scan.get('flagged_count', 0)} of "
611
+ f"{len(scan.get('candidates') or [])} candidates flagged",
612
+ ]
613
+ if scan.get("fell_back_to_candidates"):
614
+ lines.append(" (i) nothing flagged — the full candidate list was analyzed instead")
615
+ return lines + tail
616
+
617
+
618
+ def alphas_heading(result: Dict[str, Any]) -> str:
619
+ """The one-line statement of what the alphas are: how scaled, on which IC, how many
620
+ names, and whether the universe is too thin to trust. One definition for the terminal
621
+ and the HTML report, so a caveat printed in one cannot be missing from the other."""
622
+ combination = result.get("combination")
623
+ if combination:
624
+ weights = combination.get("signal_weights") or {}
625
+ return f"Alphas (combined, measured IC {combination.get('combined_ic', 0.0):+.4f}): " + ", ".join(
626
+ f"{k} {v:+.2f}" for k, v in weights.items()
627
+ )
628
+ alphas = result.get("alphas") or {}
629
+ flag = " (low confidence - thin universe, demean-only)" if alphas.get("low_confidence") else ""
630
+ return (
631
+ f"Alphas ({alphas.get('scaling', '?')} scaling, assumed IC "
632
+ f"{alphas.get('ic', 0.0):+.4f}): {alphas.get('universe_size', 0)} names{flag}"
633
+ )
634
+
635
+
636
+ def _more_line(shown: int, total: int) -> list:
637
+ """A truncated list says how many names it left out; silence reads as 'all of them'."""
638
+ return [f" ... +{total - shown} more (not shown)"] if total > shown else []
639
+
640
+
641
+ def _verdict_alpha_lines(result: Dict[str, Any], top: int = 5) -> list:
642
+ combination = result.get("combination")
643
+ alphas = result.get("alphas")
644
+ source = combination or alphas
645
+ if not source:
646
+ return []
647
+ lines = ["", " " + alphas_heading(result)]
648
+ full = source.get("alphas") or []
649
+ for row in full[:top]:
650
+ lines.append(
651
+ f" {row.get('symbol', '?'):<8} alpha {small_aware(float(row.get('alpha', 0.0)), '+.4f')}"
652
+ )
653
+ lines.extend(_more_line(min(top, len(full)), len(full)))
654
+ return lines
655
+
656
+
657
+ def _verdict_portfolio_lines(result: Dict[str, Any], top: int = 5) -> list:
658
+ pf = result.get("portfolio")
659
+ if not pf:
660
+ return []
661
+ if not pf.get("feasible"):
662
+ return [
663
+ "",
664
+ f" Portfolio: NOT FEASIBLE — {pf.get('binding_constraint') or pf.get('note') or 'unknown'}",
665
+ ]
666
+ d = pf.get("diagnostics") or {}
667
+ lines = [
668
+ "",
669
+ f" Portfolio (proposal, not an order): {len(pf.get('weights') or {})} names, "
670
+ f"TE {float(d.get('predicted_tracking_error', 0.0)):.2%} (target {pf.get('target_te', 0):.2%})",
671
+ f" expected active return {float(d.get('expected_active_return', 0.0)):+.2%} gross"
672
+ f" / {float(d.get('expected_active_return_net', d.get('expected_active_return', 0.0))):+.2%} net",
673
+ f" predicted IR {float(d.get('predicted_ir', 0.0)):+.2f} | "
674
+ f"transfer coefficient {float(d.get('transfer_coefficient', 0.0)):+.2f}",
675
+ ]
676
+ held = list((pf.get("weights") or {}).items())
677
+ for symbol, weight in held[:top]:
678
+ lines.append(f" {symbol:<8} {float(weight):>7.2%}")
679
+ lines.extend(_more_line(min(top, len(held)), len(held)))
680
+ exposures = pf.get("exposures")
681
+ if exposures:
682
+ lines.append(
683
+ " exposures: " + ", ".join(f"{k} {float(v):+.2f}" for k, v in sorted(exposures.items()))
684
+ )
685
+ return lines
686
+
687
+
688
+ def _verdict_information_lines(result: Dict[str, Any]) -> list:
689
+ inf = result.get("information")
690
+ if not inf or not inf.get("periods"):
691
+ return []
692
+ flag = " (!) low sample" if inf.get("low_sample") else ""
693
+ return [
694
+ "",
695
+ f" Information ({inf.get('periods')} rebalances, horizon {inf.get('horizon_bars')} bars):{flag}",
696
+ f" IC {float(inf.get('mean_ic', 0.0)):+.4f} t-stat {float(inf.get('ic_tstat', 0.0)):+.2f} "
697
+ f"rank-IC {float(inf.get('rank_ic', 0.0)):+.4f}",
698
+ f" breadth {float(inf.get('breadth_effective', 0.0)):.0f} effective "
699
+ f"(ρ̄ {float(inf.get('rho_bar', 0.0)):.2f}, {inf.get('n_names', 0)} names)",
700
+ f" IR predicted {float(inf.get('predicted_ir', 0.0)):+.2f} vs realized "
701
+ f"{float(inf.get('realized_ir', 0.0)):+.2f} ± {float(inf.get('ir_standard_error', 0.0)):.2f}",
702
+ f" P(any |t|>2 across {inf.get('n_trials', 1)} campaign trials) = "
703
+ f"{float(inf.get('multiple_testing_inflation', 0.0)):.2f}",
704
+ ]
705
+
706
+
707
+ def _verdict_banner_lines(result: Dict[str, Any]) -> list:
708
+ """The conclusion, with every gate that produced it shown underneath.
709
+
710
+ Checks are listed whatever the verdict says, passes included: a verdict whose
711
+ supporting numbers are only visible when it is bad teaches the reader to trust
712
+ the good ones unexamined.
713
+ """
714
+ verdict = result.get("verdict") or {}
715
+ lines = ["", f" VERDICT: {verdict.get('summary', 'unknown')}"]
716
+ for name, check in sorted((verdict.get("checks") or {}).items()):
717
+ passed = bool(check.get("passed"))
718
+ value, threshold = check.get("value"), check.get("threshold")
719
+ detail = f"{_num(value)} vs {_num(threshold)}"
720
+ # Every note states the *failing* condition ("...is indistinguishable from
721
+ # zero"), so printing it beside a PASS produced a line that contradicted its
722
+ # own verdict. On a pass the value and its threshold already say everything.
723
+ note = f" — {check['note']}" if not passed and check.get("note") else ""
724
+ lines.append(f" [{'PASS' if passed else 'FAIL'}] {name}: {detail}{note}")
725
+ if verdict.get("verdict") == "incomplete":
726
+ lines.append(" No verdict is offered for a partial run — do not act on the sections above.")
727
+ if result.get("journaled") is False and result.get("not_journaled_reason"):
728
+ lines.append(f" Not recorded as a trial: {result['not_journaled_reason']}.")
729
+ if verdict.get("verdict") == "infeasible":
730
+ lines.append(
731
+ " The constraints, not the data, are what failed: change the universe or the caps "
732
+ "(--max-weight, --max-names) yourself - nothing was relaxed for you."
733
+ )
734
+ lines.append("")
735
+ return lines
736
+
737
+
738
+ def _num(value: Any) -> str:
739
+ """Format a gate's value/threshold without pretending a bool or a None is a float."""
740
+ if isinstance(value, bool) or value is None:
741
+ return str(value)
742
+ if isinstance(value, (int, float)):
743
+ return f"{value:.4g}"
744
+ return str(value)
745
+
746
+
747
+ # --------------------------------------------------------------------------- #
748
+ # Trial-store browsing
749
+ # --------------------------------------------------------------------------- #
750
+ #: Rendered wherever a stored field is absent. A trial recorded before a field
751
+ #: existed, or of a kind that never produces one, did not score zero and did not
752
+ #: fail — it has nothing recorded, and the two must never look alike.
753
+ NOT_RECORDED = "—"
754
+
755
+
756
+ def _is_probability(key: str) -> bool:
757
+ from quantred.optimization.walk_forward import PROBABILITY_METRICS
758
+
759
+ return key in PROBABILITY_METRICS
760
+
761
+
762
+ def _prob_cell(value: Any, key: str = "deflated_sharpe") -> str:
763
+ """A probability cell through the shared unit table, so the trial browser says what the
764
+ walk-forward and its chart say about the same trial — never ``0.000`` for a small one."""
765
+ if value is None:
766
+ return NOT_RECORDED
767
+ from quantred.optimization.walk_forward import format_gate_value
768
+
769
+ return format_gate_value(key, value)
770
+
771
+
772
+ def _local_ts(value: Any) -> str:
773
+ """A recorded UTC instant as New York wall time, the zone the date filters read.
774
+
775
+ Shown as the stored UTC text, a trial at 22:00 New York on the 6th read as the 7th
776
+ and looked like a row ``--until <the 6th>`` should not have returned.
777
+ """
778
+ if not value:
779
+ return NOT_RECORDED
780
+ from quantred.utils.timeutils import NEW_YORK
781
+
782
+ try:
783
+ parsed = _parse_iso(str(value))
784
+ except ValueError:
785
+ return str(value)[:19]
786
+ if parsed.tzinfo is None:
787
+ parsed = parsed.replace(tzinfo=timezone.utc)
788
+ return parsed.astimezone(NEW_YORK).strftime("%Y-%m-%d %H:%M:%S %Z")
789
+
790
+
791
+ def _cell(value: Any, spec: str = "{:.3f}") -> str:
792
+ """A metric cell, or :data:`NOT_RECORDED` when there is genuinely nothing stored."""
793
+ if value is None:
794
+ return NOT_RECORDED
795
+ if isinstance(value, bool):
796
+ return "yes" if value else "no"
797
+ try:
798
+ return spec.format(float(value))
799
+ except (TypeError, ValueError):
800
+ return str(value)
801
+
802
+
803
+ def _plural(count: int, noun: str) -> str:
804
+ """``1 name`` / ``2 names`` — a detail view is read closely enough for this to
805
+ grate."""
806
+ return f"{count} {noun}" if count == 1 else f"{count} {noun}s"
807
+
808
+
809
+ def _basis_cell(row: Dict[str, Any]) -> str:
810
+ """What a row's Sharpe measures, in the one short cell a table has room for."""
811
+ from quantred.store.trials import SHARPE_BASIS_SHORT
812
+
813
+ return SHARPE_BASIS_SHORT.get(row.get("kind") or "", NOT_RECORDED)
814
+
815
+
816
+ def format_trials_table(
817
+ rows: List[Dict[str, Any]],
818
+ *,
819
+ total: Optional[int] = None,
820
+ in_sample_excluded: int = 0,
821
+ undated_excluded: int = 0,
822
+ ) -> str:
823
+ """The trial listing: one row per trial, absences rendered as absences.
824
+
825
+ Always states how many rows matched versus how many are shown — a listing that
826
+ silently truncates reads as "this is everything" when it is not. Every row says
827
+ what its Sharpe *is* (BASIS): the same number is an out-of-sample aggregate on a
828
+ walk-forward and a search's winning candidate on an optimize row, and a table that
829
+ prints both under one heading invites ranking them together.
830
+ """
831
+ notices = []
832
+ if in_sample_excluded:
833
+ notices.append(
834
+ f"{in_sample_excluded} in-sample row(s) (optimize/backtest/alpha) left out of this "
835
+ "ranking — none was validated out of sample. Pass --include-in-sample to see them."
836
+ )
837
+ if undated_excluded:
838
+ notices.append(
839
+ f"{undated_excluded} row(s) with no recorded timestamp are not shown: a date bound "
840
+ "cannot place them, and no date is guessed for them. Drop --since/--until to see them."
841
+ )
842
+ if not rows:
843
+ return "\n".join(["No trials matched.", *notices])
844
+ lines = [
845
+ f"{'ID':14}{'KIND':12}{'STRATEGY':16}{'SHARPE':>9} {'BASIS':<11}{'DSR':>7}{'PROMO':>7}{'ACCT':>6} TS",
846
+ ]
847
+ for r in rows:
848
+ promo = NOT_RECORDED if r.get("promotable") is None else ("yes" if r["promotable"] else "no")
849
+ lines.append(
850
+ f"{str(r.get('id', ''))[:14]:14}{str(r.get('kind', ''))[:12]:12}"
851
+ f"{str(r.get('strategy') or '')[:16]:16}"
852
+ f"{_cell(r.get('oos_sharpe')):>9} {_basis_cell(r):<11}{_prob_cell(r.get('deflated_sharpe')):>7}"
853
+ f"{promo:>7}{r.get('accounting', ''):>6} {_local_ts(r.get('ts'))}"
854
+ )
855
+ if total is not None and total > len(rows):
856
+ lines.append(f"\nShowing {len(rows)} of {total} matching trials (use --limit/--offset for more).")
857
+ elif total is not None:
858
+ lines.append(f"\n{total} matching trial(s).")
859
+ if any(r.get("kind") in ("optimize", "backtest") for r in rows):
860
+ lines.append(
861
+ "IN-SAMPLE rows: the Sharpe is a search's winning candidate (optimize) or one pass "
862
+ "over the whole window (backtest) — not out-of-sample evidence."
863
+ )
864
+ lines.extend(notices)
865
+ return "\n".join(lines)
866
+
867
+
868
+ def format_exit_concentration(report: Dict[str, Any]) -> str:
869
+ """The "Where the P&L came from" block, over a trade-analytics result.
870
+
871
+ Returns ``""`` when there is nothing to say — no table, no exit-reason column, or
872
+ a split that carries no P&L either way. A run that produced no trades gets no
873
+ block rather than a table of zeros.
874
+ """
875
+ section = report.get("exit_reasons") or {}
876
+ rows = section.get("rows") or []
877
+ if not section.get("available") or not rows:
878
+ return ""
879
+ if not any(r.get("net_pnl") for r in rows):
880
+ return ""
881
+
882
+ lines = ["", "=== Where the P&L came from ==="]
883
+ if report.get("status") == "truncated":
884
+ # Partial totals are printed only under a label saying so. The alternative is
885
+ # a table of sums that reads exactly like a complete one.
886
+ lines.append(f" SHOWN ROWS ONLY — {report.get('reason', '')}")
887
+ lines.append(f" {'exit':16}{'trades':>8}{'share':>8}{'net P&L':>14}{'win rate':>10}")
888
+ for row in rows:
889
+ share = row.get("share_of_trades")
890
+ win_rate = row.get("win_rate")
891
+ net = row.get("net_pnl")
892
+ share_cell = NOT_RECORDED if share is None else format(share, ".1%")
893
+ net_cell = _money(net)
894
+ rate_cell = NOT_RECORDED if win_rate is None else format(win_rate, ".1%")
895
+ lines.append(
896
+ f" {str(row['exit_reason']):16}{row['trades']:>8}{share_cell:>8}{net_cell:>14}{rate_cell:>10}"
897
+ )
898
+ if row.get("unmeasured"):
899
+ # Otherwise the trades column silently fails to sum to the total while the
900
+ # share column sums to 100%, and nothing on screen says why.
901
+ lines.append(f" {'':32}(+{row['unmeasured']} with no P&L recorded, counted in the share)")
902
+ concentration = section.get("concentration") or {}
903
+ share_of_gain = concentration.get("share_of_gain")
904
+ if report.get("status") == "truncated":
905
+ # The numbers above are labelled as partial and can be read as such. This line
906
+ # is not a number, it is a claim about the run — and a prefix of the trades
907
+ # cannot support it. A partial gets no verdict.
908
+ lines.append(
909
+ " No concentration verdict from a partial table: which exit carried the gain "
910
+ "is\n a statement about all of the trades, and these are some of them."
911
+ )
912
+ elif share_of_gain is not None and share_of_gain > 0.9:
913
+ lines.append(
914
+ f" Nearly all of the gain comes from {concentration['exit_reason']}. The result "
915
+ f"is a bet on that\n exit's fill assumption - stress it before believing the headline."
916
+ )
917
+ return "\n".join(lines)
918
+
919
+
920
+ def describe_trade_table(trades: Optional[Dict[str, Any]]) -> str:
921
+ """One phrase for what a stored trade table is, used everywhere one is described.
922
+
923
+ Four distinct states, and collapsing any two of them is a lie somebody will act
924
+ on: never recorded, recorded and complete, recorded and *capped* at the storage
925
+ ceiling, and recorded before the store kept the count — which is not evidence of
926
+ either completeness or truncation and must not be rendered as though it were.
927
+
928
+ Goes through ``trade_analytics.describe_source`` rather than reading the payload's
929
+ keys again, so what counts as "capped" has one definition and this cannot come to
930
+ disagree with the module that refuses to aggregate one.
931
+ """
932
+ from quantred.analytics.trade_analytics import describe_source
933
+
934
+ return describe_trade_table_source(describe_source(trades))
935
+
936
+
937
+ #: Names listed under a stored book before the rest are summarised. A verdict over a wide
938
+ #: universe proposes dozens of names; the full book is always in `--json`.
939
+ _BOOK_LINES = 25
940
+
941
+
942
+ def _book_lines(weights: Optional[Dict[str, Any]]) -> List[str]:
943
+ """The proposed book itself - symbols and weights, largest first.
944
+
945
+ `show` is where the verdict's next step sends a reader asking what the proposed book
946
+ *is*, and a count ("8 names") answers a different question. Truncated loudly, with the
947
+ remainder and where to get all of it, never quietly.
948
+ """
949
+ book = (weights or {}).get("weights") or {}
950
+ if not book:
951
+ return []
952
+ active = (weights or {}).get("active_weights") or {}
953
+ ranked = sorted(book.items(), key=lambda item: (-abs(float(item[1])), item[0]))
954
+ as_of = (weights or {}).get("as_of")
955
+ lines = ["", f" Proposed book{f' (as of {str(as_of)[:10]})' if as_of else ''}:"]
956
+ header = f" {'symbol':<10}{'weight':>9}" + (f"{'active':>9}" if active else "")
957
+ lines.append(header)
958
+ for symbol, weight in ranked[:_BOOK_LINES]:
959
+ row = f" {symbol:<10}{float(weight):>9.2%}"
960
+ if active:
961
+ row += f"{_cell(active.get(symbol), '{:+.2%}'):>9}"
962
+ lines.append(row)
963
+ if len(ranked) > _BOOK_LINES:
964
+ rest = len(ranked) - _BOOK_LINES
965
+ lines.append(f" … {rest} more name(s) not shown (`trials show --json` carries the whole book).")
966
+ lines.append(f" {'total':<10}{sum(float(w) for w in book.values()):>9.2%}")
967
+ return lines
968
+
969
+
970
+ def format_trial_detail(trial: Dict[str, Any]) -> str:
971
+ """Everything the store knows about one trial.
972
+
973
+ Companion records report their *presence and shape* rather than their contents:
974
+ a detail view exists to tell you what is recoverable, not to dump a return
975
+ series into a terminal.
976
+ """
977
+ lines = [
978
+ "",
979
+ f"=== Trial {trial.get('id')} ({trial.get('kind')}) ===",
980
+ f" recorded : {_local_ts(trial.get('ts'))}",
981
+ f" strategy : {trial.get('strategy') or NOT_RECORDED}",
982
+ f" window : {(trial.get('window_start') or NOT_RECORDED)[:19]} → "
983
+ f"{(trial.get('window_end') or NOT_RECORDED)[:19]}",
984
+ f" universe : {trial.get('universe_hash', NOT_RECORDED)[:16]} (hash)",
985
+ f" accounting v{trial.get('accounting')} | git {trial.get('git_sha') or NOT_RECORDED}",
986
+ "",
987
+ " Params (the run's identity, including the folded cost and vintage keys):",
988
+ ]
989
+ params = trial.get("params") or {}
990
+ for key in sorted(params):
991
+ lines.append(f" {key} = {params[key]}")
992
+ if not params:
993
+ lines.append(f" {NOT_RECORDED}")
994
+
995
+ lines.append("")
996
+ lines.append(" Headline metrics:")
997
+ from quantred.optimization.walk_forward import format_gate_value
998
+ from quantred.store.trials import IN_SAMPLE_KINDS, sharpe_basis
999
+
1000
+ # Named by what it is for this kind, not by the column it happens to be stored in:
1001
+ # `oos_sharpe` holds a full-window figure on a backtest and a search's winner on an
1002
+ # optimize row, and printing either as "OOS Sharpe" is the label doing the flattering.
1003
+ basis = sharpe_basis(trial.get("kind"))
1004
+ sharpe_label = "Sharpe"
1005
+ # (label, stored column, the name the shared unit table knows that quantity by)
1006
+ for label, key, unit_name in (
1007
+ (sharpe_label, "oos_sharpe", None),
1008
+ ("Deflated Sharpe", "deflated_sharpe", "deflated_sharpe"),
1009
+ ("Profit factor", "oos_profit_factor", None),
1010
+ ("Max drawdown", "oos_max_dd", "max_drawdown"),
1011
+ ("Efficiency", "efficiency", None),
1012
+ ("Trades", "oos_trades", "total_trades"),
1013
+ ):
1014
+ raw = trial.get(key)
1015
+ if unit_name is None:
1016
+ value = _cell(raw)
1017
+ else:
1018
+ value = NOT_RECORDED if raw is None else format_gate_value(unit_name, raw)
1019
+ if key == "oos_sharpe" and basis and raw is not None:
1020
+ value = f"{value} [{basis}]"
1021
+ lines.append(f" {label:<18}{value}")
1022
+ if trial.get("kind") in IN_SAMPLE_KINDS:
1023
+ lines.append(" (in-sample: describes the data it was fitted or run on, not out-of-sample skill)")
1024
+ promo = trial.get("promotable")
1025
+ lines.append(f" {'Promotable':<18}{NOT_RECORDED if promo is None else ('yes' if promo else 'no')}")
1026
+
1027
+ lines.append("")
1028
+ lines.append(" Stored alongside this trial:")
1029
+ returns = trial.get("returns")
1030
+ lines.append(
1031
+ f" return series : {NOT_RECORDED} (not recorded)"
1032
+ if not returns
1033
+ else f" return series : {returns['periods']} periods, "
1034
+ f"{str(returns['start'])[:10]} → {str(returns['end'])[:10]}"
1035
+ )
1036
+ weights = trial.get("weights")
1037
+ lines.append(
1038
+ f" proposed book : {NOT_RECORDED} (not recorded)"
1039
+ if not weights
1040
+ else f" proposed book : {_plural(len(weights.get('weights') or {}), 'name')}"
1041
+ + (", with factor exposures" if weights.get("exposures") else "")
1042
+ + (", with active weights" if weights.get("active_weights") else "")
1043
+ )
1044
+ lines.append(f" trade table : {describe_trade_table(trial.get('trades'))}")
1045
+ lines.extend(_book_lines(weights))
1046
+
1047
+ reused = trial.get("reused_by") or []
1048
+ lines.append("")
1049
+ if reused:
1050
+ lines.append(f" Reused by {len(reused)} later trial(s) with the same identity:")
1051
+ for row in reused:
1052
+ lines.append(f" {row['id']} {_local_ts(row.get('ts'))} ({row.get('kind')})")
1053
+ else:
1054
+ lines.append(" Not reused by any later trial.")
1055
+ lines.append("")
1056
+ return "\n".join(lines)
1057
+
1058
+
1059
+ def format_trial_trades(trades: Optional[Dict[str, Any]], limit: int = 25) -> str:
1060
+ """The stored trade table, truncated loudly rather than quietly.
1061
+
1062
+ Two different truncations meet here and are named separately: this view showing
1063
+ the first ``limit`` of what is stored, and the *store* holding only the first N of
1064
+ what the run produced. The first is undone by a flag; the second is not undoable
1065
+ at all, and reading one as the other is how a partial table gets totalled.
1066
+ """
1067
+ if not trades:
1068
+ return f" trade table: {NOT_RECORDED} (not recorded)"
1069
+ columns = trades.get("columns") or []
1070
+ rows = trades.get("rows") or []
1071
+ lines = ["", f" Trades — {describe_trade_table(trades)}:"]
1072
+ lines.append(" " + " ".join(f"{str(c)[:12]:>12}" for c in columns))
1073
+ for row in rows[:limit]:
1074
+ lines.append(" " + " ".join(f"{str(v)[:12]:>12}" for v in row))
1075
+ if len(rows) > limit:
1076
+ lines.append(f" … {len(rows) - limit} more stored trade(s) not shown (--trades-limit to raise).")
1077
+ return "\n".join(lines)
1078
+
1079
+
1080
+ def format_trial_analysis(report: Dict[str, Any]) -> str:
1081
+ """The trade-table report for one recorded trial. Grades nothing."""
1082
+ lines = ["", f"=== Trades of {report.get('trial_id')} ({report.get('kind')}) ==="]
1083
+ lines.append(
1084
+ f" strategy {report.get('strategy') or NOT_RECORDED} | accounting "
1085
+ f"v{report.get('accounting')} | recorded {_local_ts(report.get('recorded_at'))}"
1086
+ )
1087
+ if report.get("contaminated_at"):
1088
+ lines.append(
1089
+ f" QUARANTINED {_local_ts(report['contaminated_at'])} — "
1090
+ f"{report.get('contamination_reason') or 'no reason recorded'}"
1091
+ )
1092
+ source = report.get("source") or {}
1093
+ lines.append(f" table : {describe_trade_table_source(source)}")
1094
+
1095
+ if report.get("status") == "unavailable":
1096
+ # Only here. A truncated report *does* have totals — partial ones, labelled by
1097
+ # the line below — and printing "No totals:" directly above a table of them
1098
+ # contradicts both the table and its own reason string.
1099
+ lines.append("")
1100
+ lines.append(f" No totals: {report.get('reason', '')}")
1101
+ return "\n".join(lines + [""])
1102
+ if report.get("status") == "truncated":
1103
+ lines.append("")
1104
+ lines.append(f" SHOWN ROWS ONLY — {report.get('reason', '')}")
1105
+
1106
+ overall = report.get("overall") or {}
1107
+ if overall.get("available") is False:
1108
+ lines.append(f" overall : {overall.get('note')}")
1109
+ else:
1110
+ lines.append("")
1111
+ lines.append(" Overall:")
1112
+ lines.append(f" {'Trades':<16}{overall.get('trades'):,}")
1113
+ lines.append(f" {'Wins / losses':<16}{overall.get('wins')} / {overall.get('losses')}")
1114
+ lines.append(f" {'Win rate':<16}{_cell(overall.get('win_rate'), '{:.1%}')}")
1115
+ # Money goes through one formatter so the sign sits outside the symbol
1116
+ # everywhere in the report, rather than in whichever block was written first.
1117
+ for label, key in (
1118
+ ("Net P&L", "net_pnl"),
1119
+ ("Average win", "avg_win"),
1120
+ ("Average loss", "avg_loss"),
1121
+ ("Expectancy", "expectancy"),
1122
+ ):
1123
+ lines.append(f" {label:<16}{_money(overall.get(key))}")
1124
+ lines.append(f" {'Profit factor':<16}{_cell(overall.get('profit_factor'), '{:.2f}')}")
1125
+ if overall.get("unmeasured"):
1126
+ lines.append(f" {'Unmeasured':<16}{overall['unmeasured']} row(s) carried no P&L")
1127
+
1128
+ exits = report.get("exit_reasons") or {}
1129
+ if exits.get("available"):
1130
+ lines.append("")
1131
+ lines.append(" By exit reason:")
1132
+ lines.append(
1133
+ f" {'exit':16}{'trades':>8}{'share':>8}{'net P&L':>14}{'win rate':>10}"
1134
+ f"{'avg win':>12}{'avg loss':>12}"
1135
+ )
1136
+ for row in exits["rows"]:
1137
+ lines.append(
1138
+ f" {str(row['exit_reason']):16}{row['trades']:>8}"
1139
+ f"{_cell(row.get('share_of_trades'), '{:.1%}'):>8}"
1140
+ f"{_money(row.get('net_pnl')):>14}{_cell(row.get('win_rate'), '{:.1%}'):>10}"
1141
+ f"{_money(row.get('avg_win')):>12}{_money(row.get('avg_loss')):>12}"
1142
+ )
1143
+ if row.get("unmeasured"):
1144
+ lines.append(f" {'':24}(+{row['unmeasured']} with no P&L recorded, counted in the share)")
1145
+ elif exits:
1146
+ lines.append(f" by exit : {exits.get('note')}")
1147
+
1148
+ duration = report.get("duration") or {}
1149
+ lines.append("")
1150
+ if duration.get("available"):
1151
+ lines.append(
1152
+ f" Held : median {_cell(duration.get('median'), '{:.1f}')} days, "
1153
+ f"p25 {_cell(duration.get('p25'), '{:.1f}')}, p75 {_cell(duration.get('p75'), '{:.1f}')}, "
1154
+ f"max {_cell(duration.get('max'), '{:.1f}')} "
1155
+ f"({duration.get('n_measured')} measured, {duration.get('n_unmeasured')} not)"
1156
+ )
1157
+ else:
1158
+ lines.append(f" Held : {duration.get('note')}")
1159
+
1160
+ excursion = report.get("excursion") or {}
1161
+ if excursion.get("available"):
1162
+ lines.append(f" Excursion: {excursion.get('note')}")
1163
+ for name, label in (("mae_pct", "adverse"), ("mfe_pct", "favourable")):
1164
+ block = excursion.get(name) or {}
1165
+ if block.get("available") is False:
1166
+ lines.append(f" {label:<12}{block.get('note')}")
1167
+ continue
1168
+ lines.append(
1169
+ f" {label:<12}median {_cell(block.get('median'), '{:.2f}')}%, "
1170
+ f"p90 {_cell(block.get('p90'), '{:.2f}')}%, max {_cell(block.get('max'), '{:.2f}')}% "
1171
+ f"({block.get('n_measured')} measured, {block.get('n_unmeasured')} not)"
1172
+ )
1173
+ else:
1174
+ lines.append(f" Excursion: {excursion.get('note')}")
1175
+ lines.append("")
1176
+ return "\n".join(lines)
1177
+
1178
+
1179
+ def describe_trade_table_source(source: Dict[str, Any]) -> str:
1180
+ """The stored table described from a trade-analytics ``source`` block.
1181
+
1182
+ The one place any surface turns a table's completeness into words.
1183
+ """
1184
+ completeness = source.get("completeness")
1185
+ stored, total = source.get("rows_stored"), source.get("total_rows")
1186
+ if not source.get("recorded"):
1187
+ return f"{NOT_RECORDED} (not recorded — re-run with --record-trades, or record_trades=true over MCP)"
1188
+ if completeness == "capped":
1189
+ return (
1190
+ f"{stored:,} of {total:,} trades — TRUNCATED at the storage cap, so any "
1191
+ "total over these rows is short by the rest"
1192
+ )
1193
+ if completeness == "unknown":
1194
+ return f"{_plural(stored or 0, 'trade')} stored; whether that was all of them was not recorded"
1195
+ return _plural(stored or 0, "trade")
1196
+
1197
+
1198
+ def _money(value: Any) -> str:
1199
+ """Currency with the sign outside the symbol; ``$-780`` reads as a price."""
1200
+ if value is None:
1201
+ return NOT_RECORDED
1202
+ return f"{'-' if value < 0 else ''}${abs(value):,.0f}"
1203
+
1204
+
1205
+ #: The correlation at or above which two recorded results stop being independent
1206
+ #: evidence for each other. Deliberately high: the claim it licenses is that a campaign
1207
+ #: has one finding where it believed it had two, which is worth being sure of.
1208
+ NEAR_DUPLICATE = 0.80
1209
+
1210
+
1211
+ def _series_kind(kind: Optional[str]) -> str:
1212
+ """``walkforward: stitched out-of-sample folds``, or just the kind when unlisted."""
1213
+ from quantred.analytics.series_comparison import series_basis
1214
+
1215
+ basis = series_basis(kind)
1216
+ return f"{kind or NOT_RECORDED}: {basis}" if basis else str(kind or NOT_RECORDED)
1217
+
1218
+
1219
+ def format_series_comparison(report: Dict[str, Any]) -> str:
1220
+ """Pairwise correlation of recorded return series, refusals included.
1221
+
1222
+ The refusals are printed with the correlations rather than under them. A listing
1223
+ that shows three coefficients and quietly drops the fourth pair reads as a complete
1224
+ comparison of everything that was asked about.
1225
+ """
1226
+ lines = ["", "=== Return-series comparison ==="]
1227
+ for summary in report.get("series") or []:
1228
+ if summary.get("available"):
1229
+ lines.append(
1230
+ f" {str(summary['trial_id'])[:14]:14} {summary['periods']:>5} periods, "
1231
+ f"{str(summary['start'])[:10]} → {str(summary['end'])[:10]} "
1232
+ f"(v{summary.get('accounting')}, {summary.get('strategy') or NOT_RECORDED}, "
1233
+ f"{_series_kind(summary.get('kind'))})"
1234
+ )
1235
+ else:
1236
+ lines.append(f" {str(summary['trial_id'])[:14]:14} {NOT_RECORDED} {summary['reason']}")
1237
+
1238
+ for trial_id in report.get("unknown_trial_ids") or []:
1239
+ lines.append(f" {str(trial_id)[:14]:14} {NOT_RECORDED} no trial with this id is recorded")
1240
+
1241
+ lines.append("")
1242
+ for pair in report.get("pairs") or []:
1243
+ head = f" {str(pair['a'])[:12]:12} vs {str(pair['b'])[:12]:12}"
1244
+ span = (
1245
+ f"overlap {pair['overlap']} periods, {str(pair['start'])[:10]}..{str(pair['end'])[:10]}"
1246
+ if pair["overlap"]
1247
+ else "overlap 0 periods"
1248
+ )
1249
+ if pair["status"] != "compared":
1250
+ lines.append(f"{head} {span}\n REFUSED — {pair['reason']}")
1251
+ continue
1252
+ interval = pair.get("interval")
1253
+ band = (
1254
+ f" [{interval['low']:+.2f}, {interval['high']:+.2f}]"
1255
+ if interval
1256
+ else f" {pair.get('interval_note') or 'no interval available'}"
1257
+ )
1258
+ lines.append(f"{head} {span}\n correlation {pair['correlation']:+.2f}{band}")
1259
+ if pair.get("comparable") is False:
1260
+ lines.append(f" {pair['reason']}")
1261
+ if pair.get("unlike_basis"):
1262
+ lines.append(
1263
+ f" UNLIKE SERIES — {_series_kind(pair.get('kind_a'))} against "
1264
+ f"{_series_kind(pair.get('kind_b'))}: the overlap is arithmetic over two "
1265
+ "different things, not two estimates of one."
1266
+ )
1267
+
1268
+ if report.get("kinds_differ"):
1269
+ lines.append(
1270
+ " The trials compared are not all the same kind of result: a walk-forward or research "
1271
+ "series is stitched out-of-sample\n folds, a backtest or optimize series is one "
1272
+ "in-sample pass over its window."
1273
+ )
1274
+ lines.append("")
1275
+ lines.append(
1276
+ f" {report.get('n_compared', 0)} of {report.get('n_pairs', 0)} pair(s) compared; "
1277
+ f"{report.get('n_refused', 0)} refused. Minimum overlap {report.get('min_overlap')} periods."
1278
+ )
1279
+ highest = report.get("highest")
1280
+ if highest:
1281
+ lines.append(f" Highest: {highest['a']} vs {highest['b']} at {highest['correlation']:+.2f}.")
1282
+ # The number is always reported; the sentence about what it means is not. A
1283
+ # coefficient near zero says these results are independent, and printing "one
1284
+ # bet held twice" beside it would be a verdict the number contradicts.
1285
+ if highest["correlation"] >= NEAR_DUPLICATE:
1286
+ lines.append(
1287
+ " At that level they are one bet held twice, however differently they were\n"
1288
+ " parameterised — promoting both counts a single result as two."
1289
+ )
1290
+ lines.append("")
1291
+ return "\n".join(lines)
1292
+
1293
+
1294
+ def _recorded_cell(fact: Optional[Dict[str, Any]]) -> str:
1295
+ """One context fact, rendering *not recorded* distinctly from a recorded value.
1296
+
1297
+ The reason each fact carries ``recorded`` at all is that "this run used no scanner"
1298
+ and "nobody wrote down whether it did" are different facts. A renderer printing the
1299
+ value alone collapses them back into one blank — which is what this one did, while
1300
+ the docs showed an example of output it never produced.
1301
+ """
1302
+ if not fact or not fact.get("recorded"):
1303
+ return f"{NOT_RECORDED} not recorded"
1304
+ value = fact.get("value")
1305
+ if isinstance(value, dict):
1306
+ return ", ".join(f"{k}={v}" for k, v in sorted(value.items())) or "recorded, empty"
1307
+ if isinstance(value, float):
1308
+ return f"{value:,.2f}"
1309
+ return str(value)
1310
+
1311
+
1312
+ def _starting_book(source: Any) -> str:
1313
+ """How the starting book reads. A dry run cannot see the account, so a flat book is
1314
+ an assumption it made, not a fact it observed."""
1315
+ if source == "flat":
1316
+ return "assumed flat (a dry run cannot see the account)"
1317
+ return str(source)
1318
+
1319
+
1320
+ def format_dry_run(report: Dict[str, Any], book_lines: Optional[Sequence[str]] = None) -> str:
1321
+ """A dry run's decision path, in language that cannot be read as execution.
1322
+
1323
+ Every heading is conditional — *would* submit, *would* bind, *would* skip — and the
1324
+ banner leads, because the single worst outcome for this mode is a reader taking an
1325
+ intention for a fill. The capital carries its source: a reader judging whether these
1326
+ caps are the right ones needs to know whether the number came from the validated
1327
+ config or was typed at the prompt.
1328
+ """
1329
+ counts = report.get("counts") or {}
1330
+ lines = [
1331
+ "",
1332
+ f"=== {report.get('banner')} ===",
1333
+ "",
1334
+ f" {'capital':<20}${report.get('capital', 0):,.2f} ({report.get('capital_source')})",
1335
+ f" {'starting book':<20}{_starting_book(report.get('starting_positions'))}",
1336
+ f" {'universe':<20}{len(report.get('universe') or [])} symbol(s)",
1337
+ # Both numbers, always. "12 symbols, 12 evaluated" and "12 symbols, 7 evaluated"
1338
+ # look identical if only the first is printed, and the second is a different run.
1339
+ f" {'evaluated':<20}{report.get('n_evaluated', 0)}"
1340
+ + (
1341
+ f" ({report.get('n_unable_to_evaluate')} could not be evaluated)"
1342
+ if report.get("n_unable_to_evaluate")
1343
+ else ""
1344
+ ),
1345
+ "",
1346
+ ]
1347
+ if book_lines:
1348
+ # The caps these buckets were decided against; without them "WOULD BIND: 0" is
1349
+ # unreadable, since nothing says what there was to bind.
1350
+ lines.extend([" book checked against:", *book_lines, ""])
1351
+
1352
+ for bucket, heading in (
1353
+ ("would_submit", "WOULD SUBMIT — orders this contract would have sent"),
1354
+ ("would_bind", "WOULD BIND — a configured cap refusing an order"),
1355
+ ("would_skip", "WOULD SKIP — no order, for a reason other than a cap"),
1356
+ ("unable_to_evaluate", "UNABLE TO EVALUATE — no decision was possible"),
1357
+ ):
1358
+ rows = report.get(bucket) or []
1359
+ # Printed even when empty: "no cap bound" and "the caps were never reached" are
1360
+ # different findings, and an omitted section renders them identically.
1361
+ lines.append(f" {heading}: {counts.get(bucket, 0)}")
1362
+ for row in rows:
1363
+ plan = row.get("plan") or {}
1364
+ detail = (
1365
+ f"{plan.get('side')} {plan.get('qty'):g} @ {plan.get('reference_price'):,.2f}"
1366
+ f" (stop {plan.get('stop_loss'):,.2f} / target {plan.get('take_profit'):,.2f})"
1367
+ if plan
1368
+ else row.get("reason")
1369
+ )
1370
+ lines.append(f" {str(row.get('symbol')):<10}{detail}")
1371
+ lines.append("")
1372
+
1373
+ lines.append(f" not covered {report.get('not_covered')}")
1374
+ lines.append(
1375
+ " Nothing here was journaled: a dry run measures nothing, so recording it would\n"
1376
+ " spend the family's statistical budget on a rehearsal."
1377
+ )
1378
+ lines.append("")
1379
+ return "\n".join(lines)
1380
+
1381
+
1382
+ #: How many symbol names to print before summarising. A campaign universe runs to
1383
+ #: hundreds, and a wall of tickers is not more auditable than a count plus a sample —
1384
+ #: but a *silent* truncation would be, so the elision says how many it hid and the JSON
1385
+ #: keeps the full list either way.
1386
+ _UNIVERSE_SAMPLE = 12
1387
+
1388
+
1389
+ def _seed_cell(metadata: Dict[str, Any]) -> str:
1390
+ """The seed, however this record happens to spell it.
1391
+
1392
+ Four shapes exist in the wild and all four have to read correctly, because this
1393
+ block is embedded in saved config files and a config written last month is not going
1394
+ to be rewritten: the key **absent**, an explicit **null**, a bare **scalar** (what
1395
+ every config written before the field carried its own provenance holds), and the
1396
+ current ``{recorded, value, from}`` object.
1397
+
1398
+ A scalar is a *recorded* seed. Dropping it silently — which an `isinstance(dict)`
1399
+ check does — turns a value the record holds into no line at all, which is the same
1400
+ false absence this field was changed to fix, arriving from the other direction.
1401
+ """
1402
+ seed = metadata.get("seed")
1403
+ if isinstance(seed, dict):
1404
+ rendered = _recorded_cell(seed)
1405
+ if seed.get("recorded") and seed.get("from"):
1406
+ rendered += f" (from the {seed['from']})"
1407
+ clash = seed.get("disagrees_with")
1408
+ if clash:
1409
+ rendered += (
1410
+ f" — DISAGREES with {clash.get('value')} in the {clash.get('from')}; "
1411
+ "the recipe defines the run, but this record is inconsistent"
1412
+ )
1413
+ return rendered
1414
+ if seed is None:
1415
+ # Covers both the absent key and an explicit null: nobody wrote one down.
1416
+ return f"{NOT_RECORDED} not recorded"
1417
+ return f"{seed} (recorded before this block named its source)"
1418
+
1419
+
1420
+ def _universe_lines(universe: Dict[str, Any], readable: Optional[bool] = None) -> List[str]:
1421
+ """The names the evidence covers, and how many were considered to reach them.
1422
+
1423
+ Absent is not empty: a trial that recorded no universe says so, rather than
1424
+ rendering as a run over nothing.
1425
+
1426
+ And *unreadable* is not absent. When the journal line could not be read at all —
1427
+ an archived store queried without its journal, or a research-agent trial whose
1428
+ record this scanner does not match — nothing here knows whether a universe was
1429
+ recorded, and saying "not recorded for this trial" asserts more than the code can
1430
+ see. The recipe reason one line above already draws that distinction; this used to
1431
+ contradict it.
1432
+ """
1433
+ symbols = universe.get("symbols")
1434
+ candidates = universe.get("candidate_symbols")
1435
+ unknown = (
1436
+ f"{NOT_RECORDED} the journal line for this trial could not be read"
1437
+ if readable is False
1438
+ else f"{NOT_RECORDED} not recorded for this trial"
1439
+ )
1440
+ if symbols is None and candidates is None:
1441
+ return [f" {'universe':<16}{unknown}"]
1442
+
1443
+ lines = []
1444
+ if symbols is None:
1445
+ lines.append(f" {'universe':<16}{unknown}")
1446
+ else:
1447
+ shown = ", ".join(symbols[:_UNIVERSE_SAMPLE])
1448
+ hidden = len(symbols) - _UNIVERSE_SAMPLE
1449
+ if hidden > 0:
1450
+ shown += f", … (+{hidden} more)"
1451
+ lines.append(f" {'universe':<16}{len(symbols)} symbol(s): {shown}")
1452
+ if candidates is not None:
1453
+ # The pre-scan set. Its *size* is the interesting part — it says how much the
1454
+ # scanner rejected, which is the "how it was resolved" half of the question.
1455
+ lines.append(f" {'considered':<16}{len(candidates)} candidate(s) before the scan")
1456
+ return lines
1457
+
1458
+
1459
+ def format_holdout(holdout: Dict[str, Any]) -> List[str]:
1460
+ """A walk-forward's holdout as lines, in each of the states a journal can leave it.
1461
+
1462
+ The four states are not interchangeable and none is rendered as another: a holdout
1463
+ that was never asked for, one whose result is in the journal, one that was asked
1464
+ for and produced nothing, and one asked for on a line written before the journal
1465
+ kept the result - that last is *absent*, and says so rather than printing nothing.
1466
+ """
1467
+ from quantred.optimization.walk_forward import format_gate_value
1468
+
1469
+ state, days = holdout.get("state"), holdout.get("days")
1470
+ if state == "none_requested":
1471
+ return [" Holdout : none requested (--holdout-days 0)"]
1472
+ if state == "not_retained":
1473
+ return [
1474
+ f" Holdout : requested ({days} days) but its result was not retained — this trial "
1475
+ "was recorded before the journal kept holdout results"
1476
+ ]
1477
+ if state == "recorded":
1478
+ metrics = holdout.get("metrics")
1479
+ if not metrics:
1480
+ return [f" Holdout : requested ({days} days) but it produced no result"]
1481
+ cells = " ".join(
1482
+ f"{label} {format_gate_value(key, metrics.get(key))}"
1483
+ for label, key in (("Sharpe", "sharpe_ratio"), ("CAGR", "cagr"), ("trades", "total_trades"))
1484
+ if key in metrics
1485
+ )
1486
+ return [f" Holdout : {days} days, scored once — {cells}"]
1487
+ return [" Holdout : unknown — the journal line for this trial is not readable"]
1488
+
1489
+
1490
+ def _campaign_heading(material: Dict[str, Any]) -> str:
1491
+ """The block's title, by whether the trial was validated at all.
1492
+
1493
+ "What validated" over a backtest or an optimize row claims a validation that never
1494
+ happened: those kinds were not split into a part fitted and a part judged. The
1495
+ block is still worth printing - it is the run's recipe and record - but it is
1496
+ headed as what it is.
1497
+ """
1498
+ from quantred.store.trials import IN_SAMPLE_KINDS
1499
+
1500
+ ident = f"{material.get('trial_id')} ({material.get('kind')})"
1501
+ if material.get("kind") in IN_SAMPLE_KINDS:
1502
+ return f"=== What produced {ident} — NOT validated out of sample ==="
1503
+ return f"=== What validated {ident} ==="
1504
+
1505
+
1506
+ def format_campaign_material(material: Dict[str, Any]) -> str:
1507
+ """What validated a trial, with each section labelled by what kind of thing it is.
1508
+
1509
+ The labels are the point. A recipe survives an accounting bump and a measurement
1510
+ does not, and a reader who cannot tell them apart will carry a stale number forward
1511
+ beside a recipe that is still perfectly good.
1512
+ """
1513
+ if not material.get("available"):
1514
+ return f"\n=== Campaign ===\n {NOT_RECORDED} {material.get('reason')}\n"
1515
+
1516
+ recipe = material.get("recipe") or {}
1517
+ evidence = material.get("evidence") or {}
1518
+ metadata = material.get("metadata") or {}
1519
+ lines = [
1520
+ "",
1521
+ _campaign_heading(material),
1522
+ "",
1523
+ " RECIPE — how it was validated. Survives an accounting bump.",
1524
+ ]
1525
+ window = recipe.get("window") or {}
1526
+ lines.append(f" window {str(window.get('start'))[:10]} → {str(window.get('end'))[:10]}")
1527
+ if recipe.get("available"):
1528
+ for key, value in sorted((recipe.get("validation") or {}).items()):
1529
+ lines.append(f" {key:<16}{NOT_RECORDED if value is None else value}")
1530
+ for key, value in sorted((recipe.get("folded_into_identity") or {}).items()):
1531
+ lines.append(f" {key:<16}{value} (folded into its identity)")
1532
+ else:
1533
+ lines.append(f" {NOT_RECORDED} {recipe.get('reason')}")
1534
+
1535
+ # The universe is part of what validated this, and the block carried it in JSON while
1536
+ # this renderer printed none of it — the same "the payload has it, the text a reader
1537
+ # sees does not" defect already found once in this function. Rendered under the
1538
+ # recipe because that is what it is: which names the evidence covers, and how many
1539
+ # were considered to get there. Outside the `available` branch on purpose: a
1540
+ # backtest has no validation recipe and still has a universe.
1541
+ lines.extend(_universe_lines(recipe.get("universe") or {}, readable=metadata.get("journal_line_found")))
1542
+
1543
+ lines.append("")
1544
+ lines.append(" SET UP WITH — how the run was configured. Not part of its identity.")
1545
+ for fact, value in (recipe.get("context") or {}).items():
1546
+ lines.append(f" {fact:<16}{_recorded_cell(value)}")
1547
+
1548
+ lines.append("")
1549
+ lines.append(
1550
+ f" EVIDENCE — what was measured, under accounting v{evidence.get('accounting')}. Valid only there."
1551
+ )
1552
+ metrics = evidence.get("metrics")
1553
+ if metrics:
1554
+ # Through the shared unit table, not a flat float format. A trade count
1555
+ # rendered as `88.000` is the defect this project has now found in three
1556
+ # separate renderers, and a fourth private formatter here would be the fourth.
1557
+ # Without a benchmark these are stored as 0.0, which beside
1558
+ # `benchmark_available no` reads as a measured zero alpha. They were not measured.
1559
+ # The one reader of those flags (the HTML report uses it too), so a Treynor ratio
1560
+ # suppressed for a near-zero beta is not called "no benchmark" here.
1561
+ from quantred.analytics.performance import unmeasured_metrics
1562
+ from quantred.optimization.walk_forward import format_gate_value
1563
+
1564
+ unmeasured = unmeasured_metrics(metrics)
1565
+ for key, value in sorted(metrics.items()):
1566
+ gap = " " if len(key) >= 16 else "" # a long name must not run into its value
1567
+ shown = (
1568
+ f"not measured ({unmeasured[key]})" if key in unmeasured else format_gate_value(key, value)
1569
+ )
1570
+ lines.append(f" {key:<16}{gap}{shown}")
1571
+ if material.get("kind") == "research" and "sharpe_ratio" in metrics:
1572
+ lines.append(
1573
+ " (research round: sharpe_ratio here is the aggregate over all folds; the "
1574
+ "Sharpe in `trials list`/`show` is the median fold)"
1575
+ )
1576
+ else:
1577
+ lines.append(f" {NOT_RECORDED} no metrics recorded")
1578
+ if evidence.get("holdout") is not None:
1579
+ lines.extend(" " + line for line in format_holdout(evidence["holdout"]))
1580
+ promo = evidence.get("promotable")
1581
+ lines.append(f" {'promotable':<16}{NOT_RECORDED if promo is None else ('yes' if promo else 'no')}")
1582
+ lines.append(f" {'family n_trials':<16}{_cell(evidence.get('family_n_trials'), '{:.0f}')}")
1583
+ lines.append(f" {'trial ids':<16}{', '.join(evidence.get('trial_ids') or []) or NOT_RECORDED}")
1584
+ lines.append(f" {'probes':<16}{_recorded_cell(evidence.get('probes'))}")
1585
+ if evidence.get("staleness"):
1586
+ lines.append(f" (!) {evidence['staleness']}")
1587
+ if evidence.get("quarantined"):
1588
+ lines.append(f" (!) QUARANTINED — {evidence.get('quarantine_reason')}")
1589
+
1590
+ lines.append("")
1591
+ lines.append(" METADATA — about the record, not about the strategy.")
1592
+ lines.append(f" {'recorded':<16}{_local_ts(metadata.get('recorded_at'))}")
1593
+ lines.append(f" {'git':<16}{metadata.get('git_sha') or NOT_RECORDED}")
1594
+ # Named alongside the sha because both answer "what would it take to reproduce
1595
+ # this", and because an omitted line reads as nothing to say rather than as nothing
1596
+ # recorded. The source is printed with it: the seed's real home is the validation
1597
+ # recipe, and a reader checking provenance should know which record it came off.
1598
+ lines.append(f" {'seed':<16}{_seed_cell(metadata)}")
1599
+ lines.append(f" {'notes':<16}{_recorded_cell(metadata.get('notes'))}")
1600
+ for artifact in metadata.get("artifacts") or []:
1601
+ state = artifact["read_with"] if artifact["recorded"] else f"{NOT_RECORDED} not recorded"
1602
+ lines.append(f" {artifact['artifact']:<16}{state}")
1603
+ lines.append("")
1604
+ return "\n".join(lines)
1605
+
1606
+
1607
+ def format_leaderboard(board: Dict[str, Any]) -> str:
1608
+ """The leaderboard, and the multiple-testing context that makes it honest.
1609
+
1610
+ The family's trial count sits on every row and the caveat is always printed —
1611
+ a ranking without its selection context is the exact trap this project's
1612
+ evaluation machinery exists to close, and it would be worse coming from our own
1613
+ tooling, which lends it authority.
1614
+ """
1615
+ rows = board.get("rows") or []
1616
+ if not rows:
1617
+ return "No trials matched."
1618
+ ranked_by = "deflated Sharpe" if board.get("rank_by") == "dsr" else "RAW Sharpe"
1619
+ lines = [
1620
+ "",
1621
+ # Name the population, not just the sort key. "Top 3 by deflated Sharpe"
1622
+ # still reads as "the best of everything I have run" unless it says what it
1623
+ # ranked over.
1624
+ f" Top {len(rows)} by {ranked_by}"
1625
+ + ("" if board.get("in_sample_included") else " (validated runs only)")
1626
+ + ":",
1627
+ # KIND is not decoration. Without it a reader cannot tell a validated
1628
+ # walk-forward from a search's winning candidate, and those mean opposite
1629
+ # things about whether a number is evidence.
1630
+ f" {'#':<3}{'ID':14}{'KIND':12}{'STRATEGY':16}{'DSR':>8}{'SHARPE':>9} {'BASIS':<11}{'FAMILY n_trials':>16}",
1631
+ ]
1632
+ for i, r in enumerate(rows, 1):
1633
+ family = r.get("family_n_trials")
1634
+ lines.append(
1635
+ f" {i:<3}{str(r.get('id', ''))[:14]:14}{str(r.get('kind') or '')[:12]:12}"
1636
+ f"{str(r.get('strategy') or '')[:16]:16}"
1637
+ f"{_prob_cell(r.get('deflated_sharpe')):>8}{_cell(r.get('oos_sharpe')):>9} "
1638
+ f"{_basis_cell(r):<11}{(NOT_RECORDED if family is None else family):>16}"
1639
+ )
1640
+ lines.append("")
1641
+ if board.get("unranked_excluded"):
1642
+ lines.append(
1643
+ f" {board['unranked_excluded']} row(s) have no {ranked_by} recorded and cannot be "
1644
+ "ranked by it (a verdict records neither a Sharpe nor a DSR); they are not listed."
1645
+ )
1646
+ # Two different counts sit on a row and they are easy to read as one. The DSR was
1647
+ # fixed when the row was recorded, deflated by the configs tried inside that one run;
1648
+ # the family count is read now, across every run of the strategy over the universe.
1649
+ lines.append(
1650
+ " FAMILY n_trials = configs tried so far on this row's strategy over its universe "
1651
+ "(this accounting version), counted now. A row's DSR was deflated by the trials of "
1652
+ "its own run, so two rows' DSRs are not comparable when their runs searched different amounts."
1653
+ )
1654
+ if board.get("in_sample_included"):
1655
+ lines.append(
1656
+ " (!) IN-SAMPLE rows included. An 'optimize' row is the winner of a search — "
1657
+ "best-of-N by construction — so its rank measures selection, not skill; a "
1658
+ "'backtest' row is one pass over its window, never validated out of sample."
1659
+ )
1660
+ elif board.get("in_sample_excluded"):
1661
+ lines.append(
1662
+ f" {board['in_sample_excluded']} in-sample row(s) excluded (search candidates and "
1663
+ "plain backtests, not validated out of sample). Pass --include-in-sample to see them."
1664
+ )
1665
+ lines.append(f" {board.get('caveat', '')}")
1666
+ if board.get("max_family_n_trials", 0) >= 50:
1667
+ lines.append(
1668
+ f" This family has tried {board['max_family_n_trials']} configs. At that count "
1669
+ "the best raw Sharpe is largely selection — read the deflated column, not the rank."
1670
+ )
1671
+ lines.append("")
1672
+ return "\n".join(lines)