82c05f6f81
7 finder paralleli sul diff della giornata (8adf388..HEAD), fix dei confermati: CRITICI (money-path): - close_amount: GUARD stato-stantio sul fallback netting — il residuo non-reduce-only e' consentito solo fino al gap (conto reale - libri degli altri worker - orfani) nella direzione della chiusura (src/live/books.py = fonte unica, usata anche dal reconciler). Senza guard, un close su stato stantio (DSL scattato in outage, flatten manuale) APRIVA una posizione nuda a taglia piena bookata come 'chiusura verificata'. Fail-safe se il gap non e' calcolabile. Check polvere PRIMA di _quantize_step (che clampa al lotto minimo: un residuo 1e-17 diventava un ordine nudo da un lotto). - _real_close: market_amt = filled_amount anche a merged verified=False (i contratti chiusi dal reduce-only non si buttano se il leg netting fallisce); REAL_CLOSE_PARTIAL non piu' gateato su verified (era soppresso proprio nel caso parziale reale). - pairs: verita' per-FRAZIONE di gamba (gross proporzionale al fillato, orfano = solo il residuo — prima falsava reconciler e real_capital della parte gia' chiusa); REAL_OPEN_PAIR booka filled_amount; docstring applied corretta. - open_pair unwind: chiude il FILLATO, non il richiesto (senza il cap silenzioso del reduce-only avrebbe mangiato quota altrui). - place_tp_limit: quantize CONSERVATIVO (sell=floor/buy=ceil) — il rounding al tick piu' vicino poteva mettere il resting oltre il TP sim -> tocco genuino classificato phantom sistematicamente. ROBUSTEZZA/OSSERVABILITA': - runner: WORKER_ERROR_STREAK a 5 e poi ogni 50 poll + recovery RIPRESO + flag real_in_position nell'alert (prima: one-shot a n==5, poi silenzio). - _tp_phantom: TTL 120s sul verdetto (era ~50 HTTP/h per worker a barra fantasma); merge notes con entrambi gli order_id (audit-trail). - reset_flatten: _quantize_step Decimal (round float produceva amount che Deribit rifiuta); hourly_report: book XS01 con gambe SHORT visibili (abs). - validate_xsec_worker: POS/LEV importati (non hardcoded); xs01_tranche: regression-lock ESEGUITO vs xsec_sim; reconcile su src/live/books; drift_monitor: rolling vettoriale + exit code 1 su warn. Test: +4 guard/dust, fixture filled_amount -> 114 passed. Deferiti (TODO): resting esposti al netting, lifecycle orphan_legs, finestra trade-history TP_PHANTOM, validazione feed a monte, dedup minori. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
87 lines
3.7 KiB
Python
87 lines
3.7 KiB
Python
"""Valida il CrossSectionalWorker: replay bar-per-bar == backtest XS01.xsec_sim?
|
|
|
|
Come validate_worker_pairs: alimenta il worker con finestre trailing crescenti del
|
|
pannello 8-asset e confronta capitale finale e n.trade col backtest di riferimento
|
|
scripts.strategies.XS01_cross_sectional.xsec_sim. Se combaciano, la semantica live e' fedele.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
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.xsec_worker import CrossSectionalWorker
|
|
from scripts.strategies.XS01_cross_sectional import aligned_panel, xsec_sim, UNIVERSE, LB, HOLD
|
|
|
|
|
|
def _replay(M, dfs, params):
|
|
"""Alimenta il worker bar-per-bar su finestre trailing; ritorna il worker."""
|
|
n = len(M)
|
|
tmp = Path(tempfile.mkdtemp(prefix="xsec_val_"))
|
|
try:
|
|
w = CrossSectionalWorker(UNIVERSE, tf="1h", params=params,
|
|
fee_rt=0.0005, data_dir=tmp)
|
|
w._save = lambda: None; w._log = lambda *a, **k: None; w._notify = lambda *a, **k: None
|
|
window = LB + 6
|
|
for k in range(LB + 1, n + 1): # prima finestra = lb+1 barre -> ingresso al bar lb
|
|
lo = max(0, k - window)
|
|
w.tick({a: dfs[a].iloc[lo:k] for a in UNIVERSE})
|
|
return w
|
|
finally:
|
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
|
|
|
|
def main():
|
|
print("=" * 88)
|
|
print(" VALIDAZIONE CrossSectionalWorker — replay live vs backtest xsec_sim (fee 0.10% RT/book)")
|
|
print("=" * 88)
|
|
M = aligned_panel(UNIVERSE)
|
|
dfs = {a: pd.DataFrame({"timestamp": M.index.values, "close": M[a].values}) for a in UNIVERSE}
|
|
|
|
# [1] K=1: parita' storica col backtest canonico
|
|
w = _replay(M, dfs, {"lb": LB, "hold": HOLD})
|
|
bt = xsec_sim(UNIVERSE)
|
|
bt_cap = 1000.0 * (1 + bt["ret"] / 100)
|
|
cap_ok = abs(w.capital - bt_cap) / bt_cap < 0.02 if bt_cap else False
|
|
trd_ok = abs(w.total_trades - bt["trades"]) <= max(2, bt["trades"] * 0.02)
|
|
ww = w.total_wins / w.total_trades * 100 if w.total_trades else 0
|
|
print(f"\n [K=1] {'':<6}{'cap':>14}{'trades':>8}{'win%':>7}")
|
|
print(f" WORKER{w.capital:>14.0f}{w.total_trades:>8d}{ww:>7.1f}")
|
|
print(f" BCKTST{bt_cap:>14.0f}{bt['trades']:>8d}{bt['win']:>7.1f}")
|
|
ok1 = cap_ok and trd_ok
|
|
print(f" ESITO: {'OK (replay == backtest)' if ok1 else 'DIFF -> INDAGARE'}")
|
|
|
|
# [2] K=3 (tranching live): parita' con l'unione delle fasi 0,4,8 (gate
|
|
# xs01_tranche_gate) — capitale comune, PnL/K per trade in ordine di exit.
|
|
# POS/LEV importati dal modulo canonico, NON hardcoded: una costante stantia
|
|
# qui farebbe passare la validazione contro un sistema diverso (code-review)
|
|
from scripts.analysis.xs01_tranche_research import xsec_trades
|
|
from scripts.strategies.XS01_cross_sectional import POS, LEV
|
|
K = 3
|
|
w3 = _replay(M, dfs, {"lb": LB, "hold": HOLD, "tranches": K})
|
|
step = HOLD // K
|
|
allt = sorted([t for j in range(K) for t in xsec_trades(phase=j * step, M=M)],
|
|
key=lambda t: t[1])
|
|
cap = 1000.0
|
|
for _, _, net in allt:
|
|
cap = max(cap + cap * POS * LEV * net / K, 10.0)
|
|
cap_ok3 = abs(w3.capital - cap) / cap < 0.02
|
|
trd_ok3 = abs(w3.total_trades - len(allt)) <= max(2, len(allt) * 0.02)
|
|
print(f"\n [K=3] {'':<6}{'cap':>14}{'trades':>8}")
|
|
print(f" WORKER{w3.capital:>14.0f}{w3.total_trades:>8d}")
|
|
print(f" BCKTST{cap:>14.0f}{len(allt):>8d}")
|
|
ok3 = cap_ok3 and trd_ok3
|
|
print(f" ESITO: {'OK (replay == unione fasi)' if ok3 else 'DIFF -> INDAGARE'}")
|
|
print(f"\n ESITO COMPLESSIVO: {'OK' if (ok1 and ok3) else 'DIFF -> INDAGARE'}")
|
|
print(" (diff minime attese da bar finale aperta / troncamento)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|