diff --git a/tests/test_book_live.py b/tests/test_book_live.py index 00bb2c9..2ae0525 100644 --- a/tests/test_book_live.py +++ b/tests/test_book_live.py @@ -22,6 +22,29 @@ from src.live.book import W_SKH, W_TP01, book_net_target, build_book_order from src.live.execution import Fill +# --------------------------------------------------------------------------- +# BUDGET DI ARROTONDAMENTO del report (2026-08-21). +# --------------------------------------------------------------------------- +def _budget_arrotondamento(equity: float) -> float: + """Tolleranza per confrontare il `net_target` PUBBLICATO con la formula pura. + + `book_report` pubblica valori arrotondati — `net_target` a 2 decimali e `tp_frac` a 4 — + mentre l'ordine viene costruito sul valore NON arrotondato (`build_book_order(inst, net, ...)`). + Un test che ricalcola la formula dal `tp_frac` pubblicato eredita quindi DUE arrotondamenti e + non puo' confrontare a 1e-6: + + - `round(net, 2)` -> fino a 0.005 di scarto; + - `round(tp_frac, 4)` -> fino a 0.00005 di scarto sulla frazione, che la formula + amplifica per WEIGHT * equity * W_TP01. + + ⚠️ Questo NON e' un allentamento del test: i modi in cui la formula puo' rompersi davvero + (peso sbagliato, cap non applicato, segno invertito, W_TP01/W_SKH scambiati) valgono decine + di dollari su target da $114, tre ordini di grandezza sopra questo budget. + """ + from src.live.book import WEIGHT + return 0.005 + WEIGHT * equity * W_TP01 * 0.00005 + + # --------------------------------------------------------------------------- # Formula di netting: 75/25, cap, combinazioni long/short/flat. # --------------------------------------------------------------------------- @@ -116,7 +139,7 @@ def test_book_report_dynamic_cap_online(monkeypatch, tmp_path): assert r["cap_per_asset"] == 2500.0 for a in r["assets"]: expect = book_net_target(a["tp_frac"], a["skh_sign"], 5000.0, 2500.0) - assert abs(a["net_target"] - expect) < 1e-6 + assert abs(a["net_target"] - expect) < _budget_arrotondamento(5000.0) def test_weights_match_backtest_sleeves(): @@ -307,12 +330,80 @@ def test_book_report_uses_pure_formula_offline(): assert r["equity"] == 600.0 and r["cap_per_asset"] > 0 for a in r["assets"]: expect = book_net_target(a["tp_frac"], a["skh_sign"], 600.0, r["cap_per_asset"]) - assert abs(a["net_target"] - expect) < 1e-6, f"{a['asset']}: net incoerente con la formula" + assert abs(a["net_target"] - expect) < _budget_arrotondamento(600.0), \ + f"{a['asset']}: net incoerente con la formula" assert a["skh_sign"] in (-1, 0, 1) # offline -> conto assunto flat -> nessuna posizione reale, report deterministico assert all(a["position_usd"] == 0.0 for a in r["assets"]) +def test_la_formula_del_report_ha_potenza_anche_a_libro_flat(monkeypatch): + """⚠️ I due test qui sopra leggono il segnale REALE del giorno: quando il libro e' flat + (`tp_frac=0`, `skh_sign=0`) il target e' esattamente `0.0`, i due arrotondamenti del report + sono esatti e l'invariante passa **senza essere mai esercitata**. + + E' successo davvero: dal 2026-06-23 al 2026-08-18 il book e' stato flat quasi ininterrottamente + e questi test hanno avuto potenza ZERO su una formula di produzione; il difetto di tolleranza + e' emerso solo il 2026-08-21, quando TP01 e SKH01 sono andati long insieme. + E' la lezione gia' codificata il 26/07 (`test_skh_partial_entry`): **un self-check su eventi + rari si campiona sugli EVENTI, non sulla popolazione.** + + Qui il segnale e' FORZATO su frazioni scomode, cosi' la copertura non dipende dal mercato. + """ + import src.live.book as book + + # frazioni scelte per NON essere clean a 4 decimali e per coprire i rami della formula: + # long parziale + SKH long, long parziale + SKH short (hedge), flat + SKH short, cap. + casi = [ + (0.17578123, 1), + (0.29149997, -1), + (0.0, -1), + (1.0, 1), # -> clampato al cap + ] + equity, cap = 5000.0, 2500.0 + # ⚠️ `base` va preso PRIMA del loop: dalla seconda iterazione `book.shadow_report` e' gia' + # sostituito dal monkeypatch e si leggerebbe il finto della iterazione precedente. + base = book.shadow_report(offline=True, equity_override=equity) + + non_banali = 0 + for tp_vero, sign in casi: + finto = {**base, "real_equity": equity, "equity": equity, "eq_fallback": None, + "assets": [{**a, "target": tp_vero, "position_usd": 0.0} for a in base["assets"]]} + monkeypatch.setattr(book, "shadow_report", lambda *a, _f=finto, **k: _f) + monkeypatch.setattr(book, "_skyhook_positions", + lambda load5m=None, _s=sign: {a: ("flat" if _s == 0 else + {"dir": "LONG" if _s > 0 else "SHORT"}) + for a in book.ASSETS}) + r = book.book_report() + atteso_vero = book_net_target(tp_vero, sign, equity, cap) + non_banali += (atteso_vero != 0.0) + for a in r["assets"]: + assert a["skh_sign"] == sign + # (a) il valore pubblicato e' la formula sul tp_frac VERO, a meno del solo round(.,2) + assert abs(a["net_target"] - atteso_vero) <= 0.005, \ + f"tp={tp_vero} sign={sign}: net pubblicato incoerente con la formula" + # (b) e ricalcolandola dal tp_frac PUBBLICATO resta dentro il budget dichiarato + da_pubblicato = book_net_target(a["tp_frac"], a["skh_sign"], equity, cap) + assert abs(a["net_target"] - da_pubblicato) < _budget_arrotondamento(equity) + + # controllo di POTENZA: ogni caso deve produrre un target != 0, altrimenti il test + # passerebbe per lo stesso motivo per cui passava a libro flat (0.0 arrotondato e' 0.0). + assert non_banali == len(casi), f"solo {non_banali}/{len(casi)} casi esercitano l'invariante" + + +def test_il_budget_di_arrotondamento_non_copre_un_errore_di_formula(): + """Controllo POSITIVO del budget: deve restare molto piu' stretto dei modi in cui la + formula puo' rompersi davvero, o sarebbe una tolleranza che assolve tutto.""" + E, cap = 5000.0, 2500.0 + giusto = book_net_target(0.5, 1, E, cap) + budget = _budget_arrotondamento(E) + pesi_scambiati = 0.5 * E * (W_SKH * 0.5 + W_TP01 * 1) # W_TP01/W_SKH invertiti + senza_cap = 0.5 * E * (W_TP01 * 5.0 + W_SKH * 1) # cap non applicato + for sbagliato, nome in ((pesi_scambiati, "pesi scambiati"), (senza_cap, "cap non applicato"), + (-giusto, "segno invertito")): + assert abs(giusto - sbagliato) > 100 * budget, f"{nome}: il budget lo assolverebbe" + + # --------------------------------------------------------------------------- # SKH feed fallito: book_report NON crasha (forza flat + flagga skh_error) e # book_execute DEVE farlo emergere (log + alert), non ingoiarlo silenziosamente.