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:
@@ -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/
|
||||
|
||||
@@ -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:
|
||||
|
||||
+40
-62
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user