#!/usr/bin/env python
"""Render the trade ledger as a self-contained interactive HTML dashboard.
.venv/bin/python dashboard.py # -> dashboard.html
.venv/bin/python dashboard.py -o /tmp/out.html --open
.venv/bin/python dashboard.py --since 2026-08-01
Round trips come from analytics.py. The page embeds them as JSON and does its own
filtering/aggregation client-side, so the strategy, symbol and date-range filters
recompute every chart without regenerating the file. No external assets, no CDN:
the output is one portable HTML file.
"""
from __future__ import annotations
import argparse
import json
import webbrowser
from pathlib import Path
import analytics
# Reference thresholds drawn on the return-distribution chart. Read from config
# when it is importable so the annotations follow the live parameters.
try:
from config import config as _cfg
DEFAULT_MIN_PROFIT = _cfg.short_term.min_profit_pct
DEFAULT_STOP_LOSS = _cfg.short_term.stop_loss_pct
DEFAULT_MAX_DAILY_LOSS = _cfg.max_daily_loss
except Exception:
DEFAULT_MIN_PROFIT, DEFAULT_STOP_LOSS, DEFAULT_MAX_DAILY_LOSS = 1.5, 2.5, 150.0
HTML = r"""
Bot Trade History
Bot Trade History & Performance
Realized equity curve
Cumulative net P&L by exit time. Shaded band is drawdown from the running peak.
Daily realized P&L
Net per calendar day. The dashed line marks the $ daily-loss circuit breaker.
By strategy
Net P&L; label shows trade count and win rate.
By symbol
Net P&L; label shows trade count and win rate.
By entry signal
Which signal actually pays. Needs reason in the ledger.
By exit reason
How trades end. Needs reason in the ledger.
Hold time vs return
One dot per closed trade. Colour is the owning strategy.
Open lots
Unmatched buy lots at the end of the ledger — cost basis only, not live marks.
Trade history
Every closed round trip in range, newest first. Click a header to sort. SELL* = exit price estimated (fill missed while the bot was offline).
"""
def build(data: dict, min_profit: float, stop_loss: float, max_daily_loss: float) -> str:
"""Inject the analysis bundle into the page template."""
payload = {
"ledger": data["ledger"],
"generated": data["generated"],
"n_records": data["n_records"],
"unmatched_sell_qty": data["unmatched_sell_qty"],
"overall": data["overall"],
"trips": data["trips"],
"open_lots": data["open_lots"],
"min_profit_pct": min_profit,
"stop_loss_pct": stop_loss,
"max_daily_loss": max_daily_loss,
}
return HTML.replace("__DATA__", json.dumps(payload, default=str))
def main():
ap = argparse.ArgumentParser(description="Render trades.jsonl as an HTML dashboard")
ap.add_argument("--ledger", default=str(analytics.LEDGER))
ap.add_argument("-o", "--out", default="dashboard.html")
ap.add_argument("--since", help="only include trades closed on/after YYYY-MM-DD")
ap.add_argument("--until", help="only include trades closed on/before YYYY-MM-DD")
ap.add_argument("--min-profit", type=float, default=DEFAULT_MIN_PROFIT,
help="min-profit annotation on the distribution chart")
ap.add_argument("--stop-loss", type=float, default=DEFAULT_STOP_LOSS,
help="stop-loss annotation on the distribution chart")
ap.add_argument("--max-daily-loss", type=float, default=DEFAULT_MAX_DAILY_LOSS,
help="daily circuit-breaker reference line")
ap.add_argument("--open", action="store_true", help="open the result in a browser")
args = ap.parse_args()
path = Path(args.ledger)
if not path.exists():
raise SystemExit(f"ledger not found: {path}")
data = analytics.analyze(path, args.since, args.until)
out = Path(args.out)
out.write_text(build(data, args.min_profit, args.stop_loss, args.max_daily_loss))
print(f"wrote {out} ({data['overall']['n']} closed trips, "
f"net {data['overall']['pnl']:+.2f})")
if args.open:
webbrowser.open(out.resolve().as_uri())
if __name__ == "__main__":
main()