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:
@@ -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