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,11 @@
|
|||||||
|
Old/
|
||||||
|
data/
|
||||||
|
.venv/
|
||||||
|
.git/
|
||||||
|
logs/
|
||||||
|
__pycache__/
|
||||||
|
**/__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.env
|
||||||
|
.env.mainnet
|
||||||
|
docs/
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
||||||
|
WORKDIR /app
|
||||||
|
COPY pyproject.toml uv.lock ./
|
||||||
|
RUN uv sync --frozen --no-dev
|
||||||
|
COPY src/ src/
|
||||||
|
COPY scripts/ scripts/
|
||||||
|
COPY VERSION ./
|
||||||
|
VOLUME /app/data
|
||||||
|
# Monitor PAPER del portafoglio attivo (TP01+XS01). Esecuzione REALE disabilitata.
|
||||||
|
CMD ["uv", "run", "python", "-m", "src.live.dashboard", "--port", "8787"]
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Solo MONITOR (dashboard paper) del portafoglio attivo. Niente runner/esecuzione reale
|
||||||
|
# (archiviati in Old/). v2.0.0+.
|
||||||
|
services:
|
||||||
|
dashboard:
|
||||||
|
build: .
|
||||||
|
container_name: pythagoras-dashboard
|
||||||
|
restart: unless-stopped
|
||||||
|
command: ["uv", "run", "python", "-m", "src.live.dashboard", "--port", "8787"]
|
||||||
|
ports:
|
||||||
|
- "8787:8787"
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data:ro
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
"""PAPER PORTFOLIO — forward-only del portafoglio attivo (TP01 + XS01), simulato.
|
||||||
|
|
||||||
|
Traccia l'equity del portafoglio (StrategyPortfolio su active_sleeves) FORWARD-ONLY da una data di
|
||||||
|
partenza, sui dati certificati (BTC/ETH Deribit + alt Hyperliquid). Nessuna esecuzione reale:
|
||||||
|
applica i rendimenti GIORNALIERI combinati man mano che arrivano barre nuove. Stato persistente.
|
||||||
|
Il dashboard (src/live/dashboard.py) legge questo stato + ricalcola il backtest a colpo d'occhio.
|
||||||
|
|
||||||
|
uv run python scripts/live/paper_portfolio.py # avanza (init al 1o run)
|
||||||
|
uv run python scripts/live/paper_portfolio.py --status # solo stato
|
||||||
|
uv run python scripts/live/paper_portfolio.py --reset # azzera (riparte da ora)
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import sys, json
|
||||||
|
from pathlib import Path
|
||||||
|
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
|
||||||
|
from src.portfolio.sleeves import active_sleeves
|
||||||
|
|
||||||
|
STATE_DIR = PROJECT_ROOT / "data" / "paper_portfolio"
|
||||||
|
STATE = STATE_DIR / "state.json"
|
||||||
|
EQ = STATE_DIR / "equity.csv"
|
||||||
|
INITIAL = 2000.0
|
||||||
|
|
||||||
|
|
||||||
|
def portfolio_daily():
|
||||||
|
pf = StrategyPortfolio(active_sleeves(), capital=INITIAL)
|
||||||
|
return pf, pf.combined_daily()
|
||||||
|
|
||||||
|
|
||||||
|
def load():
|
||||||
|
return json.loads(STATE.read_text()) if STATE.exists() else None
|
||||||
|
|
||||||
|
|
||||||
|
def save(st):
|
||||||
|
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
STATE.write_text(json.dumps(st, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def advance():
|
||||||
|
pf, r = portfolio_daily()
|
||||||
|
st = load()
|
||||||
|
if st is None: # init: forward-only, parte dall'ultima barra
|
||||||
|
last = str(r.index[-1])
|
||||||
|
st = dict(start=last, last=last, equity=INITIAL, initial=INITIAL,
|
||||||
|
peak=INITIAL, max_dd=0.0, n_days=0)
|
||||||
|
save(st)
|
||||||
|
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
EQ.write_text("date,equity\n" + f"{last},{INITIAL}\n")
|
||||||
|
return st
|
||||||
|
last = pd.Timestamp(st["last"])
|
||||||
|
new = r[r.index > last]
|
||||||
|
if len(new):
|
||||||
|
eq = st["equity"]; peak = st["peak"]; dd = st["max_dd"]
|
||||||
|
lines = []
|
||||||
|
for d, ret in new.items():
|
||||||
|
eq *= (1.0 + float(ret)); peak = max(peak, eq); dd = max(dd, (peak - eq) / peak if peak > 0 else 0)
|
||||||
|
lines.append(f"{d},{eq:.4f}")
|
||||||
|
st.update(equity=eq, last=str(new.index[-1]), peak=peak, max_dd=dd, n_days=st["n_days"] + len(new))
|
||||||
|
save(st)
|
||||||
|
with open(EQ, "a") as f:
|
||||||
|
f.write("\n".join(lines) + "\n")
|
||||||
|
return st
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
a = sys.argv[1:]
|
||||||
|
if "--reset" in a:
|
||||||
|
for f in (STATE, EQ):
|
||||||
|
f.unlink(missing_ok=True)
|
||||||
|
print("paper portfolio azzerato.")
|
||||||
|
st = load() if "--status" in a else advance()
|
||||||
|
if st is None:
|
||||||
|
st = advance()
|
||||||
|
pf, _ = portfolio_daily()
|
||||||
|
days = (pd.Timestamp(st["last"]) - pd.Timestamp(st["start"])).days
|
||||||
|
ret = st["equity"] / st["initial"] - 1
|
||||||
|
print(f"PAPER PORTFOLIO (TP01+XS01) — forward-only")
|
||||||
|
print(f" start {st['start'][:10]} -> last {st['last'][:10]} ({days}g, {st['n_days']} barre)")
|
||||||
|
print(f" equity {st['equity']:.2f} (start {st['initial']:.0f}) ret {ret*100:+.2f}% maxDD {st['max_dd']*100:.1f}%")
|
||||||
|
print(f" posizioni correnti: {pf.current_positions()}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -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