feat(monitor): dashboard PAPER del portafoglio attivo (TP01+XS01) + paper forward loop
src/live/dashboard.py: web UI stdlib (:8787) che mostra metriche (FULL/HOLD Sharpe, DD, CAGR), per-sleeve, posizioni correnti, equity (backtest + paper forward), ultimo dato. Solo MONITOR, esecuzione REALE disabilitata. scripts/live/paper_portfolio.py: forward-only del portafoglio (StrategyPortfolio su active_sleeves), stato persistente in data/paper_portfolio (gitignored). Dockerfile + docker-compose.yml minimali (solo servizio dashboard; runner/esecuzione restano in Old/). Container pythagoras-dashboard ricostruito col codice nuovo (il vecchio mostrava dati pre-reset). Mount data/ read-only. .dockerignore esclude Old/data/.venv/.git/.env. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Monitoraggio paper (dashboard). Lo stack live REALE resta in Old/."""
|
||||
@@ -0,0 +1,136 @@
|
||||
"""DASHBOARD web del portafoglio attivo (TP01 + XS01) — monitoraggio PAPER, stdlib only.
|
||||
|
||||
Mostra: metriche (FULL/HOLD Sharpe, DD, CAGR), per-sleeve, posizioni correnti, equity (backtest +
|
||||
paper forward da scripts/live/paper_portfolio.py), ultima data dato. Nessuna auth -> solo rete
|
||||
interna. Esecuzione REALE disabilitata: e' un monitor, non un trader.
|
||||
|
||||
uv run python -m src.live.dashboard --port 8787
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys, json, time
|
||||
from pathlib import Path
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
import numpy as np, pandas as pd
|
||||
from src.portfolio.portfolio import StrategyPortfolio, metrics, HOLDOUT
|
||||
from src.portfolio.sleeves import active_sleeves
|
||||
from src.version import APP_VERSION
|
||||
|
||||
PAPER = PROJECT_ROOT / "data" / "paper_portfolio" / "state.json"
|
||||
_CACHE = {"t": 0.0, "data": None}
|
||||
_TTL = 120.0
|
||||
|
||||
|
||||
def build():
|
||||
if _CACHE["data"] is not None and time.time() - _CACHE["t"] < _TTL:
|
||||
return _CACHE["data"]
|
||||
pf = StrategyPortfolio(active_sleeves(), capital=2000.0)
|
||||
bt = pf.backtest()
|
||||
eq = bt["equity"]; idx = bt["index"]
|
||||
# sparkline: subsample ~400 punti
|
||||
step = max(1, len(eq) // 400)
|
||||
spark = [(str(idx[i].date()), float(eq[i])) for i in range(0, len(eq), step)]
|
||||
paper = json.loads(PAPER.read_text()) if PAPER.exists() else None
|
||||
data = dict(
|
||||
version=APP_VERSION,
|
||||
last_data=str(idx[-1].date()),
|
||||
full=bt["full"], holdout=bt["holdout"], weights=bt["weights"],
|
||||
per_sleeve=bt["per_sleeve"], yearly=bt["yearly"],
|
||||
positions=pf.current_positions(), spark=spark, paper=paper,
|
||||
bh=None,
|
||||
)
|
||||
_CACHE.update(t=time.time(), data=data)
|
||||
return data
|
||||
|
||||
|
||||
def svg_spark(spark, w=900, h=220):
|
||||
ys = [v for _, v in spark]
|
||||
lo, hi = min(ys), max(ys)
|
||||
rng = hi - lo or 1
|
||||
pts = []
|
||||
for i, (_, v) in enumerate(spark):
|
||||
x = i / (len(spark) - 1) * w
|
||||
y = h - (v - lo) / rng * (h - 10) - 5
|
||||
pts.append(f"{x:.1f},{y:.1f}")
|
||||
return (f'<svg viewBox="0 0 {w} {h}" width="100%" height="{h}" preserveAspectRatio="none">'
|
||||
f'<polyline fill="none" stroke="#2ecc71" stroke-width="2" points="{" ".join(pts)}"/></svg>')
|
||||
|
||||
|
||||
def html():
|
||||
d = build()
|
||||
f, ho = d["full"], d["holdout"]
|
||||
rows = ""
|
||||
for name, s in d["per_sleeve"].items():
|
||||
rows += (f"<tr><td>{name}</td><td>{s['weight']*100:.0f}%</td>"
|
||||
f"<td>{s['full']['sharpe']:.2f}</td><td>{s['full']['maxdd']*100:.0f}%</td>"
|
||||
f"<td>{s['holdout']['sharpe']:.2f}</td></tr>")
|
||||
yrs = "".join(f"<span class=y>{y}: {v['ret']*100:+.0f}%</span>" for y, v in sorted(d["yearly"].items()))
|
||||
pos = ""
|
||||
for sl, p in d["positions"].items():
|
||||
pos += f"<tr><td>{sl}</td><td>{'flat (in cash)' if p == {'BTC': 0.0, 'ETH': 0.0} else (p if p is not None else 'stat-mode (book 19 gambe)')}</td></tr>"
|
||||
pp = d["paper"]
|
||||
if pp:
|
||||
days = (pd.Timestamp(pp["last"]) - pd.Timestamp(pp["start"])).days
|
||||
ret = pp["equity"] / pp["initial"] - 1
|
||||
paper_html = (f"<b>{pp['equity']:.2f}</b> (start {pp['initial']:.0f}, {pp['start'][:10]} → "
|
||||
f"{pp['last'][:10]}, {days}g) ret <b>{ret*100:+.2f}%</b> maxDD {pp['max_dd']*100:.1f}%")
|
||||
else:
|
||||
paper_html = "non inizializzato (gira <code>paper_portfolio.py</code>)"
|
||||
return f"""<!doctype html><html><head><meta charset=utf-8>
|
||||
<meta http-equiv=refresh content=300><title>PythagorasGoal — Portafoglio</title>
|
||||
<style>body{{font-family:-apple-system,Segoe UI,Roboto,sans-serif;background:#0e1116;color:#e6e6e6;margin:0;padding:24px;max-width:980px;margin:auto}}
|
||||
h1{{font-size:20px;margin:0 0 2px}}.sub{{color:#8a93a0;font-size:13px;margin-bottom:18px}}
|
||||
.cards{{display:flex;gap:14px;flex-wrap:wrap;margin-bottom:18px}}
|
||||
.card{{background:#161b22;border:1px solid #222b36;border-radius:10px;padding:14px 18px;min-width:150px}}
|
||||
.card .k{{color:#8a93a0;font-size:12px}}.card .v{{font-size:24px;font-weight:600}}.g{{color:#2ecc71}}.r{{color:#e74c3c}}
|
||||
table{{width:100%;border-collapse:collapse;margin:8px 0 20px}}td,th{{text-align:left;padding:7px 10px;border-bottom:1px solid #222b36;font-size:14px}}
|
||||
th{{color:#8a93a0;font-weight:500}}.y{{display:inline-block;background:#161b22;border:1px solid #222b36;border-radius:6px;padding:3px 8px;margin:2px;font-size:12px}}
|
||||
.box{{background:#161b22;border:1px solid #222b36;border-radius:10px;padding:14px 18px;margin-bottom:18px}}
|
||||
.warn{{color:#f1c40f;font-size:12px}}</style></head><body>
|
||||
<h1>PythagorasGoal — Portafoglio attivo (TP01 + XS01)</h1>
|
||||
<div class=sub>monitor PAPER · v{d['version']} · ultimo dato {d['last_data']} · esecuzione REALE disabilitata</div>
|
||||
<div class=cards>
|
||||
<div class=card><div class=k>FULL Sharpe</div><div class="v g">{f['sharpe']:.2f}</div></div>
|
||||
<div class=card><div class=k>HOLD-OUT Sharpe (2025-26)</div><div class="v g">{ho['sharpe']:.2f}</div></div>
|
||||
<div class=card><div class=k>maxDD</div><div class=v>{f['maxdd']*100:.1f}%</div></div>
|
||||
<div class=card><div class=k>CAGR</div><div class=v>{f['cagr']*100:.0f}%</div></div>
|
||||
<div class=card><div class=k>ret totale</div><div class=v>{f['ret']*100:+.0f}%</div></div>
|
||||
</div>
|
||||
<div class=box><div class=k style="color:#8a93a0;font-size:12px">EQUITY backtest (2019→oggi, €2k)</div>{svg_spark(d['spark'])}</div>
|
||||
<div class=box><b>Paper forward-only:</b> {paper_html}</div>
|
||||
<h3 style="font-size:14px;color:#8a93a0">Sleeve</h3>
|
||||
<table><tr><th>sleeve</th><th>peso</th><th>FULL Sh</th><th>DD</th><th>HOLD Sh</th></tr>{rows}</table>
|
||||
<h3 style="font-size:14px;color:#8a93a0">Posizioni correnti (ultima barra chiusa)</h3>
|
||||
<table>{pos}</table>
|
||||
<div style="margin-top:10px">{yrs}</div>
|
||||
<p class=warn>⚠️ Paper/monitor. XS01 e' STAT-MODE (book a 19 gambe market-neutral, non eseguibile a €2k). Storia XS ~2.5 anni.</p>
|
||||
</body></html>"""
|
||||
|
||||
|
||||
class H(BaseHTTPRequestHandler):
|
||||
def log_message(self, *a):
|
||||
pass
|
||||
|
||||
def do_GET(self):
|
||||
if self.path not in ("/", "/index.html"):
|
||||
self.send_response(404); self.end_headers(); return
|
||||
try:
|
||||
body = html().encode()
|
||||
except Exception as e:
|
||||
body = f"<pre>errore: {type(e).__name__}: {e}</pre>".encode()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.end_headers(); self.wfile.write(body)
|
||||
|
||||
|
||||
def main():
|
||||
port = 8787
|
||||
if "--port" in sys.argv:
|
||||
port = int(sys.argv[sys.argv.index("--port") + 1])
|
||||
print(f"dashboard su :{port} (Ctrl-C per uscire)")
|
||||
ThreadingHTTPServer(("0.0.0.0", port), H).serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user