3 Commits

Author SHA1 Message Date
Adriano Dal Pastro 4650aa71a2 feat(live): ARMA l'esecuzione di TP01 (execution_enabled=true) + cabla al cron giornaliero
execution_enabled=true: con --execute il loop invia ordini REALI. Aggiunto al cron_daily.sh (00:30
UTC, dopo il refresh dati) lo step live_execute.py --execute. Validato armato: TP01 flat -> HOLD,
zero ordini. Da qui TP01 opera da solo sul conto reale al prossimo ENTRY del segnale.

NB: il loop NON piazza ancora il disaster-SL on-book (metodo presente, lifecycle bracket da cablare
prima del primo ENTRY). Rischio posizione comunque limitato dal cap $300/asset (~1x, no leva).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:41:03 +00:00
Adriano Dal Pastro bc9e322d0d 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>
2026-06-20 15:37:51 +00:00
Adriano Dal Pastro a3d6b97db6 fix(dashboard): sezioni PAPER/LIVE evidenti (barra colorata) + Cache-Control no-cache
Le sezioni erano testo grigio poco visibile e il browser cacheava la pagina ('non vedo differenza').
Ora: header PAPER con barra verde, LIVE con barra rossa + sfondo rosso-tenue (separazione netta);
risposta HTTP con Cache-Control no-cache/no-store -> niente pagina stantia.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:36:21 +00:00
5 changed files with 152 additions and 4 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"_nota": "Config esecuzione LIVE di TP01. execution_enabled=true + --execute -> ordini REALI. ARMATO 2026-06-20.",
"execution_enabled": true,
"max_notional_per_asset_usd": 300,
"min_order_usd": 5,
"disaster_sl_pct": 0.30
}
+1
View File
@@ -9,5 +9,6 @@ mkdir -p logs
uv run python scripts/analysis/fetch_hyperliquid.py # 52 alt Hyperliquid (certify) uv run python scripts/analysis/fetch_hyperliquid.py # 52 alt Hyperliquid (certify)
uv run python scripts/research/fetch_dvol.py # DVOL (per ricerca opzioni) 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_portfolio.py # avanza paper TP01+XS01
uv run python scripts/live/live_execute.py --execute # TP01 LIVE su Deribit (gated da config/live.json)
echo "===== done $(date -u '+%H:%M:%SZ') =====" echo "===== done $(date -u '+%H:%M:%SZ') ====="
} >> logs/cron_daily.log 2>&1 } >> logs/cron_daily.log 2>&1
+114
View File
@@ -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()
+3 -2
View File
@@ -132,8 +132,8 @@ table{{width:100%;border-collapse:collapse;margin:8px 0 20px}}td,th{{text-align:
th{{color:#8a93a0;font-weight:500}}.y{{display:inline-block;background:#161b22;border:1px solid #222b36;border-radius:6px;padding:3px 8px;margin:2px;font-size:12px}} th{{color:#8a93a0;font-weight:500}}.y{{display:inline-block;background:#161b22;border:1px solid #222b36;border-radius:6px;padding:3px 8px;margin:2px;font-size:12px}}
.box{{background:#161b22;border:1px solid #222b36;border-radius:10px;padding:14px 18px;margin-bottom:18px}} .box{{background:#161b22;border:1px solid #222b36;border-radius:10px;padding:14px 18px;margin-bottom:18px}}
.warn{{color:#f1c40f;font-size:12px}} .warn{{color:#f1c40f;font-size:12px}}
.section{{font-size:13px;font-weight:600;letter-spacing:.05em;margin:30px 0 12px;padding-bottom:7px;border-bottom:1px solid #222b36;color:#8a93a0}} .section{{font-size:15px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;margin:34px 0 14px;padding:10px 14px;border-radius:9px;background:#12181f;border-left:5px solid #2ecc71;color:#d7dee6}}
.section.live{{color:#e74c3c;border-color:#3a2329}}</style></head><body> .section.live{{border-left-color:#e74c3c;background:#1c1316;color:#f0c4c4}}</style></head><body>
<h1>PythagorasGoal — Portafoglio attivo (TP01 + XS01 + VRP01)</h1> <h1>PythagorasGoal — Portafoglio attivo (TP01 + XS01 + VRP01)</h1>
<div class=sub>monitor · v{d['version']} · ultimo dato {d['last_data']} · esecuzione REALE non attiva (solo micro-test)</div> <div class=sub>monitor · v{d['version']} · ultimo dato {d['last_data']} · esecuzione REALE non attiva (solo micro-test)</div>
<div class="section">PAPER — simulato (backtest + forward virtuale)</div> <div class="section">PAPER — simulato (backtest + forward virtuale)</div>
@@ -174,6 +174,7 @@ class H(BaseHTTPRequestHandler):
body = f"<pre>errore: {type(e).__name__}: {e}</pre>".encode() body = f"<pre>errore: {type(e).__name__}: {e}</pre>".encode()
self.send_response(200) self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
self.end_headers(); self.wfile.write(body) self.end_headers(); self.wfile.write(body)
+27 -2
View File
@@ -17,9 +17,11 @@ from dataclasses import dataclass
from src.live.deribit import DeribitRead, notional_to_amount, quantize_price 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"} 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 FLAT_USD = 1.0 # |notional| < $1 = posizione considerata flat
@@ -115,6 +117,29 @@ class DeribitTrader(DeribitRead):
side = "sell" if pos_usd > 0 else "buy" side = "sell" if pos_usd > 0 else "buy"
return self._submit(instrument, side, amount, reduce_only=True, label=label) 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) --- # --- DISASTER BRACKET (assicurazione on-book per outage; da Old) ---
def place_disaster_sl(self, instrument: str, side_held: str, amount: float, def place_disaster_sl(self, instrument: str, side_held: str, amount: float,
stop_price: float, label: str = "disaster-sl") -> Fill: stop_price: float, label: str = "disaster-sl") -> Fill: