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) <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user