feat(live): arma il BOOK DERIBIT (TP01+SKH01 nettati in software)
Estende l'esecuzione live da TP01-only al book Deribit-only completo. TP01 e SKH01 tradano lo STESSO strumento (una sola posizione netta per conto su Deribit) -> netting in software: un solo ordine/asset verso il target netto. - src/live/book.py: target NETTO per asset = clamp(0.5*E*(0.75*tp_frac + 0.25*skh_sign), ±cap). Riusa current_target (TP01, causale) + _skyhook_positions (segno L/S, book 230m) + conto reale. - src/live/execution.py: rebalance_signed() — reconcile CON SEGNO (long/short, flip via close+open, reduce reduce_only). La close resta sempre permessa (si esce da qualunque posizione). - src/live/livefeed.py: fresh_5m() — feed 5m certificato + coda recente EFFIMERA da Deribit pubblico (stesso simbolo inverse, in-memory, NON scrive su disco -> dati certificati intatti; fallback al certificato su errore). Solo SKH01 ne ha bisogno (e' a 230m); TP01 e' giornaliero. - scripts/live/book_execute.py: executor doppio-gate (config + --execute), disaster-SL on-book sulla posizione netta, log in data/live/book_executions.jsonl. Feed SKH fresco (live_feed=True). - scripts/cron_book.sh + crontab ORARIO: book idempotente ogni ora (riconcilia al netto corrente); rimossa la riga live_execute.py (TP01-only) dal cron daily per non far collidere i due. - config/live.json: ARMATO (execution_enabled=true). cap/asset $300 split 75/25, disaster-SL -30%. Tutto flat all'arming -> nessun ordine finche' un segnale non arma. - tests/test_book_live.py: 20 test (sizing 75/25, cap, flip/close/reduce, parita' pesi backtest, gate-safety, reconcile con trader fittizio, merge/dedup feed + fallback, loader onorato). 76/76 pass. CAVEAT: exit SKH SOFTWARE (latenza fino a fine barra 230m; solo disaster-SL on-book); TP01 scende a peso 0.75 (max $225/asset); SKH01 resta research-grade (equity daily-step, margine DD ETH sottile). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
# BOOK DERIBIT-ONLY (TP01+SKH01 nettati) — esecuzione LIVE a cadenza ORARIA. v2.0.0+.
|
||||
# SKH01 decide su griglia 230m -> serve girare piu' spesso del daily; orario IDEMPOTENTE:
|
||||
# riconcilia al target NETTO corrente (se non cambia nulla -> HOLD). Il feed 5m fresco per il
|
||||
# segnale SKH e' preso IN MEMORIA dentro book_execute (livefeed.fresh_5m): NON tocca i dati
|
||||
# certificati su disco. Esecuzione reale gated da config/live.json (execution_enabled) + --execute.
|
||||
export PATH="/home/adriano/.local/bin:$PATH"
|
||||
cd /opt/docker/PythagorasGoal || exit 1
|
||||
mkdir -p logs
|
||||
{
|
||||
echo "===== $(date -u '+%Y-%m-%dT%H:%M:%SZ') cron_book ====="
|
||||
uv run python scripts/live/book_execute.py --execute
|
||||
echo "===== done $(date -u '+%H:%M:%SZ') ====="
|
||||
} >> logs/cron_book.log 2>&1
|
||||
@@ -10,7 +10,9 @@ mkdir -p logs
|
||||
uv run python scripts/research/fetch_dvol.py # DVOL (per ricerca opzioni)
|
||||
uv run python scripts/live/paper_portfolio.py # avanza paper TP01+XS01
|
||||
uv run python scripts/live/paper_prevday.py # forward-monitor lead prevday-breakout (PAPER, non deploy)
|
||||
uv run python scripts/live/live_execute.py --execute # TP01 LIVE su Deribit (gated da config/live.json)
|
||||
# NB: l'esecuzione Deribit e' passata al BOOK (TP01+SKH01 nettati) via scripts/cron_book.sh a
|
||||
# cadenza ORARIA (SKH01 e' a 230m: il daily mancherebbe gli ingressi). live_execute.py
|
||||
# (TP01-only) NON va piu' eseguito qui, sennò i due farebbero a pugni sullo stesso strumento.
|
||||
# --- COMBO cross-venue (PAPER): refresh ETF IB (GTAA) + avanza paper TP01+GTAA ---
|
||||
docker compose up -d ib-gateway >/dev/null 2>&1 # gateway IB paper (idempotente)
|
||||
for i in $(seq 1 25); do (echo > /dev/tcp/127.0.0.1/4002) >/dev/null 2>&1 && break; sleep 6; done
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
"""BOOK DERIBIT-ONLY LIVE EXECUTE — TP01 + SKH01 NETTATI in software su un solo conto Deribit mainnet.
|
||||
|
||||
Porta il conto reale al target NETTO per asset (vedi src/live/book.py): per ogni asset combina la
|
||||
frazione long-flat di TP01 (peso 0.75) e il segno L/S di SKH01 (peso 0.25), e manda UN ordine con
|
||||
segno (long/short/flip) per raggiungerlo. Poi assicura un disaster-SL on-book sulla posizione NETTA.
|
||||
|
||||
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/book_executions.jsonl.
|
||||
|
||||
⚠️ CADENZA: SKH01 decide su griglia 230m -> questo script va lanciato ogni ~230 minuti con la feed
|
||||
fresca all'ultima barra chiusa (NON il cron giornaliero, che mancherebbe gli ingressi). Gli exit di
|
||||
SKH sono SOFTWARE (latenza fino a fine barra 230m); solo il disaster-SL (-30%) e' on-book.
|
||||
|
||||
uv run python scripts/live/book_execute.py # DRY-RUN (piano, nessun ordine)
|
||||
uv run python scripts/live/book_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.book import book_report
|
||||
from src.live.execution import DeribitTrader
|
||||
from src.live.notifier import notify
|
||||
|
||||
CONFIG = PROJECT_ROOT / "config" / "live.json"
|
||||
LOG_DIR = PROJECT_ROOT / "data" / "live"
|
||||
LOG = LOG_DIR / "book_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)
|
||||
cfg.setdefault("disaster_sl_pct", 0.30)
|
||||
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 _run():
|
||||
cfg = load_config()
|
||||
want_execute = "--execute" in sys.argv[1:]
|
||||
enabled = bool(cfg["execution_enabled"])
|
||||
do_execute = want_execute and enabled
|
||||
min_order = float(cfg["min_order_usd"])
|
||||
sl_pct = float(cfg["disaster_sl_pct"])
|
||||
|
||||
r = book_report(live_feed=True) # target NETTO + conto/posizioni reali (feed SKH fresco)
|
||||
equity = r["equity"]
|
||||
|
||||
print("=" * 88)
|
||||
print(" BOOK DERIBIT LIVE EXECUTE — TP01(0.75)+SKH01(0.25) NETTATI — Deribit mainnet (USDC linear)")
|
||||
print("=" * 88)
|
||||
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 ${r['cap_per_asset']:.0f} | min ${min_order:.0f} | disaster-SL -{sl_pct*100:.0f}%")
|
||||
print(f" ultima barra : {r['last_data']}\n")
|
||||
|
||||
if not r["online"]:
|
||||
print(" conto non leggibile (offline) -> stop, non eseguo a cieco.")
|
||||
if do_execute:
|
||||
notify("⚠️ BOOK LIVE — conto offline", {"nota": "salto l'esecuzione, non opero a cieco"})
|
||||
return
|
||||
|
||||
trader = DeribitTrader() if do_execute else None
|
||||
actions = []
|
||||
for a in r["assets"]:
|
||||
asset, inst = a["asset"], a["instrument"]
|
||||
net, cur, mark = a["net_target"], a["position_usd"], a["mark"]
|
||||
sk = a["skh_state"]
|
||||
sk_txt = "flat" if sk == "flat" else f"{sk['dir']}@{sk.get('entry')}"
|
||||
order = a["order"]
|
||||
if order is None:
|
||||
act = "HOLD (a target)"
|
||||
elif order.get("is_close"):
|
||||
act = f"CLOSE ${cur:,.0f}"
|
||||
elif order.get("needs_flip"):
|
||||
act = f"FLIP -> ${net:,.0f}"
|
||||
else:
|
||||
act = f"{order['side'].upper()} ${order['delta']:+,.0f}"
|
||||
print(f" {asset:<3} TP {a['tp_frac']:+.3f} · SKH {a['skh_sign']:+d}({sk_txt}) -> net ${net:+,.0f} "
|
||||
f"| pos ${cur:+,.0f} -> {act}")
|
||||
|
||||
if do_execute and order is not None:
|
||||
fills = trader.rebalance_signed(inst, net, mark, min_usd=min_order)
|
||||
newpos = trader.position_usd(inst)
|
||||
for f in fills:
|
||||
print(f" -> {f.side.upper()} {f.filled:.4f} @ ${f.price or 0:,.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, net_target=net, pos_after=newpos,
|
||||
tp_frac=a["tp_frac"], skh_sign=a["skh_sign"]))
|
||||
det = dict(asset=asset, side=f.side, amount=round(f.filled, 4), price=round(f.price or 0, 1),
|
||||
fee=round(f.fee_usdc, 5), net=round(net, 0), pos_after=round(newpos, 0))
|
||||
notify(f"✅ BOOK {act}" if f.verified else "⚠️ BOOK ORDINE NON VERIFICATO",
|
||||
det if f.verified else {**det, "notes": f.notes})
|
||||
print(f" reconcile: pos ${newpos:,.0f}")
|
||||
if do_execute:
|
||||
ds = trader.ensure_disaster_sl(inst, sl_pct) # bracket su posizione NETTA (adatta long/short)
|
||||
print(f" disaster-SL: {ds.get('state')}" + (f" @ ${ds['stop']:,.1f}" if ds.get("stop") else ""))
|
||||
if ds.get("state") == "placed":
|
||||
notify("🛡️ BOOK disaster-SL piazzato", {"asset": asset, "stop": round(ds.get("stop") or 0, 1),
|
||||
"amount": round(ds.get("amount") or 0, 4)})
|
||||
elif ds.get("state") == "place-failed":
|
||||
notify("⚠️ BOOK disaster-SL FALLITO", {"asset": asset, "notes": ds.get("notes")})
|
||||
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 netto del book.")
|
||||
else:
|
||||
print(" => Esecuzione completata (vedi data/live/book_executions.jsonl).")
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
_run()
|
||||
except Exception as e:
|
||||
notify("🛑 BOOK LIVE — ERRORE", {"error": f"{type(e).__name__}: {e}"})
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user