From bc9e322d0d7a62090f4882dd95bcca69dbe99d27 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 20 Jun 2026 15:37:51 +0000 Subject: [PATCH] feat(live): loop di esecuzione GATED di TP01 (execution_enabled + --execute, default OFF) scripts/live/live_execute.py porta il conto reale al target di TP01 (min(0.5*frazione*equity, cap/asset)): apre/riduce/chiude via DeribitTrader.rebalance_to(). DOPPIO GATE: config/live.json execution_enabled=true (master, default false) E flag --execute; senza entrambi e' dry-run. Reconciliation post-ordine + log in data/live/executions.jsonl. TP01 flat -> 0 azioni. - execution.py: rebalance_to() (open/reduce/close al target); MAX_AMOUNT alzato a tetto hard anti-fat-finger (~$630/$430 su conto ~$600), il sizing operativo lo decide config max_notional. - config/live.json: master switch + cap/asset $300 + min ordine $5 + disaster_sl_pct. Co-Authored-By: Claude Opus 4.8 (1M context) --- config/live.json | 7 +++ scripts/live/live_execute.py | 114 +++++++++++++++++++++++++++++++++++ src/live/execution.py | 29 ++++++++- 3 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 config/live.json create mode 100644 scripts/live/live_execute.py diff --git a/config/live.json b/config/live.json new file mode 100644 index 0000000..460ab04 --- /dev/null +++ b/config/live.json @@ -0,0 +1,7 @@ +{ + "_nota": "Config esecuzione LIVE di TP01. execution_enabled=false -> SOLO shadow (nessun ordine). Master switch.", + "execution_enabled": false, + "max_notional_per_asset_usd": 300, + "min_order_usd": 5, + "disaster_sl_pct": 0.30 +} diff --git a/scripts/live/live_execute.py b/scripts/live/live_execute.py new file mode 100644 index 0000000..9bf7944 --- /dev/null +++ b/scripts/live/live_execute.py @@ -0,0 +1,114 @@ +"""TP01 LIVE EXECUTE — loop di esecuzione GATED su Deribit mainnet (USDC linear). + +Porta il conto reale al target di TP01 (causale, dati certificati): per ogni asset calcola il notional +bersaglio = min(0.5 * frazione * equity, max_notional), e apre/riduce/chiude per raggiungerlo. + +DOPPIO GATE DI SICUREZZA (entrambi necessari per inviare ordini reali): + 1. config/live.json -> "execution_enabled": true (master switch, default false) + 2. flag CLI --execute +Senza entrambi e' un DRY-RUN (stampa il piano, NON invia). Reconciliation dopo ogni ordine; log in +data/live/executions.jsonl. TP01 oggi e' FLAT -> target 0 -> nessuna azione finche' il segnale non gira. + + uv run python scripts/live/live_execute.py # DRY-RUN (piano, nessun ordine) + uv run python scripts/live/live_execute.py --execute # esegue SOLO se execution_enabled=true +""" +from __future__ import annotations + +import json +import sys +from pathlib import Path + +import pandas as pd + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT)) + +from src.live.deribit import INSTRUMENT +from src.live.execution import DeribitTrader +from src.live.shadow import ASSETS, WEIGHT, shadow_report + +CONFIG = PROJECT_ROOT / "config" / "live.json" +LOG_DIR = PROJECT_ROOT / "data" / "live" +LOG = LOG_DIR / "executions.jsonl" + + +def load_config() -> dict: + cfg = json.loads(CONFIG.read_text()) if CONFIG.exists() else {} + cfg.setdefault("execution_enabled", False) + cfg.setdefault("max_notional_per_asset_usd", 300.0) + cfg.setdefault("min_order_usd", 5.0) + return cfg + + +def log_event(rec: dict): + LOG_DIR.mkdir(parents=True, exist_ok=True) + with open(LOG, "a") as f: + f.write(json.dumps(rec) + "\n") + + +def main(): + cfg = load_config() + want_execute = "--execute" in sys.argv[1:] + enabled = bool(cfg["execution_enabled"]) + do_execute = want_execute and enabled + max_notional = float(cfg["max_notional_per_asset_usd"]) + min_order = float(cfg["min_order_usd"]) + + r = shadow_report() # targets causali + conto/posizioni reali (online) + equity = r["equity"] + + print("=" * 84) + print(" TP01 LIVE EXECUTE — Deribit mainnet (USDC linear)") + print("=" * 84) + mode = ("ESECUZIONE REALE" if do_execute else + ("ARMATO ma manca --execute" if enabled else "DRY-RUN (execution_enabled=false)")) + print(f" modo : {mode}") + print(f" gate : execution_enabled={enabled} | --execute={want_execute}") + print(f" conto reale : ${r['real_equity']:,.2f}" if r["real_equity"] else f" conto: {r['eq_basis']}") + print(f" sizing base : ${equity:,.2f} | cap/asset ${max_notional:.0f} | min ordine ${min_order:.0f}") + print(f" ultima barra : {r['last_data']}\n") + + if not r["online"]: + print(" conto non leggibile (offline) -> stop, non eseguo a cieco.") + return + + trader = DeribitTrader() if do_execute else None + actions = [] + for a in r["assets"]: + asset = a["asset"]; frac = a["target"]; mark = a["mark"]; cur = a["position_usd"] + tgt = min(WEIGHT * frac * equity, max_notional) if frac > 0 else 0.0 + delta = tgt - cur + if abs(delta) < min_order: + act = "HOLD (a target)" + elif tgt < 1.0 and cur > 1.0: + act = f"CLOSE ${cur:,.0f}" + elif delta > 0: + act = f"BUY ${delta:,.0f}" + else: + act = f"REDUCE ${-delta:,.0f}" + print(f" {asset:<3} target {frac:+.3f}x -> ${tgt:,.0f} | pos ${cur:,.0f} | delta ${delta:+,.0f} -> {act}") + + if do_execute and not act.startswith("HOLD"): + fills = trader.rebalance_to(INSTRUMENT[asset], tgt, mark, min_usd=min_order) + newpos = trader.position_usd(INSTRUMENT[asset]) + for f in fills: + print(f" -> {f.side.upper()} {f.filled:.4f} @ ${f.price:,.1f} fee {f.fee_usdc:.5f} " + f"({'OK' if f.verified else 'NON VERIFICATO: ' + f.notes})") + log_event(dict(ts_utc=str(pd.Timestamp(r['last_data'])), asset=asset, action=act, + side=f.side, filled=f.filled, price=f.price, fee=f.fee_usdc, + verified=f.verified, notes=f.notes, pos_after=newpos)) + print(f" reconcile: pos ${newpos:,.0f}") + actions.append(act) + + print() + if not do_execute: + print(" => DRY-RUN: nessun ordine inviato." + + ("" if enabled else " Per armare: config/live.json execution_enabled=true + --execute.")) + elif all(x.startswith("HOLD") for x in actions): + print(" => Nessuna azione: conto gia' al target di TP01 (oggi flat).") + else: + print(" => Esecuzione completata (vedi data/live/executions.jsonl).") + + +if __name__ == "__main__": + main() diff --git a/src/live/execution.py b/src/live/execution.py index db2bf14..02eac0d 100644 --- a/src/live/execution.py +++ b/src/live/execution.py @@ -17,9 +17,11 @@ from dataclasses import dataclass from src.live.deribit import DeribitRead, notional_to_amount, quantize_price -# Conto USDC -> perp LINEARE USDC (amount in base-coin). Cap micro-test: ~$13. +# Conto USDC -> perp LINEARE USDC (amount in base-coin). MAX_AMOUNT = TETTO HARD anti-fat-finger +# (~$630/$430 su un conto ~$600): backstop sopra il sizing di TP01, non il sizing operativo (quello +# lo decide config/live.json max_notional_per_asset_usd). Il micro-test invia comunque size fissa minima. ALLOWED = {"BTC_USDC-PERPETUAL", "ETH_USDC-PERPETUAL"} -MAX_AMOUNT = {"BTC_USDC-PERPETUAL": 0.0002, "ETH_USDC-PERPETUAL": 0.005} +MAX_AMOUNT = {"BTC_USDC-PERPETUAL": 0.01, "ETH_USDC-PERPETUAL": 0.25} FLAT_USD = 1.0 # |notional| < $1 = posizione considerata flat @@ -115,6 +117,29 @@ class DeribitTrader(DeribitRead): side = "sell" if pos_usd > 0 else "buy" return self._submit(instrument, side, amount, reduce_only=True, label=label) + # --- RIBILANCIO al target (long-only TP01): apre / riduce / chiude --- + def rebalance_to(self, instrument: str, target_notional_usd: float, mark: float, + min_usd: float = 5.0) -> list[Fill]: + """Porta la posizione su `instrument` al target (USD notional). Long-only: target>=0. + - delta < min_usd -> niente (gia' a target); + - target ~0 & posizione -> close() (uscita piena, reduce_only); + - delta > 0 -> open buy (aumenta); + - delta < 0 (resta long) -> sell reduce_only del delta (riduce). + Ritorna i Fill eseguiti.""" + cur = self.position_usd(instrument) + delta = target_notional_usd - cur + if abs(delta) < min_usd: + return [] + if target_notional_usd < FLAT_USD and cur > FLAT_USD: + f = self.close(instrument, label="tp01-exit") + return [f] if f else [] + amount = notional_to_amount(instrument, abs(delta), price=mark) + if amount <= 0: + return [] + if delta > 0: + return [self.open(instrument, "buy", amount, label="tp01-buy")] + return [self._submit(instrument, "sell", amount, reduce_only=True, label="tp01-reduce")] + # --- DISASTER BRACKET (assicurazione on-book per outage; da Old) --- def place_disaster_sl(self, instrument: str, side_held: str, amount: float, stop_price: float, label: str = "disaster-sl") -> Fill: