Skip to content

Commit 1655a44

Browse files
committed
docs(demo): expand storage_layer_demo with full backtest report + HTML dashboard
Two new sections in examples/storage_layer_demo/demo.py: - 6b. _print_backtest_full_report(): per-run breakdown (window / days / orders / trades / positions / final_value), end-of-backtest positions snapshot, first few trades, and a richer slice of per-run BacktestMetrics (cagr, annual_volatility, max_drawdown_absolute, gross_profit/loss, best_trade, max consecutive wins/losses) with safe n/a fallbacks. Built on top of the existing compact _print_backtest_report(). - 9. Storage layer -> HTML dashboard: wires the Tier-1 SQLite index, the Tier-2 LocalDirStore and the BacktestReport HTML dashboard end-to-end. rank_index() picks the top-N bundles from SQLite alone, store.open(handle) materialises just those via the BacktestStore protocol, BacktestReport(backtests=[...]).save() renders a self-contained interactive HTML dashboard. Demonstrates that the new storage layer plugs straight into the existing reporting stack with no glue code. README updated to describe both new sections.
1 parent b6ebab0 commit 1655a44

2 files changed

Lines changed: 176 additions & 4 deletions

File tree

examples/storage_layer_demo/README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,21 @@ The script will:
4545
3. Print the equivalent `iaf` CLI commands you could run by hand.
4646
4. Run `list_index` / `rank_index` / a raw SQL query and print
4747
the formatted tables.
48-
5. Open the top-ranked bundle and print its full backtest report
48+
5. Open the top-ranked bundle and print its compact backtest report
4949
(this is the only step that decodes per-run Parquet metric blobs).
50-
6. Walk the index in rank order and print a one-line summary per
50+
6. Render an _expanded_ report for the same winning bundle —
51+
per-run breakdown, end-of-backtest positions, the first few
52+
trades, and a richer slice of per-run risk / return metrics.
53+
7. Walk the index in rank order and print a one-line summary per
5154
bundle straight out of the SQLite index — no bundle is opened.
52-
7. Iterate every bundle in rank order and print a full per-bundle
55+
8. Iterate every bundle in rank order and print a full per-bundle
5356
report so you can scan _all_ backtests at a glance.
57+
9. Tie the storage layer end-to-end into the **HTML dashboard**:
58+
pick the top-N bundles via `rank_index` (Tier-1 SQLite only),
59+
load each one through `LocalDirStore.open(handle)` (the
60+
`BacktestStore` protocol), and feed the resulting list straight
61+
into `BacktestReport(...).save(...)`. Open the generated
62+
`dashboard.html` to see all selected backtests side-by-side.
5463

5564
## CLI cheatsheet
5665

examples/storage_layer_demo/demo.py

Lines changed: 164 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
rank_index,
3333
format_table,
3434
)
35+
from investing_algorithm_framework.services.backtest_store import (
36+
LocalDirStore,
37+
)
38+
from investing_algorithm_framework import BacktestReport
3539

3640

3741
# Directory-format fixture shipped with the test suite. We use it
@@ -109,6 +113,103 @@ def _row(label: str, value, fmt: str = "") -> None:
109113
)
110114

111115

116+
def _print_backtest_full_report(bt: Backtest) -> None:
117+
"""Render a detailed multi-section report for a single backtest.
118+
119+
Goes beyond :func:`_print_backtest_report` by walking the
120+
per-run breakdown, listing positions, and showing the first few
121+
trades/orders so the demo's "open the winner" step actually
122+
looks like a backtest report and not just a summary row.
123+
"""
124+
_print_backtest_report(bt)
125+
126+
runs = bt.backtest_runs or []
127+
if not runs:
128+
return
129+
130+
# ------------------- per-run breakdown -------------------
131+
print("\n --- per-run breakdown ---")
132+
header = (
133+
f" {'#':>2} {'window':<20} {'days':>5} {'orders':>7} "
134+
f"{'trades':>7} {'positions':>10} {'final_value':>14}"
135+
)
136+
print(header)
137+
print(" " + "-" * (len(header) - 2))
138+
for i, r in enumerate(runs, start=1):
139+
window = (
140+
r.backtest_date_range_name
141+
if r.backtest_date_range_name
142+
else f"{r.backtest_start_date.date()}..{r.backtest_end_date.date()}"
143+
)[:20]
144+
m = r.backtest_metrics
145+
final = (
146+
f"{float(m.final_value):.2f}"
147+
if m is not None and getattr(m, "final_value", None) is not None
148+
else "n/a"
149+
)
150+
print(
151+
f" {i:>2} {window:<20} {r.number_of_days:>5} "
152+
f"{r.number_of_orders:>7} {r.number_of_trades:>7} "
153+
f"{r.number_of_positions:>10} {final:>14}"
154+
)
155+
156+
# ------------------- positions snapshot -------------------
157+
last = runs[-1]
158+
positions = list(getattr(last, "positions", None) or [])
159+
if positions:
160+
print(f"\n --- positions (last run, {len(positions)}) ---")
161+
for p in positions[:10]:
162+
symbol = getattr(p, "symbol", None) or "?"
163+
amount = getattr(p, "amount", None)
164+
cost = getattr(p, "cost", None)
165+
print(
166+
f" {symbol:<14} amount={amount} cost={cost}"
167+
)
168+
if len(positions) > 10:
169+
print(f" ... and {len(positions) - 10} more")
170+
171+
# ------------------- trades preview -------------------
172+
trades = list(getattr(last, "trades", None) or [])
173+
if trades:
174+
print(f"\n --- trades (last run, {len(trades)} total, first 5) ---")
175+
for t in trades[:5]:
176+
print(
177+
f" {getattr(t, 'symbol', '?'):<10} "
178+
f"opened={getattr(t, 'opened_at', None)} "
179+
f"net_gain={getattr(t, 'net_gain', None)}"
180+
)
181+
182+
# ------------------- richer metrics from first run -------------------
183+
m = runs[0].backtest_metrics
184+
if m is None:
185+
return
186+
print("\n --- additional risk / return metrics (first run) ---")
187+
extras = [
188+
("cagr", "cagr", ".4f"),
189+
("annual_volatility", "annual_volatility", ".4f"),
190+
("max_drawdown_abs", "max_drawdown_absolute", ".2f"),
191+
("max_drawdown_dur", "max_drawdown_duration", ""),
192+
("gross_profit", "gross_profit", ".2f"),
193+
("gross_loss", "gross_loss", ".2f"),
194+
("best_trade", "best_trade", ".2f"),
195+
("max_consec_wins", "max_consecutive_wins", ""),
196+
("max_consec_losses", "max_consecutive_losses", ""),
197+
]
198+
for label, attr, fmt in extras:
199+
val = getattr(m, attr, None)
200+
if val is None:
201+
rendered = "n/a"
202+
elif fmt:
203+
try:
204+
rendered = format(float(val), fmt)
205+
except (TypeError, ValueError):
206+
rendered = str(val)
207+
else:
208+
rendered = str(val)
209+
print(f" {label:<22}: {rendered}")
210+
211+
212+
112213
def _seed_bundles(out_dir: Path, n: int = 6) -> None:
113214
"""Write ``n`` synthetic ``.iafbt`` bundles into *out_dir*."""
114215
if not TEMPLATE.is_dir():
@@ -240,6 +341,20 @@ def main() -> None:
240341
winner_bt = Backtest.open(str(winner_path))
241342
_print_backtest_report(winner_bt)
242343

344+
# ------------------------------------------------------------------
345+
# 6b. Full backtest report — per-run breakdown, positions, trades
346+
# ------------------------------------------------------------------
347+
_print_section(
348+
"6b. Full backtest report for the winner "
349+
"(runs / positions / trades)"
350+
)
351+
print(
352+
"Same bundle, expanded view: per-run breakdown, end-of-backtest "
353+
"positions, the first few trades, and a richer slice of "
354+
"per-run risk/return metrics.\n"
355+
)
356+
_print_backtest_full_report(winner_bt)
357+
243358
# ------------------------------------------------------------------
244359
# 7. Iterate the index and print a one-line report per backtest
245360
# ------------------------------------------------------------------
@@ -272,16 +387,64 @@ def main() -> None:
272387
print(f"\n--- [{i}] {r['algorithm_id']} ".ljust(72, "-"))
273388
_print_backtest_report(bt)
274389

390+
# ------------------------------------------------------------------
391+
# 9. Storage layer -> HTML dashboard
392+
# ------------------------------------------------------------------
393+
_print_section(
394+
"9. Storage layer -> HTML dashboard "
395+
"(rank in SQLite, load via store, render report)"
396+
)
397+
print(
398+
"Wire the Tier-1 SQLite index, the Tier-2 LocalDirStore and the\n"
399+
"BacktestReport HTML dashboard end-to-end:\n"
400+
" 1. rank_index() picks the top-N bundles from SQLite alone\n"
401+
" (no Parquet metric blob is decoded yet).\n"
402+
" 2. LocalDirStore.open(handle) materialises just those\n"
403+
" bundles through the BacktestStore protocol.\n"
404+
" 3. BacktestReport(backtests=[...]).save(path) renders the\n"
405+
" interactive HTML dashboard with every loaded bundle.\n"
406+
)
407+
store = LocalDirStore(work)
408+
print(f" store : {type(store).__name__}({work})")
409+
print(f" bundles in store: {len(store)}")
410+
411+
top = rank_index(str(work), by="sharpe_ratio", limit=3)
412+
print(f" top-3 by sharpe: {[r['algorithm_id'] for r in top]}")
413+
414+
loaded: list[Backtest] = []
415+
for r in top:
416+
# The Tier-1 row's bundle_path is relative to the store root,
417+
# which is exactly what LocalDirStore uses as a handle.
418+
handle = r["bundle_path"]
419+
bt = store.open(handle)
420+
loaded.append(bt)
421+
print(
422+
f" store.open({handle!r}) -> {bt.algorithm_id} "
423+
f"({len(bt.backtest_runs)} run(s))"
424+
)
425+
426+
report = BacktestReport(backtests=loaded)
427+
html_path = work / "dashboard.html"
428+
report.save(str(html_path))
429+
html_size_kb = os.path.getsize(html_path) / 1024.0
430+
print(
431+
f"\n wrote HTML dashboard -> {html_path}\n"
432+
f" size: {html_size_kb:.1f} KB "
433+
f"(self-contained: CSS + JS + data inlined)\n"
434+
f" open it with: open {html_path}"
435+
)
436+
275437
# ------------------------------------------------------------------
276438
# Wrap up
277439
# ------------------------------------------------------------------
278440
_print_section("Done")
279441
print(
280-
"Bundles + index left in:\n"
442+
"Bundles + index + dashboard left in:\n"
281443
f" {work}\n"
282444
"Try the CLI directly:\n"
283445
f" iaf list {work} --sort calmar_ratio --json\n"
284446
f" iaf rank {work} --by sharpe_ratio -n 3\n"
447+
f" open {work / 'dashboard.html'}\n"
285448
)
286449

287450

0 commit comments

Comments
 (0)