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