fb01c5714c
Book, pesi, config INVARIATI. Nuovo sorvegliante in cron_daily. IL CAMPIONE C'ERA GIA': 8 scadenze utilizzabili per asset su 8, entrambe le strutture, 16 osservazioni ciascuna. Non serviva aspettare per fare la misura; serve aspettare per rispondere alla DIFFERENZA, che e' un'altra domanda. CORREZIONE A UN ARGOMENTO PUBBLICATO POCHE ORE FA. Nel gate del tenore avevo scritto che la cella vincente 'sta massimizzando l'errore di modello' perche' compra l'ala piu' lontana. Misurato: il meccanismo e' confermato e piu' forte del previsto (f_long 5.85 contro 2.23) ma la conclusione era ROVESCIATA — quell'ala pesa il 3.2% del premio corto invece del 18.4%, quindi sul credito NETTO l'effetto e' minore: f_net 0.852 contro 0.718, il candidato ha un f MIGLIORE. Con f_net = (f_short - k*f_long)/(1-k), un f_long grande fa danno solo moltiplicato per un k grande: avevo guardato il fattore e non il peso. La decisione (nessun cambio) regge, ma su tre gambe invece di quattro. Artefatto di tick escluso prima di crederci: l'ask dell'ala sta a 22 tick mediani, minimo 15, 0% delle osservazioni a <=2 tick. LA DIFFERENZA NON E' STABILITA: appaiata per (asset, scadenza) fa +0.109 con IC95 [-0.047, +0.193], 11/16 positive -> contiene lo zero. Replica indipendente del canonico: 0.718 per un percorso con finestra DTE e pairing diversi da quello che stamattina dava 0.73. CRITERIO PRE-REGISTRATO, congelato in un test: >=40 coppie E ampiezza IC95 <=0.12 (oggi 16 e 0.241), prima gamba verso fine ottobre 2026. Il sorvegliante rifa' la misura ogni giorno e notifica una volta sola quando basta — lezione DVOLSPREAD, un lead senza sorvegliante e senza data e' un lead perso. Calibrazione della soglia verificata prima di fidarsene: con differenza vera nulla lo zero e' escluso nel 5.0/4.0/3.0% dei casi a n=16/40/60, cioe' il 5% atteso. Il test iniziale su seed fisso falliva perche' quel seed era uno dei 5% legittimi: sostituito con un test sulla proprieta'. REGOLE: (a) un fattore di errore si giudica moltiplicato per il suo peso; (b) prima di credere a un rapporto estremo su un prezzo piccolo, contare i tick; (c) una soglia sull'ampiezza di un IC richiede di verificarne la copertura; (d) 'non abbastanza campione' non e' 'nessuna differenza'. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
145 lines
6.3 KiB
Python
145 lines
6.3 KiB
Python
"""Test del sorvegliante di f sulle strutture VRP (scripts/live/vrp_f_watch.py).
|
|
|
|
Cosa va congelato:
|
|
|
|
* **Il criterio di sufficienza**, perche' e' pre-registrato: se un domani non si arriva alla
|
|
soglia, la tentazione e' abbassarla. Il test la lega a una costante, e cambiarla e' un atto
|
|
visibile in un diff.
|
|
* **La statistica appaiata**: la domanda e' una DIFFERENZA fra due strutture sulla stessa
|
|
scadenza, non due intervalli guardati a occhio.
|
|
* **Il fatto che 'non abbastanza' NON e' 'nessuna differenza'** — con l'IC che contiene lo zero
|
|
si aspetta, non si conclude.
|
|
* **I controlli positivi**: un criterio che non scatta mai e uno rotto si somigliano troppo.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT))
|
|
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
|
|
|
_spec = importlib.util.spec_from_file_location(
|
|
"vrp_f_watch", ROOT / "scripts" / "live" / "vrp_f_watch.py")
|
|
W = importlib.util.module_from_spec(_spec)
|
|
_spec.loader.exec_module(W)
|
|
|
|
|
|
def _frame(pairs: int, diff: float, spread: float = 0.0, seed: int = 0) -> pd.DataFrame:
|
|
"""Coppie sintetiche: `pairs` scadenze con canonico ~0.72 e candidato ~0.72+diff."""
|
|
r = np.random.default_rng(seed)
|
|
rows = []
|
|
for i in range(pairs):
|
|
base = 0.72 + r.normal(0, spread)
|
|
for name, v in (("canonico", base), ("candidato", base + diff + r.normal(0, spread))):
|
|
rows.append(dict(asset="BTC", struct=name, exp=pd.Timestamp("2026-01-01") + pd.Timedelta(days=i),
|
|
ts=pd.Timestamp("2026-01-01"), dte=7.0, f_short=1.0, f_long=2.0,
|
|
f_net=v, f_net_mid=v, quota_lunga=0.18))
|
|
return pd.DataFrame(rows)
|
|
|
|
|
|
# ===========================================================================
|
|
# il criterio, che e' pre-registrato
|
|
# ===========================================================================
|
|
def test_il_criterio_di_sufficienza_e_quello_dichiarato_il_30_luglio():
|
|
assert W.MIN_PAIRS == 40
|
|
assert W.MAX_CI_WIDTH == 0.12
|
|
|
|
|
|
def test_le_strutture_confrontate_sono_quelle_congelate():
|
|
"""Il canonico e' VRP01 in produzione; il candidato e' la cella scelta al buio dal gate."""
|
|
assert W.STRUCTS["canonico"][3:] == (-0.28, -0.10)
|
|
assert W.STRUCTS["candidato"][3:] == (-0.28, -0.05)
|
|
assert W.STRUCTS["candidato"][2] == 10.0 # il tenore sotto esame
|
|
|
|
|
|
# ===========================================================================
|
|
# la statistica
|
|
# ===========================================================================
|
|
def test_la_differenza_e_appaiata_per_scadenza():
|
|
"""Con una differenza costante di +0.10 su ogni coppia, la mediana appaiata deve essere
|
|
esattamente +0.10 e l'IC strettissimo — cosa che due IC separati non garantirebbero."""
|
|
a = W.assess(_frame(50, diff=0.10, spread=0.05, seed=1))
|
|
assert a["pairs"] == 50
|
|
assert a["diff"] == pytest.approx(0.10, abs=0.03)
|
|
assert a["ci_width"] < 0.10
|
|
|
|
|
|
def test_poche_coppie_non_bastano_anche_se_la_differenza_e_grande():
|
|
"""Il criterio ha DUE gambe: un effetto grande su 5 coppie non e' una misura."""
|
|
a = W.assess(_frame(5, diff=0.30, spread=0.05, seed=2))
|
|
assert not a["ready"] and a["pairs"] < W.MIN_PAIRS
|
|
|
|
|
|
def test_molte_coppie_con_ic_largo_non_bastano():
|
|
"""L'altra gamba: tante osservazioni ma rumorose non chiudono la domanda."""
|
|
a = W.assess(_frame(60, diff=0.10, spread=0.60, seed=3))
|
|
assert a["pairs"] >= W.MIN_PAIRS
|
|
assert a["ci_width"] > W.MAX_CI_WIDTH and not a["ready"]
|
|
|
|
|
|
# ===========================================================================
|
|
# CONTROLLI POSITIVI
|
|
# ===========================================================================
|
|
def test_controllo_positivo_il_criterio_sa_dire_di_si():
|
|
a = W.assess(_frame(60, diff=0.15, spread=0.05, seed=4))
|
|
assert a["ready"] and a["esclude_zero"]
|
|
|
|
|
|
def test_una_differenza_nulla_e_misurata_come_nulla_non_come_ignota():
|
|
"""A campione pieno e differenza vera zero la domanda E' risolta (`ready`): 'misurato
|
|
uguale' e' un risultato, 'non misurato' no."""
|
|
a = W.assess(_frame(60, diff=0.0, spread=0.05, seed=5))
|
|
assert a["ready"]
|
|
|
|
|
|
def test_l_intervallo_bootstrap_e_calibrato():
|
|
"""La soglia pre-registrata sull'AMPIEZZA dell'IC vale solo se l'IC e' calibrato: se
|
|
fosse troppo stretto, 'ready' arriverebbe presto e con un falso positivo in mano.
|
|
Misurato: con differenza vera nulla lo zero viene escluso nel ~5% dei casi, come deve.
|
|
|
|
(Serve un test sulla PROPRIETA' e non su un seed: la prima stesura fissava seed=5 e
|
|
falliva perche' quel seed era uno dei ~5% legittimi.)"""
|
|
n_seeds = 100
|
|
esclusi = sum(W.assess(_frame(40, diff=0.0, spread=0.05, seed=s))["esclude_zero"]
|
|
for s in range(n_seeds))
|
|
assert esclusi / n_seeds <= 0.12 # 5% atteso, +3 sd binomiali su 100 estrazioni
|
|
|
|
|
|
# ===========================================================================
|
|
# stato reale del progetto
|
|
# ===========================================================================
|
|
@pytest.fixture(scope="module")
|
|
def reale():
|
|
return W.assess(W.measure())
|
|
|
|
|
|
def test_oggi_il_campione_non_basta_e_lo_script_lo_dice(reale):
|
|
"""Congelato: al 2026-07-30 sono 16 coppie con IC95 che CONTIENE lo zero. Se un domani
|
|
questo test fallisce e' perche' il campione e' cresciuto — ed e' il momento di guardare."""
|
|
assert reale["pairs"] >= 16
|
|
if reale["pairs"] < W.MIN_PAIRS:
|
|
assert not reale["ready"]
|
|
|
|
|
|
def test_il_f_del_canonico_replica_la_misura_del_30_luglio(reale):
|
|
"""Percorso indipendente da `r0730_vrp_real_quotes` (finestra DTE e pairing diversi):
|
|
deve ritrovare ~0.73. Se diverge, una delle due misure e' rotta."""
|
|
assert 0.65 <= reale["f_canonico"] <= 0.80
|
|
|
|
|
|
def test_l_ala_piu_lontana_e_piu_mispriced_ma_pesa_meno():
|
|
"""Il meccanismo, misurato: f_long molto peggiore sul candidato, ma quota sul premio corto
|
|
molto minore -> effetto NETTO minore. E' il fatto che ha refutato il mio sospetto."""
|
|
R = W.measure()
|
|
med = R.groupby("struct")[["f_long", "quota_lunga", "f_net"]].median()
|
|
assert med.loc["candidato", "f_long"] > med.loc["canonico", "f_long"]
|
|
assert med.loc["candidato", "quota_lunga"] < med.loc["canonico", "quota_lunga"]
|
|
assert med.loc["candidato", "f_net"] > med.loc["canonico", "f_net"]
|