Files
PythagorasGoal/tests/test_gates_implausible_anchor.py
Adriano Dal Pastro 37be1df838 research: ondata 26/07-bis — TP01 su barra parziale + i 2 gate mai costruiti
0 sleeve nuovi. Book, pesi, cron, config INVARIATI.

T1 — anche TP01 legge una barra giornaliera PARZIALE nel live, ma non conta.
Stesso fatto strutturale di SKH01: resample_tf non scarta il giorno in corso e
current_target prende [-1]; il feed si ricostruisce alle 00:30 UTC, quindi per
tutta la giornata il book vede oggi come 1 barra oraria su 24 (verificato).
Il docstring "ultima barra CHIUSA" era falso: corretto.

Tre path a un grado di liberta' per volta, 24 ancore, differenze appaiate:
  barra parziale   ΔFULL -0.031 (pos 7/24)  ΔHOLD +0.118 (pos 19/24)
  ritardo 1h       ΔFULL -0.004 (pos 11/24 = moneta)
  leva LIVE/MODEL  1.004
-> trascurabile, nessun cambio al live.

All'ancora canonica sembra peggio del vero: a offset 0 la parziale costa -0.230
di hold-out, che e' il MINIMO della banda (mediana +0.118). Speculare alla
lezione del 26/07: li' l'ancora canonica nascondeva un vantaggio, qui inventa
un danno.

REGOLA: la parzialita' dell'ultima barra conta in proporzione a quanto il
segnale pesa la barra piu' recente. Donchian breakout su 230m (la barra corrente
E' il segnale) -> +0.38; TSMOM 30/90/180g -> ±0.03. Non si trasferisce.

T2 — implausible_sharpe e anchor_luck_band codificati in altlib (debito
raccomandato 3 volte e mai scritto), piu' anchor_luck_delta che codifica
l'errore di stamattina (mediana delle differenze appaiate, non differenza
delle mediane).

Il gate ha segnalato VRP01 e il difetto era MIO: perdite contate su tutte le
barre, ma VRP01 e' settimanale su griglia giornaliera (94.2% di zeri) -> "0.96%,
coda assente" su uno sleeve in produzione. Sulle barre ATTIVE e' 16.5%, la forma
giusta di un credit spread a rischio definito. La lezione era gia' cablata il
giorno prima nel monitor DVOLSPREAD ("barre attive, non giorni di calendario").

Applicazione retroattiva 7/7 tutti ok, con controlli positivi obbligatori
superati (firme CC01 e deep-OTM segnalate, rumore Sh 0.62 no).

Replica indipendente del finding d'ancora del 02/07: il gate applicato alla
cieca a TP01 ritrova canonica +0.237 = 92 pctl delle 24, mediana onesta +0.056,
fortuna +0.182 — contro mediana 0.04 misurata il 02/07 con implementazione
separata. L'hold-out onesto di TP01 e' ~+0.05, non 0.31.

NON fatto: il book ricalcolato sul path live, bloccato da incompatibilita' di
lenti (simulatore per-trade vs sleeve vol-targeted). Follow-up dichiarato.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 22:36:40 +00:00

173 lines
7.7 KiB
Python

"""Test dei due gate codificati il 2026-07-26-bis: `implausible_sharpe` e `anchor_luck_band`.
Entrambi erano DEBITO DICHIARATO del progetto (raccomandati piu' volte, mai scritti). Un gate e'
utile solo se ha **potenza** (segnala cio' che deve) E **specificita'** (non segnala il resto):
un gate che non scatta mai passerebbe inosservato per mesi sembrando una buona notizia. Quindi
qui si testano sempre le due facce.
Il caso piu' importante e' `test_sleeve_flat_la_maggior_parte_del_tempo_non_e_implausibile`:
la prima stesura del gate contava le perdite su TUTTE le barre e segnalava VRP01 (settimanale su
griglia giornaliera, 94% di zeri) — un falso positivo su uno sleeve IN PRODUZIONE.
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pytest
ROOT = Path(__file__).resolve().parents[1]
for p in (ROOT, ROOT / "scripts" / "research" / "alt"):
sys.path.insert(0, str(p))
pytest.importorskip("pandas")
import pandas as pd # noqa: E402
import altlib as al # noqa: E402
def _idx(n):
return pd.date_range("2020-01-01", periods=n, freq="1D", tz="UTC")
def _noise(n=900, mu=0.0006, sd=0.011, seed=1):
rng = np.random.default_rng(seed)
return pd.Series(rng.normal(mu, sd, n), index=_idx(n))
# --------------------------------------------------------------- implausible_sharpe: POTENZA
def test_firma_cc01_viene_segnalata():
"""CC01 (Sharpe modellato 11-13, maxDD 0.3%) passava OGNI gate del marginal scorer: e' il
punto cieco che questo gate esiste per coprire."""
rng = np.random.default_rng(0)
s = pd.Series(0.0006 + rng.normal(0, 0.00012, 900), index=_idx(900))
r = al.implausible_sharpe(s)
assert r["implausible"]
assert any("Sharpe" in x for x in r["reasons"])
def test_zero_perdite_su_molti_trade_attiva_la_regola_del_tre():
"""0/142 non significa 'senza rischio': lascia un tasso vero fino a ~3/142 = 2.1%, e su un
payoff deep-OTM quel residuo ribalta l'expectancy."""
s = pd.Series(np.abs(_noise(900, 0.0008, 0.0003, seed=2).values), index=_idx(900))
r = al.implausible_sharpe(s, n_trades=142, n_losing_trades=0)
assert r["implausible"]
assert r["loss_rate_ub95"] == pytest.approx(3.0 / 142)
assert any("regola del tre" in x for x in r["reasons"])
def test_calmar_assurdo_viene_segnalato_anche_a_sharpe_moderato():
"""Il modo di perdita puo' mancare senza che lo Sharpe esploda: un DD ~0 basta da solo."""
v = np.full(900, 0.0004)
v[::120] = -0.00005 # perdite presenti ma minuscole -> DD ~0
r = al.implausible_sharpe(pd.Series(v, index=_idx(900)))
assert r["implausible"]
assert any("Calmar" in x or "maxDD" in x for x in r["reasons"])
# --------------------------------------------------------------- implausible_sharpe: SPECIFICITA'
def test_rumore_a_sharpe_uno_non_viene_segnalato():
"""Specificita': senza questo, un gate sempre-acceso sarebbe inutile quanto uno sempre-spento."""
assert not al.implausible_sharpe(_noise())["implausible"]
def test_sleeve_flat_la_maggior_parte_del_tempo_non_e_implausibile():
"""IL falso positivo della prima stesura. VRP01 e' settimanale su griglia giornaliera: 94% di
zeri. Contando le perdite su TUTTE le barre esce 0.96% ('coda assente') invece del 16.5% reale
sulle barre ATTIVE -> il gate segnalava uno sleeve in PRODUZIONE."""
rng = np.random.default_rng(3)
v = np.zeros(900)
act = np.arange(0, 900, 7) # una barra attiva a settimana
v[act] = np.where(rng.random(len(act)) < 0.17,
-rng.uniform(0.02, 0.05, len(act)), # ~17% di settimane in perdita
rng.uniform(0.004, 0.012, len(act)))
r = al.implausible_sharpe(pd.Series(v, index=_idx(900)))
assert r["n_active"] == len(act)
assert 0.05 < r["loss_frac"] < 0.35, r["loss_frac"]
assert not r["implausible"], r["reasons"]
def test_poche_barre_attive_sospende_il_giudizio_invece_di_promuovere():
"""Con pochissime osservazioni attive la coda non e' 'assente', e' NON MISURABILE: il gate
deve bloccare, non assolvere."""
v = np.zeros(900)
v[::100] = 0.01
r = al.implausible_sharpe(pd.Series(v, index=_idx(900)))
assert r["implausible"]
assert any("non e' misurabile" in x for x in r["reasons"])
def test_indice_non_temporale_e_un_errore_esplicito():
"""Senza indice temporale l'annualizzazione sarebbe arbitraria: meglio alzare che indovinare."""
with pytest.raises(TypeError):
al.implausible_sharpe(pd.Series(np.zeros(100) + 0.001))
# --------------------------------------------------------------- anchor_luck_band
def test_banda_dancora_riconosce_il_massimo_come_fortuna():
"""Se l'ancora canonica e' la MIGLIORE della griglia, il gate deve dirlo: pctl alto e
stima onesta = mediana, non il valore canonico."""
vals = {o: 0.05 * o for o in range(10)} # 9 = il migliore
b = al.anchor_luck_band(lambda o: vals[o], range(10), canonical=9, metric=lambda x: x)
assert b["canonical_pctl"] >= 0.85
assert b["median"] == pytest.approx(0.225)
assert b["luck"] > 0
def test_banda_dancora_non_penalizza_chi_e_al_centro():
"""VRP01 e' l'unico sleeve la cui ancora canonica NON e' fortunata: il gate non deve
inventare una penalita' dove non c'e'."""
vals = {o: 1.0 + 0.01 * o for o in range(9)}
b = al.anchor_luck_band(lambda o: vals[o], range(9), canonical=4, metric=lambda x: x)
assert 0.3 <= b["canonical_pctl"] <= 0.7
assert abs(b["luck"]) < 1e-9
assert b["gate_pass"]
def test_segno_che_dipende_dallancora_non_passa():
"""Il senso del gate: se la meta' delle ancore e' negativa, il titolo e' una scelta d'ancora."""
vals = {o: (1.0 if o % 2 else -1.0) for o in range(10)}
b = al.anchor_luck_band(lambda o: vals[o], range(10), canonical=1, metric=lambda x: x)
assert not b["gate_pass"]
def test_ancore_rotte_non_uccidono_la_banda():
"""Un'ancora che solleva (dati mancanti a quell'offset) va saltata, non propagata."""
def f(o):
if o == 3:
raise ValueError("feed mancante")
return 1.0
b = al.anchor_luck_band(f, range(8), canonical=0, metric=lambda x: x)
assert b["n_anchors"] == 7 and b["gate_pass"]
# --------------------------------------------------------------- anchor_luck_delta
def test_delta_appaiato_ribalta_la_differenza_di_mediane():
"""IL motivo per cui questa funzione esiste (errore commesso il 26/07 sul recupero on-book
di SKH01): le mediane di A e B cadono su offset DIVERSI, quindi la loro differenza confronta
ancore diverse e puo' RIBALTARE il verdetto.
NB: la costruzione non e' banale — se A battesse B a OGNI offset le mediane non potrebbero
invertirsi (la mediana e' monotona). Il caso reale, e quello qui: A vince nella MAGGIORANZA
degli offset (4/5) ma la sua mediana cade sotto quella di B."""
A = {0: 1.0, 1: 1.0, 2: 3.0, 3: 5.0, 4: 6.0} # mediana 3
B = {0: 0.0, 1: 0.0, 2: 4.0, 3: 4.5, 4: 5.0} # mediana 4
naive = float(np.median(list(A.values())) - np.median(list(B.values())))
d = al.anchor_luck_delta(lambda o: A[o], lambda o: B[o], range(5), metric=lambda x: x)
assert naive < 0, "il test non direbbe nulla se la statistica ingenua non sbagliasse"
assert d["median_paired"] > 0
assert d["n_positive"] == 4 and d["gate_pass"]
def test_delta_appaiato_non_promuove_una_vittoria_di_una_sola_ancora():
"""Un vantaggio concentrato su un'ancora sola non e' un vantaggio: e' la fortuna di quell'ancora."""
A = {0: 5.0, 1: 0.9, 2: 0.8, 3: 0.7}
B = {0: 1.0, 1: 1.0, 2: 1.0, 3: 1.0}
d = al.anchor_luck_delta(lambda o: A[o], lambda o: B[o], range(4), metric=lambda x: x)
assert d["median_paired"] < 0 and not d["gate_pass"]