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
+79
View File
@@ -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)