From e3288366143b317e875e5eee08452b3e077e6c64 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Wed, 17 Jun 2026 21:15:55 +0000 Subject: [PATCH] =?UTF-8?q?feat(live):=20switch=20a=20MAINNET=20=E2=80=94?= =?UTF-8?q?=20micro-test=20fade-only=20=E2=82=AC500=20+=20funds-watch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Conversione testnet -> mainnet Deribit (soldi veri). Vedi docs/specs/mainnet-microtest-plan.md, Fase 1. - portfolios.yml: total_capital 500, leva 3, weighting equal; eseguono REALE solo i 7 single-leg (6 fade MR01/02/07 x BTC/ETH 15m + DIP01 1h); pairs/SH01/multi-asset -> paper (rumore arrotondamento a €500); real_truth. - docker-compose.yml: portfolio+dashboard caricano anche .env.mainnet (token mainnet, prevale su .env; .env.mainnet resta gitignored). - reconcile_account.py: watermark FONDI (compute_funds_change) — rileva aumenti di capitale (deposito/top-up) e cali anomali (prelievo) sul balance USDC, alert Telegram FUNDS_INCREASE/FUNDS_DECREASE; soglia max(€25, 5%). Stato in data/funds_watch.json (host-writable). - .gitignore: ignora data/funds_watch.json (stato runtime). Ledger testnet archiviato in data/_reset_backup/pre_mainnet_*.tgz e azzerato. Crons host ri-puntati al conto reale (sourcing .env + .env.mainnet). Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 1 + docker-compose.yml | 2 + portfolios.yml | 102 ++++++++++---------------- scripts/analysis/reconcile_account.py | 79 ++++++++++++++++++++ 4 files changed, 122 insertions(+), 62 deletions(-) diff --git a/.gitignore b/.gitignore index 3a9f070..9cea3df 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ data/paper_trades/ data/portfolio_paper/ data/portfolio_paper_stats/ data/portfolios/ +data/funds_watch.json # watermark fondi del reconciler (stato runtime, contiene il balance) # stato locale di tooling (non condiviso) .claude/ diff --git a/docker-compose.yml b/docker-compose.yml index 595fad2..596fea4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,7 @@ services: - ./portfolios.yml:/app/portfolios.yml:ro env_file: - .env + - .env.mainnet # token MAINNET (soldi veri); prevale su .env (vedi micro-test plan) environment: - PYTHONUNBUFFERED=1 healthcheck: @@ -36,6 +37,7 @@ services: - ./docs/report:/app/docs/report:ro # scheda strategie_attive.html (modal "scheda dettagliata") env_file: - .env + - .env.mainnet # token MAINNET (soldi veri); prevale su .env (vedi micro-test plan) environment: - PYTHONUNBUFFERED=1 healthcheck: diff --git a/portfolios.yml b/portfolios.yml index b7ec908..9cabc6b 100644 --- a/portfolios.yml +++ b/portfolios.yml @@ -1,77 +1,55 @@ -# Config LIVE del paper trader a portafoglio. Seleziona UN portafoglio attivo +# Config LIVE del trader a portafoglio. Seleziona UN portafoglio attivo # (definito in scripts/portfolios/_defs.py) e ne fa l'override dei parametri operativi. -active: PORT06 # default raccomandato: master + shape +# +# ============ MICRO-TEST MAINNET — soldi VERI (Fase 1, 2026-06-17) ============ +# Conversione da testnet a mainnet Deribit (vedi docs/specs/mainnet-microtest-plan.md). +# Capitale REALE €500 (periodo di prova; poi si scala col verdetto ledger-vs-backtest). +# Eseguono reale SOLO le 6 fade (MR01/MR02/MR07 x BTC/ETH, 15m) + DIP01 (BTC 1h); +# pairs/SH01/multi-asset -> PAPER (sola statistica, fuori dal pool/conto). Il token +# mainnet arriva da .env.mainnet (env_file del servizio). Ledger ripartito da ZERO: +# lo storico testnet e' archiviato in data/_reset_backup/pre_mainnet_20260617-205943.tgz. +active: PORT06 overrides: - total_capital: 2000 - weighting: cap # equal | cap | inverse_vol | cluster_rp | manual - # NB: questo dict SOSTITUISCE interamente i caps di _defs.py (setattr in - # base.py:load_active_portfolio) → va ridichiarato COMPLETO. Il cap SHAPE - # 0.0588 (mitigazione coda SH01, 2026-06-05) era stato perso per questo. - caps: {PAIRS: 0.33, SHAPE: 0.0588} - # Leva 3x (2026-06-12, scelta utente, da frontiera ACCEL50: OOS CAGR 111->206%, - # FULL DD 3.5->5.2% nel modello lineare; vedi scripts/analysis/accel50_research.py - # e docs/diary/2026-06-12-accel50.md). Era 2 ("sobrio") dal primo deploy live. + # CAPITALE REALE del micro-test (era 2000 su testnet). €500 = minimo del piano + # (rumore arrotondamento BTC ~5-6%); si sale dopo il verdetto ledger-vs-backtest. + total_capital: 500 + # equal-weight: nel pool REALE restano solo FADE (6) + DIP01 (1) -> 1/7 ciascuno. + # I cap PAIRS/SHAPE non servono piu' (quelle famiglie sono PAPER, fuori dal pool). + weighting: equal + # Leva 3x (scelta utente 2026-06-17 per il micro-test = config che vogliamo deployare, + # frontiera ACCEL50). NB su soldi veri al capitale minimo: DD pieno (alt prudente 2x). leverage: 3 rebalance: 1D poll_seconds: 60 - # Gate feed CONGELATO (2026-06-15): salta entry+exit degli sleeve concentrati - # (single/ml/pairs) il cui feed di DECISIONE 1h e' fermo da >= N barre di close - # INVARIATA (guasto testnet: ETH-PERPETUAL a 1661.95 per 36h+ -> z-score pairs - # spuri e perdite reali, i fill avvengono al book vero). Distingue il guasto - # dall'illiquidita' (alt flat ma che si muovono ogni ~10 barre). Auto-guarente: - # si rilascia alla prima barra completa non-flat. 24 = un giorno di prezzo - # immobile (margine pulito tra le due popolazioni). 0 = disattivo. + # Gate feed CONGELATO: su mainnet l'arbitraggio tiene il feed vivo (niente freeze come + # sul testnet), lo lascio attivo come rete di sicurezza (non scattera' su BTC/ETH + # liquidi). 0 = disattivo. feed_freeze_gate_bars: 24 - # SLEEVE PAPER (2026-06-08): fuori dal pool/pesi/ledger — i €2000 si dividono SOLO - # tra i 14 sleeve REALI (fade+DIP01+pairs+SH01). I multi-asset (no esecuzione reale, - # bloccati dal capitale) girano in data/portfolio_paper_stats/ con capitale nozionale - # fisso, SOLO per statistica in vista di future implementazioni reali. NB: il portafoglio - # live diverge ora dal PORT06 canonico (17 sleeve) -> DD reale ~5.35% vs 3.96% validato: - # il prezzo di vedere il risultato reale puro (scelta utente). - paper_sleeves: [TR01, ROT02, TSM01, XS01] - # Frazione di capitale-sleeve per posizione (canonico backtest = 0.15). - # 0.5 con leva 2x = 100% della fetta impegnata quando in posizione (max impiego - # dei 2K senza debito di margine). NB: il DD scala ~lineare (~×3.3 vs validato). + # SLEEVE PAPER (fuori dal pool/pesi/ledger): i €500 si dividono SOLO tra i 7 sleeve + # REALI (6 fade + DIP01). Pairs (PR01, 2 gambe) e SH01 fuori dalla Fase 1: a €500 il + # rumore di arrotondamento li soffoca (pairs ~30%/gamba; servono ~€5-8k) e aggiungono + # superfici d'errore (leg-risk). Multi-asset (TR01/ROT02/TSM01/XS01) paper come sempre. + paper_sleeves: [TR01, ROT02, TSM01, XS01, PR01, SH01] + # Frazione di capitale-sleeve per posizione (0.5 con leva 3 = 1.5x la fetta impegnata). position_size: 0.5 - # Override per-famiglia (chiave = weighting.family_of). PAIRS: famiglia SENZA - # stop, validata a esposizione 0.45 (pos 0.15 lev 3) — il gate 2026-06-07 - # (pairspos_port06_impact.py) la fisso' a 0.40 di esposizione (0.20 × lev 2): - # PORT06 OOS DD 3.40→1.26%, costo OOS Sharpe 9.05→8.43. Col passaggio a lev 3 - # (2026-06-12) il pos scende a 0.13 per CONSERVARE la stessa esposizione - # (0.13×3≈0.40): la leva accelera le famiglie con stop, non i pairs no-stop. + # Override per-famiglia: irrilevante per il conto reale (i pairs sono PAPER), tenuto + # solo perche' i worker pairs in sola-statistica dimensionino come da gate storico. position_size_family: {PAIRS: 0.13} - # Esecuzione REALE su Deribit testnet, in SHADOW (sim + reale in parallelo). - # I 7 single-leg con TP/SL in metadata: 6 fade (MR01/MR02/MR07 x BTC/ETH) + - # DIP01 BTC (attivato 2026-06-04: stesso wiring StrategyWorker, TP limit resting - # incluso). Ordini sui LINEARI USDC (payoff lineare = matematica del backtest; - # fee/PnL in USDC). Gli altri sleeve (pairs/rotation/tsmom/shape) restano - # simulati: pairs richiede executor a 2 gambe, shape non ha TP (orizzonte puro). + # Esecuzione REALE su Deribit MAINNET. Solo i 7 single-leg con TP/SL in metadata: + # 6 fade (MR01/MR02/MR07 x BTC/ETH 15m) + DIP01 (BTC 1h). Ordini sui LINEARI USDC + # (payoff lineare = matematica del backtest; fee/PnL in USDC). execution: enabled: true - # SH01 aggiunto 2026-06-08: e' il diversificatore piu' decorrelato (corr 0.07, - # senza di lui il DD del portafoglio sale 3.96->5.35%). Single-leg, esce a - # orizzonte H=12 (no TP/SL) -> _place_real_tp no-op, _real_close market reduce-only; - # disaster-bracket on-book = unica protezione di coda durante outage. - sleeves: [MR01, MR02, MR07, DIP01, SH01] + # SOLO fade + DIP01 in Fase 1 (SH01 e pairs -> paper, vedi paper_sleeves). + sleeves: [MR01, MR02, MR07, DIP01] instruments: BTC: BTC_USDC-PERPETUAL ETH: ETH_USDC-PERPETUAL - LTC: LTC_USDC-PERPETUAL - ADA: ADA_USDC-PERPETUAL - SOL: SOL_USDC-PERPETUAL - # Esecuzione REALE a 2 gambe per i pairs (PairsExecutionClient, 2026-06-08). - # Long A / short B sui lineari USDC, con unwind se una gamba sola filla (leg-risk). - # ATTIVATO 2026-06-08 a conto flat (smoke testnet end-to-end verificato). - pairs_enabled: true - # Disaster-bracket on-book (2026-06-07): STOP_MARKET reduce-only a ~-30% - # dall'ingresso, piazzato a ogni REAL_OPEN e cancellato alla chiusura. - # Assicurazione per gli outage (runner fermo = exit non valutati); in - # operativita' normale non scatta mai -> 0 costo Sharpe. 0 = disattivo. + # niente esecuzione a 2 gambe in Fase 1 (pairs sono paper). + pairs_enabled: false + # Disaster-bracket on-book (~-30%) a ogni apertura: assicurazione per gli outage + # del runner. In operativita' normale non scatta mai -> 0 costo. 0 = disattivo. disaster_sl_pct: 0.30 - # REAL-TRUTH (2026-06-10, scelta utente): il ledger `capital` degli sleeve - # eseguiti si aggiorna col PnL dei FILL REALI (fee reali incluse) — il sim - # resta solo diagnostica nel log CLOSE (pnl_source/sim_pnl/real_pnl). - # Fallback al sim SOLO se il trade reale non e' mai esistito/fillato - # (REAL_OPEN_FAIL): flag pnl_source=sim_fallback nel log. Cosi' equity, - # ribilanci e sizing derivano dai soldi veri sul conto, non dal sim. + # REAL-TRUTH: equity/ribilanci/sizing derivano dai FILL REALI (fee reali incluse), + # non dal sim. Il sim resta solo diagnostica nel log CLOSE. real_truth: true diff --git a/scripts/analysis/reconcile_account.py b/scripts/analysis/reconcile_account.py index 23be32e..ace6b11 100644 --- a/scripts/analysis/reconcile_account.py +++ b/scripts/analysis/reconcile_account.py @@ -23,6 +23,7 @@ from __future__ import annotations import json import sys import time +from datetime import datetime, timezone from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parents[2] @@ -39,6 +40,14 @@ TOL_STEPS = 1.5 # tolleranza = 1.5 × step contratto STALE_REAL_MIN = 15 # un worker vivo aggiorna status.json ~ogni poll (60s); # real_in_position con mtime oltre questo = NON gestito +# --- Watermark FONDI (2026-06-17): rileva aumenti di capitale (depositi/top-up) e +# cali anomali (prelievi) confrontando il balance USDC col precedente osservato. +# Serve a sapere quando un versamento e' arrivato (per scalare total_capital). --- +FUNDS_STATE = PROJECT_ROOT / "data" / "funds_watch.json" # data/ e' scrivibile dal + # cron host (adriano); data/portfolios e' root (container) +FUNDS_DELTA_MIN = 25.0 # USDC: sotto questo Δbalance e' rumore di trading (PnL/fee orarie) +FUNDS_DELTA_PCT = 0.05 # ...o 5% del balance precedente (robusto al crescere del conto) + def compute_drift(client: CerberoClient | None = None) -> list[dict]: client = client or CerberoClient() @@ -139,11 +148,55 @@ def compute_stale_real_positions(max_age_min: int = STALE_REAL_MIN) -> list[dict return out +def compute_funds_change(client: CerberoClient | None = None, + currency: str = "USDC") -> dict: + """Watermark dei FONDI: confronta il `balance` USDC corrente con l'ultimo + osservato (persistito in FUNDS_STATE) e segnala un AUMENTO (probabile deposito/ + top-up di capitale) o un CALO anomalo (probabile prelievo). Read-only; aggiorna + SEMPRE il watermark (-> l'alert scatta UNA volta sul gradino, non si ripete). + + Si traccia `balance` (cassa realizzata: cambia su deposito/prelievo/PnL chiuso/fee), + NON `equity` (che ondeggia con l'unrealized -> falsi positivi). Soglia robusta = + max(FUNDS_DELTA_MIN, FUNDS_DELTA_PCT × balance_prec): il PnL realizzato di trading + in un'ora resta sotto, un versamento la supera nettamente. Primo run = solo seed.""" + client = client or CerberoClient() + s = client.get_account_summary(currency) + cur_bal = float(s.get("balance", s.get("equity", 0.0)) or 0.0) + cur_eq = float(s.get("equity", cur_bal) or 0.0) + iso = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + out = dict(currency=currency, balance=cur_bal, equity=cur_eq, ts=iso, + prev_balance=None, delta=0.0, thr=0.0, kind="seed") + prev = None + if FUNDS_STATE.exists(): + try: + prev = json.loads(FUNDS_STATE.read_text()) + except Exception: + prev = None + if prev is not None and "balance" in prev: + pb = float(prev["balance"]) + delta = cur_bal - pb + thr = max(FUNDS_DELTA_MIN, FUNDS_DELTA_PCT * abs(pb)) + out.update(prev_balance=pb, prev_ts=prev.get("ts"), delta=delta, thr=thr, + kind="increase" if delta >= thr else + "decrease" if delta <= -thr else "flat") + try: # persisti il nuovo watermark + FUNDS_STATE.parent.mkdir(parents=True, exist_ok=True) + FUNDS_STATE.write_text(json.dumps( + {"ts": iso, "balance": cur_bal, "equity": cur_eq, "currency": currency})) + except Exception as e: + out["persist_error"] = str(e) + return out + + def main(): client = CerberoClient() rows = compute_drift(client) resting = compute_resting_drift(client) stale = compute_stale_real_positions() + try: + funds = compute_funds_change(client) + except Exception as e: + funds = {"kind": "error", "error": str(e)} bad = [r for r in rows if not r["ok"]] bad_rest = [r for r in resting if r["status"] != "OK"] if bad or bad_rest: @@ -176,6 +229,17 @@ def main(): if not stale: print("(nessuna posizione reale stantia)") + print(f"\n{'FONDI conto (' + funds.get('currency', 'USDC') + ')':<30}{'prec':>12}{'attuale':>12}{'Δ':>12}") + if funds["kind"] == "error": + print(f" ⚠️ lettura fondi fallita: {funds.get('error', '')[:120]}") + elif funds["kind"] == "seed": + print(f" baseline registrata: balance={funds['balance']:.2f} (primo run, nessun confronto)") + else: + tag = {"increase": "⬆️ AUMENTO (deposito?)", "decrease": "⬇️ CALO (prelievo?)", + "flat": "= invariato"}[funds["kind"]] + print(f" {'balance':<27}{funds['prev_balance']:>12.2f}{funds['balance']:>12.2f}" + f"{funds['delta']:>+12.2f} {tag} (soglia ±{funds['thr']:.2f})") + print("\nESITO:", "OK — conto allineato ai libri" if not (bad or bad_rest or stale) else f"⚠️ ANOMALIE (drift_pos={len(bad)} resting={len(bad_rest)} stale={len(stale)})") @@ -209,6 +273,21 @@ def main(): "(caso MR02_BTC 2026-06-12); MISSING = atteso ma non in book; " "STALE = in book senza libro corrispondente")}) print("[telegram] alert RESTING_DRIFT inviato") + + if funds.get("kind") in ("increase", "decrease") and "--telegram" in sys.argv: + from src.live.telegram_notifier import notify_event + ev = "FUNDS_INCREASE" if funds["kind"] == "increase" else "FUNDS_DECREASE" + notify_event(ev, { + "valuta": funds["currency"], + "balance_prec": round(funds["prev_balance"], 2), + "balance_attuale": round(funds["balance"], 2), + "delta": round(funds["delta"], 2), + "equity": round(funds["equity"], 2), + "note": ("AUMENTO fondi (probabile DEPOSITO/top-up): per usarli, scala " + "total_capital in portfolios.yml e riavvia il runner " + "(docker compose up -d)" if funds["kind"] == "increase" + else "CALO fondi non spiegato dal trading (prelievo? verifica il conto)")}) + print(f"[telegram] alert {ev} inviato") return bool(bad or bad_rest or stale)