842420ce7b
Il follow-up "book sul path live" era fermo da tre sessioni su questa premessa:
"il simulatore compone per-trade a nozionale unitario, LO SLEEVE E' VOL-TARGETED".
LA PREMESSA ERA FALSA. Verificato in tre modi: _skyhook_returns chiama backtest_signals
con leverage=1.0/position_size=1.0; nessun target_vol/vol_target/realized_vol nel
sorgente; sim_equity(canonical) riproduce backtest_signals a max|diff| = 0.0. Il 20.7%
di vol realizzata dello sleeve e' un PRODOTTO della strategia (uscite % asimmetriche +
poco tempo a mercato), non un parametro: SKH01 e' l'unica delle 5 a NON essere
vol-targeted, il contrario di quanto si credeva. Test permanente cablato.
MISURA (ingressi live vs backtest, de-luckata su 8 offset a priori; sanity 120/120 bin
con ingresso ricostruiti):
- il numero del 26/07 era ~4x troppo grande: like-with-like (mediana PER-ASSET) +0.38 su
3 offset -> +0.097 su 8, e "6/6 non negativi" -> 13/16. Sleeve 50/50: +0.112, 8/8.
- a livello di BOOK lo Sharpe e' una monetina (FULL +0.048, HOLD +0.051) ma il DRIFT e'
+0.73pp positivo nel 100% delle estrazioni: l'ingresso intra-bin prende un prezzo
migliore, i falsi ingressi aggiungono churn, vol e ritorno salgono insieme.
IL FATTORE x0.6 DECOMPOSTO E MISURATO:
(a) fortuna d'ancora sul DRIFT: x0.874 (5-sleeve) / x0.890 (book live). La vol e'
invariata fra le ancore (7.80->7.76%): la fortuna sta tutta nel drift.
(b) path live: NON-NEGATIVO ovunque (uscite +0.081 Sh, ingressi +0.73pp, TP01 ~0).
Il x0.6 implicava un residuo x0.687 attribuito al live oltre l'ancora, che nessuna misura
sostiene -> FATTORE ONESTO x0.87-0.91, troppo severo del 31-34%. Il sospetto registrato
il 25/07 ("conta due volte la degradazione SKH01") e' confermato quantitativamente.
MURI DI CAPITALE ricalcolati con la stessa macchineria del 25/07: perpetua 6.00% ->
10.91%, muro per 50 EUR/g $494.758 -> $272.061 (-45%) a leva 1.0. La conclusione
STRUTTURALE non cambia: $272k restano ~453x il conto di oggi.
IPOTESI MIA REFUTATA nella stessa sessione: che la grid timing-luck di SKH01 fosse un
artefatto della lente a chiusura-di-bin. Dispersione LIVE/CANONICO = 1.59x -> il live e'
PIU' disperso. L'audit del 02/07 resta valido com'e'. (Nata su 2 offset, chiusa a 8.)
Trovato per strada: simulate() in r0726_skh_partial_entry.py non era mai chiamata da
main() — i numeri headline di quel diario venivano da una corsa mai committata.
Book, pesi, cron, config INVARIATI.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
120 lines
4.6 KiB
Python
120 lines
4.6 KiB
Python
"""Test per il follow-up "book sul path live di SKH01" (r0726_skh_live_book / _deluck_factor).
|
|
|
|
Il test piu' importante e' `test_lente_sleeve_uguale_a_quella_del_simulatore`: e' l'invariante su
|
|
cui poggia TUTTA la misura, ed e' esattamente la premessa che per tre sessioni si era creduta
|
|
FALSA ("lo sleeve e' vol-targeted, il simulatore no"). Se qualcuno domani aggiunge un vol-target
|
|
a `_skyhook_returns`, quella premessa diventa vera e i numeri di questo filone smettono di valere:
|
|
il test deve fallire, non passare in silenzio.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import inspect
|
|
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"))
|
|
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
|
|
|
|
|
|
# ===========================================================================
|
|
# L'INVARIANTE DI LENTE — la premessa del follow-up
|
|
# ===========================================================================
|
|
def test_sleeve_skyhook_non_e_vol_targeted():
|
|
"""`_skyhook_returns` compone per-trade a nozionale unitario: niente vol-target.
|
|
E' il fatto che sblocca il follow-up; se cambia, va rifatta tutta la misura."""
|
|
from src.portfolio.sleeves import _skyhook_returns
|
|
|
|
src = inspect.getsource(_skyhook_returns)
|
|
assert "leverage=1.0" in src
|
|
for kw in ("target_vol", "vol_target", "realized_vol"):
|
|
assert kw not in src, f"lo sleeve ora usa {kw}: la lente e' cambiata, rifare r0726"
|
|
|
|
|
|
def test_lente_sleeve_uguale_a_quella_del_simulatore():
|
|
"""sim_equity(mode='canonical') deve riprodurre backtest_signals ESATTAMENTE.
|
|
E' cio' che rende confrontabili path live e sleeve senza conversioni."""
|
|
import r0702_anchor_skh01 as r02
|
|
|
|
_, m, ltf, ent = r02.run_asset("BTC", 0)
|
|
eq = r02.sim_equity(ltf, ent, "canonical")
|
|
assert float(np.max(np.abs(eq - m.equity))) == 0.0
|
|
|
|
|
|
# ===========================================================================
|
|
# Convenzioni di combinazione
|
|
# ===========================================================================
|
|
def _s(vals, start="2020-01-01"):
|
|
return pd.Series(np.asarray(vals, float),
|
|
index=pd.date_range(start, periods=len(vals), freq="1D", tz="UTC"))
|
|
|
|
|
|
def test_skh_book_e_5050_su_inner_join():
|
|
import r0726_skh_live_book as LB
|
|
|
|
a = _s([0.01, 0.02, 0.03])
|
|
b = _s([0.03, 0.02, 0.01])
|
|
out = LB.skh_book({"BTC": a, "ETH": b})
|
|
assert list(np.round(out.values, 10)) == [0.02, 0.02, 0.02]
|
|
|
|
|
|
def test_skh_book_inner_join_taglia_le_date_non_comuni():
|
|
"""Convenzione dello sleeve: inner join. Se un asset ha meno storia, il book parte dopo."""
|
|
import r0726_skh_live_book as LB
|
|
|
|
a = _s([0.01] * 5, start="2020-01-01")
|
|
b = _s([0.03] * 3, start="2020-01-03")
|
|
out = LB.skh_book({"BTC": a, "ETH": b})
|
|
assert len(out) == 3
|
|
assert out.index[0] == pd.Timestamp("2020-01-03", tz="UTC")
|
|
|
|
|
|
def test_book_metrics_drift_e_annualizzato_a_365():
|
|
import r0726_skh_live_book as LB
|
|
|
|
r = 0.001
|
|
cols = {"X": _s([r] * 400)}
|
|
_, _, drift = LB.book_metrics(cols, {"X": 1.0})
|
|
assert drift == pytest.approx(r * 365.25, rel=1e-9)
|
|
|
|
|
|
def test_drift_and_cagr_coerenti():
|
|
import r0726_deluck_factor as DF
|
|
|
|
r = 0.0005
|
|
cols = {"X": _s([r] * 800)}
|
|
drift, cagr, vol = DF.drift_and_cagr(cols, {"X": 1.0})
|
|
assert drift == pytest.approx(r * 365.25, rel=1e-9)
|
|
assert cagr == pytest.approx((1 + r) ** 365.25 - 1, rel=1e-6) # serie costante -> CAGR esatto
|
|
assert vol == pytest.approx(0.0, abs=1e-12) # nessuna varianza
|
|
|
|
|
|
# ===========================================================================
|
|
# Il sottocampione di offset: dichiarato a priori, non selezionato
|
|
# ===========================================================================
|
|
def test_offsets_sub_e_un_sottoinsieme_uniforme_della_griglia_piena():
|
|
import r0702_anchor_skh01 as r02
|
|
import r0726_skh_sigcache as SC
|
|
|
|
full = set(r02.OFFSETS)
|
|
assert set(SC.OFFSETS_SUB) <= full, "il sottocampione contiene offset fuori griglia"
|
|
assert SC.OFFSETS_SUB[0] == 0, "l'offset canonico deve essere incluso"
|
|
passi = np.diff(np.array(SC.OFFSETS_SUB))
|
|
assert len(set(passi.tolist())) == 1, f"sottocampione non uniforme: passi {passi}"
|
|
|
|
|
|
def test_cache_path_e_deterministico_e_separa_asset_e_offset():
|
|
import r0726_skh_sigcache as SC
|
|
|
|
p1 = SC.cache_path("BTC", 0)
|
|
p2 = SC.cache_path("BTC", 90)
|
|
p3 = SC.cache_path("ETH", 0)
|
|
assert p1 != p2 and p1 != p3
|
|
assert p1 == SC.cache_path("BTC", 0)
|
|
assert p1.suffix == ".parquet"
|