feat(live): switch a MAINNET — micro-test fade-only €500 + funds-watch

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) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-17 21:15:55 +00:00
parent a0842a61cc
commit e328836614
4 changed files with 122 additions and 62 deletions
+1
View File
@@ -20,6 +20,7 @@ data/paper_trades/
data/portfolio_paper/ data/portfolio_paper/
data/portfolio_paper_stats/ data/portfolio_paper_stats/
data/portfolios/ data/portfolios/
data/funds_watch.json # watermark fondi del reconciler (stato runtime, contiene il balance)
# stato locale di tooling (non condiviso) # stato locale di tooling (non condiviso)
.claude/ .claude/
+2
View File
@@ -9,6 +9,7 @@ services:
- ./portfolios.yml:/app/portfolios.yml:ro - ./portfolios.yml:/app/portfolios.yml:ro
env_file: env_file:
- .env - .env
- .env.mainnet # token MAINNET (soldi veri); prevale su .env (vedi micro-test plan)
environment: environment:
- PYTHONUNBUFFERED=1 - PYTHONUNBUFFERED=1
healthcheck: healthcheck:
@@ -36,6 +37,7 @@ services:
- ./docs/report:/app/docs/report:ro # scheda strategie_attive.html (modal "scheda dettagliata") - ./docs/report:/app/docs/report:ro # scheda strategie_attive.html (modal "scheda dettagliata")
env_file: env_file:
- .env - .env
- .env.mainnet # token MAINNET (soldi veri); prevale su .env (vedi micro-test plan)
environment: environment:
- PYTHONUNBUFFERED=1 - PYTHONUNBUFFERED=1
healthcheck: healthcheck:
+40 -62
View File
@@ -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. # (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: overrides:
total_capital: 2000 # CAPITALE REALE del micro-test (era 2000 su testnet). €500 = minimo del piano
weighting: cap # equal | cap | inverse_vol | cluster_rp | manual # (rumore arrotondamento BTC ~5-6%); si sale dopo il verdetto ledger-vs-backtest.
# NB: questo dict SOSTITUISCE interamente i caps di _defs.py (setattr in total_capital: 500
# base.py:load_active_portfolio) → va ridichiarato COMPLETO. Il cap SHAPE # equal-weight: nel pool REALE restano solo FADE (6) + DIP01 (1) -> 1/7 ciascuno.
# 0.0588 (mitigazione coda SH01, 2026-06-05) era stato perso per questo. # I cap PAIRS/SHAPE non servono piu' (quelle famiglie sono PAPER, fuori dal pool).
caps: {PAIRS: 0.33, SHAPE: 0.0588} weighting: equal
# Leva 3x (2026-06-12, scelta utente, da frontiera ACCEL50: OOS CAGR 111->206%, # Leva 3x (scelta utente 2026-06-17 per il micro-test = config che vogliamo deployare,
# FULL DD 3.5->5.2% nel modello lineare; vedi scripts/analysis/accel50_research.py # frontiera ACCEL50). NB su soldi veri al capitale minimo: DD pieno (alt prudente 2x).
# e docs/diary/2026-06-12-accel50.md). Era 2 ("sobrio") dal primo deploy live.
leverage: 3 leverage: 3
rebalance: 1D rebalance: 1D
poll_seconds: 60 poll_seconds: 60
# Gate feed CONGELATO (2026-06-15): salta entry+exit degli sleeve concentrati # Gate feed CONGELATO: su mainnet l'arbitraggio tiene il feed vivo (niente freeze come
# (single/ml/pairs) il cui feed di DECISIONE 1h e' fermo da >= N barre di close # sul testnet), lo lascio attivo come rete di sicurezza (non scattera' su BTC/ETH
# INVARIATA (guasto testnet: ETH-PERPETUAL a 1661.95 per 36h+ -> z-score pairs # liquidi). 0 = disattivo.
# 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.
feed_freeze_gate_bars: 24 feed_freeze_gate_bars: 24
# SLEEVE PAPER (2026-06-08): fuori dal pool/pesi/ledger i €2000 si dividono SOLO # SLEEVE PAPER (fuori dal pool/pesi/ledger): i €500 si dividono SOLO tra i 7 sleeve
# tra i 14 sleeve REALI (fade+DIP01+pairs+SH01). I multi-asset (no esecuzione reale, # REALI (6 fade + DIP01). Pairs (PR01, 2 gambe) e SH01 fuori dalla Fase 1: a €500 il
# bloccati dal capitale) girano in data/portfolio_paper_stats/ con capitale nozionale # rumore di arrotondamento li soffoca (pairs ~30%/gamba; servono ~€5-8k) e aggiungono
# fisso, SOLO per statistica in vista di future implementazioni reali. NB: il portafoglio # superfici d'errore (leg-risk). Multi-asset (TR01/ROT02/TSM01/XS01) paper come sempre.
# live diverge ora dal PORT06 canonico (17 sleeve) -> DD reale ~5.35% vs 3.96% validato: paper_sleeves: [TR01, ROT02, TSM01, XS01, PR01, SH01]
# il prezzo di vedere il risultato reale puro (scelta utente). # Frazione di capitale-sleeve per posizione (0.5 con leva 3 = 1.5x la fetta impegnata).
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).
position_size: 0.5 position_size: 0.5
# Override per-famiglia (chiave = weighting.family_of). PAIRS: famiglia SENZA # Override per-famiglia: irrilevante per il conto reale (i pairs sono PAPER), tenuto
# stop, validata a esposizione 0.45 (pos 0.15 lev 3) — il gate 2026-06-07 # solo perche' i worker pairs in sola-statistica dimensionino come da gate storico.
# (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.
position_size_family: {PAIRS: 0.13} position_size_family: {PAIRS: 0.13}
# Esecuzione REALE su Deribit testnet, in SHADOW (sim + reale in parallelo). # Esecuzione REALE su Deribit MAINNET. Solo i 7 single-leg con TP/SL in metadata:
# I 7 single-leg con TP/SL in metadata: 6 fade (MR01/MR02/MR07 x BTC/ETH) + # 6 fade (MR01/MR02/MR07 x BTC/ETH 15m) + DIP01 (BTC 1h). Ordini sui LINEARI USDC
# DIP01 BTC (attivato 2026-06-04: stesso wiring StrategyWorker, TP limit resting # (payoff lineare = matematica del backtest; fee/PnL in USDC).
# 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).
execution: execution:
enabled: true enabled: true
# SH01 aggiunto 2026-06-08: e' il diversificatore piu' decorrelato (corr 0.07, # SOLO fade + DIP01 in Fase 1 (SH01 e pairs -> paper, vedi paper_sleeves).
# senza di lui il DD del portafoglio sale 3.96->5.35%). Single-leg, esce a sleeves: [MR01, MR02, MR07, DIP01]
# 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]
instruments: instruments:
BTC: BTC_USDC-PERPETUAL BTC: BTC_USDC-PERPETUAL
ETH: ETH_USDC-PERPETUAL ETH: ETH_USDC-PERPETUAL
LTC: LTC_USDC-PERPETUAL # niente esecuzione a 2 gambe in Fase 1 (pairs sono paper).
ADA: ADA_USDC-PERPETUAL pairs_enabled: false
SOL: SOL_USDC-PERPETUAL # Disaster-bracket on-book (~-30%) a ogni apertura: assicurazione per gli outage
# Esecuzione REALE a 2 gambe per i pairs (PairsExecutionClient, 2026-06-08). # del runner. In operativita' normale non scatta mai -> 0 costo. 0 = disattivo.
# 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.
disaster_sl_pct: 0.30 disaster_sl_pct: 0.30
# REAL-TRUTH (2026-06-10, scelta utente): il ledger `capital` degli sleeve # REAL-TRUTH: equity/ribilanci/sizing derivano dai FILL REALI (fee reali incluse),
# eseguiti si aggiorna col PnL dei FILL REALI (fee reali incluse) — il sim # non dal sim. Il sim resta solo diagnostica nel log CLOSE.
# 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: true real_truth: true
+79
View File
@@ -23,6 +23,7 @@ from __future__ import annotations
import json import json
import sys import sys
import time import time
from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2] 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); STALE_REAL_MIN = 15 # un worker vivo aggiorna status.json ~ogni poll (60s);
# real_in_position con mtime oltre questo = NON gestito # 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]: def compute_drift(client: CerberoClient | None = None) -> list[dict]:
client = client or CerberoClient() client = client or CerberoClient()
@@ -139,11 +148,55 @@ def compute_stale_real_positions(max_age_min: int = STALE_REAL_MIN) -> list[dict
return out 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(): def main():
client = CerberoClient() client = CerberoClient()
rows = compute_drift(client) rows = compute_drift(client)
resting = compute_resting_drift(client) resting = compute_resting_drift(client)
stale = compute_stale_real_positions() 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 = [r for r in rows if not r["ok"]]
bad_rest = [r for r in resting if r["status"] != "OK"] bad_rest = [r for r in resting if r["status"] != "OK"]
if bad or bad_rest: if bad or bad_rest:
@@ -176,6 +229,17 @@ def main():
if not stale: if not stale:
print("(nessuna posizione reale stantia)") 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) 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)})") 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; " "(caso MR02_BTC 2026-06-12); MISSING = atteso ma non in book; "
"STALE = in book senza libro corrispondente")}) "STALE = in book senza libro corrispondente")})
print("[telegram] alert RESTING_DRIFT inviato") 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) return bool(bad or bad_rest or stale)