chore(reset): v2.0.0 — storico certificato Deribit mainnet, ripartenza pulita
Reset del progetto su fondamenta verificate dopo la scoperta che l'intera libreria "validata OOS" era artefatto di feed contaminato (print fantasma del feed Cerbero TESTNET + storico Binance/USDT). - Storico ricostruito da Deribit MAINNET (ccxt pubblico, tokenless) e CERTIFICATO (certify_feed.py): BTC/ETH puliti su TUTTA la storia (mediana 2-6 bps vs Coinbase USD), integrita' OHLC + coerenza resample (maxΔ 0.00) + cross-venue OK. Alt esclusi (illiquidi/divergenti: LTC/DOGE 50-82% barre flat; XRP/BNB non certificabili). - Verdetto sul feed pulito: FADE / PAIRS / XS01 / TSM01 morti (ogni portafoglio Sharpe -2.3..-3.0, DD ~40%); solo SH01 e frammenti HONEST con segnale residuo, da ri-validare in isolamento. - Cleanup "restart pulito": strategie, stack live (src/live, src/portfolio, runner/executor, yml, docker), ~100 script ricerca/gate, waste/games/ portfolios, dati non certificati + cache e 60+ diari -> archiviati in Old/ (preservati, non cancellati). Diario consolidato in un unico documento. - Skeleton ricerca tenuto: Strategy ABC + indicatori + src/fractal + src/backtest/engine + load_data; tool dati certificati (rebuild_history, certify_feed, audit_feed, multi_source_check). - Universo dati ATTIVO: solo BTC/ETH (5m/15m/1h); guardrail fisico (load_data su alt -> FileNotFoundError). Esecuzione DISABILITATA, conto flat. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
"""Report orario PORT06 -> Telegram.
|
||||
|
||||
Legge lo stato persistito del paper trader a portafoglio (data/portfolio_paper/*/ +
|
||||
data/portfolios/PORT06/status.json) e invia su Telegram:
|
||||
1) trade CHIUSI: positivi/negativi (netto fee) con breakdown per motivo e PnL;
|
||||
2) trade IN CORSO (posizioni aperte);
|
||||
3) PnL realizzato totale + equity mark-to-market.
|
||||
|
||||
Eseguibile standalone (es. da cron orario):
|
||||
cd /opt/docker/PythagorasGoal && uv run python scripts/portfolios/hourly_report.py
|
||||
|
||||
Carica .env da solo (cron non eredita l'env del container). Legge file world-readable
|
||||
scritti dal container; non tocca lo stato del trader.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import glob
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
PAPER = ROOT / "data" / "portfolio_paper"
|
||||
PAPER_STATS = ROOT / "data" / "portfolio_paper_stats" # sleeve PAPER fuori dal conto reale
|
||||
PORT_STATUS = ROOT / "data" / "portfolios" / "PORT06" / "status.json"
|
||||
|
||||
|
||||
def _load_env():
|
||||
"""Carica TELEGRAM_* da .env nell'os.environ (cron non li ha)."""
|
||||
import os
|
||||
envf = ROOT / ".env"
|
||||
if not envf.exists():
|
||||
return
|
||||
for line in envf.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
k, v = line.split("=", 1)
|
||||
os.environ.setdefault(k.strip(), v.strip())
|
||||
|
||||
|
||||
def _short(wid: str) -> str:
|
||||
"""SH01_shape_ml__BTC__1h -> SH01/BTC ; PR01_..._ETH_SOL__1h -> PR01/ETH_SOL."""
|
||||
parts = wid.split("__")
|
||||
code = parts[0].split("_")[0]
|
||||
tag = parts[1] if len(parts) > 1 else ""
|
||||
return f"{code}/{tag}" if tag else code
|
||||
|
||||
|
||||
# --- monitor filtri fade: stop-rate per epoca di config ---
|
||||
# epoche: PRE (nessun filtro) -> HURST (loss-guard 0.55, v1.0.0) -> TREND (swap
|
||||
# hurst->trend_max=3.0, 2026-06-07: gate trendmax_port06_impact, hurst ridondante
|
||||
# post-EXIT-16). Confronta lo stop-rate live di ogni epoca col backtest.
|
||||
LOSSGUARD_SINCE = "2026-06-02T14:34:30"
|
||||
TRENDSWAP_SINCE = "2026-06-07T10:10:00"
|
||||
FADE_PREFIXES = ("MR01", "MR02", "MR07")
|
||||
LOSSGUARD_MIN_SAMPLE = 30
|
||||
|
||||
|
||||
def lossguard_section() -> str:
|
||||
pre = [0, 0] # [closes, stops] nessun filtro
|
||||
hurst = [0, 0] # epoca loss-guard Hurst
|
||||
trend = [0, 0] # epoca filtro trend (config attuale)
|
||||
for sp in glob.glob(str(PAPER / "*" / "status.json")):
|
||||
wid = Path(sp).parent.name
|
||||
if not wid.startswith(FADE_PREFIXES):
|
||||
continue
|
||||
tp = Path(sp).parent / "trades.jsonl"
|
||||
if not tp.exists():
|
||||
continue
|
||||
for line in tp.read_text().splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
ev = json.loads(line)
|
||||
if ev.get("event") != "CLOSE":
|
||||
continue
|
||||
ts = ev.get("ts", "")
|
||||
b = trend if ts >= TRENDSWAP_SINCE else hurst if ts >= LOSSGUARD_SINCE else pre
|
||||
b[0] += 1
|
||||
if ev.get("reason") == "stop_loss":
|
||||
b[1] += 1
|
||||
|
||||
def rate(b):
|
||||
return b[1] / b[0] * 100 if b[0] else 0.0
|
||||
|
||||
L = ["🛡️ <b>Filtri fade — stop-rate per epoca</b>"]
|
||||
L.append(f" PRE {rate(pre):.0f}% (n={pre[0]}) → HURST {rate(hurst):.0f}% (n={hurst[0]})"
|
||||
f" → TREND {rate(trend):.0f}% (n={trend[0]})")
|
||||
if trend[0] >= LOSSGUARD_MIN_SAMPLE:
|
||||
delta = rate(pre) - rate(trend)
|
||||
L.append(f" VERDETTO epoca TREND (n≥{LOSSGUARD_MIN_SAMPLE}): {delta:+.0f}pp vs PRE → "
|
||||
f"{'✅ riduce gli stop' if delta > 0 else '⚠️ nessuna riduzione'}")
|
||||
else:
|
||||
L.append(f" campione TREND {trend[0]}/{LOSSGUARD_MIN_SAMPLE} → verdetto rimandato")
|
||||
return "\n".join(L)
|
||||
|
||||
|
||||
# Epoca v1.1.26 (deploy 2026-06-11 ~21:40 UTC): gate TP_PHANTOM attivo. I close
|
||||
# precedenti includono il churn da TP fantasma dell'11-06 17:32-17:58 (~24 giri,
|
||||
# win-rate inquinato) -> le accuratezze "pulite" si leggono da qui in poi.
|
||||
EPOCH_V1126 = "2026-06-11T21:40:00"
|
||||
|
||||
|
||||
def collect():
|
||||
closed = [] # (sleeve, reason, net_return, pnl, win, ts)
|
||||
open_pos = [] # dict per posizione aperta
|
||||
realized = 0.0
|
||||
for sp in sorted(glob.glob(str(PAPER / "*" / "status.json"))):
|
||||
d = Path(sp).parent
|
||||
wid = d.name
|
||||
st = json.loads(Path(sp).read_text())
|
||||
tp = d / "trades.jsonl"
|
||||
if tp.exists():
|
||||
for line in tp.read_text().splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
ev = json.loads(line)
|
||||
if ev.get("event") != "CLOSE":
|
||||
continue
|
||||
nr = ev.get("net_return", 0.0)
|
||||
pnl = ev.get("pnl", 0.0)
|
||||
realized += pnl
|
||||
# win = flag del worker (col real-truth segue il PnL REALE; net_return
|
||||
# resta il sim diagnostico: sui TP fantasma da spike testnet diceva
|
||||
# 26/0 mentre il reale era 11/15). Fallback nr>0 per eventi storici.
|
||||
closed.append((_short(wid), ev.get("reason", "?"), nr, pnl,
|
||||
bool(ev.get("win", nr > 0)), ev.get("ts", "")))
|
||||
if "positions" in st or "weights" in st:
|
||||
continue # multi-asset (TR01/ROT02/TSM01): sezione dedicata
|
||||
if st.get("in_position"):
|
||||
open_pos.append({
|
||||
"sleeve": _short(wid),
|
||||
"dir": st.get("direction", 0),
|
||||
"entry": st.get("entry_price") or st.get("entry_a"),
|
||||
"entry_b": st.get("entry_b"),
|
||||
"bars": st.get("bars_held", 0),
|
||||
"cap": st.get("capital", 0.0),
|
||||
})
|
||||
return closed, open_pos, realized
|
||||
|
||||
|
||||
def multi_asset_section() -> str:
|
||||
"""Sleeve PAPER (TR01/ROT02/TSM01): SOLO statistica, FUORI dal conto reale dei
|
||||
€2000 (2026-06-08). Vivono in data/portfolio_paper_stats/ con capitale nozionale
|
||||
fisso, raccolti per eventuali future implementazioni reali. Book corrente + eta'
|
||||
ultimo flip + freschezza."""
|
||||
now = datetime.now(timezone.utc)
|
||||
rows = []
|
||||
for sp in sorted(glob.glob(str(PAPER_STATS / "*" / "status.json"))):
|
||||
d = Path(sp).parent
|
||||
st = json.loads(Path(sp).read_text())
|
||||
book = st.get("positions") if "positions" in st else st.get("weights")
|
||||
if book is None and "books" in st:
|
||||
# XS01 tranched (2026-06-11): aggrega i sub-book in un book medio per-asset
|
||||
k = max(1, len(st["books"]))
|
||||
book = {}
|
||||
for b in st["books"]:
|
||||
for a, v in (b.get("weights") or {}).items():
|
||||
book[a] = book.get(a, 0.0) + v / k
|
||||
if book is None:
|
||||
continue # single-leg/pairs: gia' coperti da collect()
|
||||
# abs(): il book XS01 e' long/short market-neutral — col filtro v>0 le
|
||||
# gambe SHORT sparivano e un book net-short appariva "flat" nel report
|
||||
held = {a: v for a, v in book.items() if abs(v) > 1e-4}
|
||||
flip = "mai"
|
||||
tp = d / "trades.jsonl"
|
||||
if tp.exists():
|
||||
lines = [ln for ln in tp.read_text().splitlines() if ln.strip()]
|
||||
if lines:
|
||||
ts = json.loads(lines[-1]).get("ts", "")
|
||||
if ts:
|
||||
days = (now - datetime.fromisoformat(ts)).days
|
||||
flip = f"{days}g fa"
|
||||
fresh = "?"
|
||||
lu = st.get("ts") or st.get("last_update")
|
||||
if lu:
|
||||
h = (now - datetime.fromisoformat(lu)).total_seconds() / 3600
|
||||
fresh = "OK" if h < 2 else f"STALE {h:.0f}h"
|
||||
code = d.name.split("__")[0].split("_")[0] # TR01_basket__... -> TR01
|
||||
hb = ",".join(f"{a}:{v:.2f}" for a, v in sorted(held.items())) if held else "flat"
|
||||
rows.append(f"{code:<7}{hb:<26}{flip:>8} {fresh}")
|
||||
if not rows:
|
||||
return ""
|
||||
return ("📈 <b>PAPER — solo statistica, FUORI dal conto reale</b> (book | ultimo flip | status)\n<pre>"
|
||||
+ "\n".join(rows) + "</pre>")
|
||||
|
||||
|
||||
def build_report() -> str:
|
||||
closed, open_pos, realized = collect()
|
||||
pos = sum(1 for c in closed if c[4])
|
||||
neg = len(closed) - pos
|
||||
|
||||
# breakdown per motivo
|
||||
by_reason = defaultdict(lambda: [0, 0, 0.0]) # reason -> [win, loss, pnl]
|
||||
for _, reason, _, pnl, win, _ in closed:
|
||||
r = by_reason[reason]
|
||||
r[0 if win else 1] += 1
|
||||
r[2] += pnl
|
||||
|
||||
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
||||
try:
|
||||
ver = (ROOT / "VERSION").read_text().strip()
|
||||
except Exception:
|
||||
ver = "?"
|
||||
eq = dd = cap = None
|
||||
if PORT_STATUS.exists():
|
||||
ps = json.loads(PORT_STATUS.read_text())
|
||||
eq, cap, dd = ps.get("equity"), ps.get("total_capital"), ps.get("max_dd")
|
||||
|
||||
L = [f"📊 <b>PORT06 — Report orario</b> <code>v{ver}</code>", now]
|
||||
if eq is not None:
|
||||
L.append(f"Equity €{eq:.2f} | Cap €{cap:.2f} | maxDD {dd:.3f}%")
|
||||
|
||||
# 1) CHIUSI — totale storico + epoca corrente (post gate TP_PHANTOM): i
|
||||
# numeri pre-v1.1.26 includono il churn fantasma e non misurano la strategia
|
||||
cur = [c for c in closed if c[5] >= EPOCH_V1126]
|
||||
cpos = sum(1 for c in cur if c[4])
|
||||
L.append(f"\n✅ <b>CHIUSI</b>: {pos} positivi / {neg} negativi (netto fee)")
|
||||
L.append(f" epoca v1.1.26+ (TP_PHANTOM attivo): {cpos}/{len(cur) - cpos}")
|
||||
rows = [f"{'motivo':<12}{'✅':>3}{'❌':>4}{'PnL€':>9}"]
|
||||
for reason, (w, l, pnl) in sorted(by_reason.items(), key=lambda x: x[1][2]):
|
||||
rows.append(f"{reason:<12}{w:>3}{l:>4}{pnl:>+9.2f}")
|
||||
L.append("<pre>" + "\n".join(rows) + "</pre>")
|
||||
|
||||
# 2) IN CORSO
|
||||
L.append(f"🟢 <b>IN CORSO</b>: {len(open_pos)} posizioni")
|
||||
if open_pos:
|
||||
rows = [f"{'sleeve':<14}{'d':<2}{'barre':>6} {'entry'}"]
|
||||
for p in sorted(open_pos, key=lambda x: x["sleeve"]):
|
||||
d = "L" if p["dir"] == 1 else "S" if p["dir"] == -1 else "-"
|
||||
entry = p["entry"]
|
||||
es = f"{entry:.6g}" if isinstance(entry, (int, float)) else str(entry)
|
||||
if p["entry_b"]:
|
||||
es = f"{entry:.6g}/{p['entry_b']:.6g}" # coppia: 2 gambe
|
||||
rows.append(f"{p['sleeve']:<14}{d:<2}{p['bars']:>6} {es}")
|
||||
L.append("<pre>" + "\n".join(rows) + "</pre>")
|
||||
|
||||
# 2a) worker multi-asset (TR01/ROT02/TSM01)
|
||||
mas = multi_asset_section()
|
||||
if mas:
|
||||
L.append(mas)
|
||||
|
||||
# 2b) monitor loss-guard
|
||||
L.append(lossguard_section())
|
||||
|
||||
# 3) TOTALE
|
||||
L.append(f"💰 <b>PnL realizzato totale: €{realized:+.2f}</b>")
|
||||
if eq is not None:
|
||||
unreal = eq - cap
|
||||
L.append(f" equity mark-to-market: €{eq:.2f} (non realizz. €{unreal:+.2f})")
|
||||
return "\n".join(L)
|
||||
|
||||
|
||||
def main():
|
||||
_load_env()
|
||||
import sys
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from src.live.telegram_notifier import send_telegram
|
||||
report = build_report()
|
||||
print(report)
|
||||
ok = send_telegram(report)
|
||||
print("\n[telegram]", "inviato" if ok else "NON inviato (token/chat mancanti o errore rete)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user