research: follow-up SKH01 chiuso — il blocco non esisteva, il de-luck x0.6 era troppo severo
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>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python
|
||||
"""r0726_capwall_refresh.py — i MURI DI CAPITALE ricalcolati col fattore de-luck corretto.
|
||||
|
||||
Il 25/07 (`r0725_capcurve.py`) ha calcolato rendita perpetua, muro di capitale per 50 EUR/g e
|
||||
traiettoria da $600 applicando un de-luck **×0.6 scelto a occhio** sul drift del book.
|
||||
|
||||
Il 26/07 quel fattore e' stato MISURATO e decomposto (`r0726_deluck_factor.py` +
|
||||
`r0726_skh_live_book.py`):
|
||||
* fortuna d'ancora sul drift, book live Deribit (TP01/SKH01 75/25): **×0.890**
|
||||
(24 x 23 = 552 configurazioni, estrazioni congiunte);
|
||||
* degradazione del path live: **non-negativa su tutto cio' che e' misurato**
|
||||
(SKH01 uscite +0.081 Sharpe di book su 23/23 offset; SKH01 ingressi +0.73pp di drift,
|
||||
positivo nel 100% delle estrazioni; TP01 barra parziale trascurabile su 24 ancore).
|
||||
Il ×0.6 implicava quindi un residuo ×0.687 attribuito al live OLTRE l'ancora, che nessuna misura
|
||||
sostiene: era il doppio conteggio gia' sospettato in CLAUDE.md, ora quantificato.
|
||||
|
||||
Questo script rifa' i numeri della DECISIONE ai tre fattori, sulla stessa macchineria del 25/07
|
||||
(nessuna re-implementazione: `book_series`, `survival`, `_boot_paths` importati da li').
|
||||
|
||||
uv run python scripts/research/r0726_capwall_refresh.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
|
||||
|
||||
import r0725_capcurve as CC # noqa: E402
|
||||
|
||||
# fattori a confronto. 0.890 = fortuna d'ancora MISURATA sul book live Deribit; il vero fattore
|
||||
# onesto e' >= 0.890 perche' la componente live e' non-negativa (non la conto: e' conservativo).
|
||||
FACTORS = (
|
||||
(0.60, "×0.60 (25/07, a occhio)"),
|
||||
(0.89, "×0.89 (26/07, ancora MISURATA)"),
|
||||
(1.00, "×1.00 (nessun haircut)"),
|
||||
)
|
||||
LEVERAGES = (1.0, 1.5)
|
||||
N_PATHS = 2000
|
||||
|
||||
|
||||
def perp_and_wall(r: np.ndarray, k: float, gross_needed: float) -> tuple[float, float, float]:
|
||||
"""(SWR-20a, tasso di rendita PERPETUA, capitale necessario) — stessa ricerca binaria
|
||||
del 25/07: perpetua = prelievo con P(cap a 20a >= cap iniziale) >= 90%."""
|
||||
lo, hi = 0.0, 0.40
|
||||
for _ in range(13):
|
||||
mid = (lo + hi) / 2
|
||||
s = CC.survival(r, 1e6, 1e6 * mid, 20, k, n_paths=N_PATHS)
|
||||
if s["p10_end"] >= 1e6:
|
||||
lo = mid
|
||||
else:
|
||||
hi = mid
|
||||
perp = lo
|
||||
lo2, hi2 = 0.0, 0.40
|
||||
for _ in range(13):
|
||||
mid = (lo2 + hi2) / 2
|
||||
if CC.survival(r, 1e6, 1e6 * mid, 20, k, n_paths=N_PATHS)["p_survive"] >= 0.95:
|
||||
lo2 = mid
|
||||
else:
|
||||
hi2 = mid
|
||||
cap = gross_needed / perp if perp > 0.002 else float("inf")
|
||||
return lo2, perp, cap
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print("=" * 100)
|
||||
print(" r0726 — MURI DI CAPITALE col fattore de-luck MISURATO (era ×0.6 a occhio)")
|
||||
print("=" * 100)
|
||||
|
||||
gross = CC.TARGET_EUR_DAY * 365 * CC.EURUSD / (1 - CC.TAX_RATE)
|
||||
print(f" bersaglio: {CC.TARGET_EUR_DAY:.0f} EUR/g netti = EUR "
|
||||
f"{CC.TARGET_EUR_DAY*365:,.0f}/anno netti = ${gross:,.0f}/anno LORDI "
|
||||
f"(fisco {CC.TAX_RATE:.0%}, EURUSD {CC.EURUSD})")
|
||||
print(" book: TP01+SKH01 75/25 su path live (lente 'hourly'), costi reali Deribit\n")
|
||||
|
||||
base = CC.book_series("hourly")
|
||||
mu = float(base.values.mean())
|
||||
|
||||
print(f" {'fattore':<32}{'leva':>6}{'SWR-20a':>10}{'PERPETUA':>10}{'capitale':>16}")
|
||||
print(" " + "-" * 74)
|
||||
res = {}
|
||||
for f, lab in FACTORS:
|
||||
r = (base - (1.0 - f) * mu).values.astype(float)
|
||||
for k in LEVERAGES:
|
||||
swr, perp, cap = perp_and_wall(r, k, gross)
|
||||
res[(f, k)] = (swr, perp, cap)
|
||||
capf = f"${cap:,.0f}" if np.isfinite(cap) else "IRRAGGIUNGIBILE"
|
||||
print(f" {lab:<32}{k:>6.2f}{swr:>9.2%}{perp:>10.2%}{capf:>16}")
|
||||
print()
|
||||
|
||||
print("=" * 100)
|
||||
print(" COSA CAMBIA NELLA DECISIONE")
|
||||
print("=" * 100)
|
||||
for k in LEVERAGES:
|
||||
c_old = res[(0.60, k)][2]
|
||||
c_new = res[(0.89, k)][2]
|
||||
if np.isfinite(c_old) and np.isfinite(c_new):
|
||||
print(f" leva {k:.2f}: muro ${c_old:,.0f} -> ${c_new:,.0f} "
|
||||
f"({(c_new/c_old - 1)*100:+.0f}%)")
|
||||
c_new = res[(0.89, 1.0)][2]
|
||||
print(f"\n Il muro scende di ~45%, ma resta ${c_new:,.0f} = ~{c_new/600:,.0f}x il conto")
|
||||
print(" di oggi ($600). La conclusione STRUTTURALE del 25/07 non cambia: i 600 euro")
|
||||
print(" come CAPITALE restano refutati, come BIGLIETTO (prop) no. Cambia la taglia")
|
||||
print(" dell'errore — e cambia che il numero ora e' misurato invece che scelto.")
|
||||
print("\n ⚠️ Il ×0.89 e' la sola fortuna d'ancora: e' un LIMITE INFERIORE del fattore")
|
||||
print(" onesto, perche' ogni componente misurata del path live e' non-negativa.")
|
||||
print(" Il muro vero sta quindi fra la riga ×0.89 e la riga ×1.00.")
|
||||
|
||||
# rendita a $600 — il numero che il 25/07 dava a ~0.11 EUR/g
|
||||
print("\n" + "=" * 100)
|
||||
print(" RENDITA A $600 (lo stesso conto di oggi)")
|
||||
print("=" * 100)
|
||||
for f, lab in FACTORS:
|
||||
r = (base - (1.0 - f) * mu).values.astype(float)
|
||||
cagr = float((1 + r.mean()) ** 365.25 - 1)
|
||||
eur = 600.0 * cagr * (1 - CC.TAX_RATE) / CC.EURUSD / 365.0
|
||||
print(f" {lab:<32} CAGR {cagr*100:6.2f}% rendita EUR {eur:.2f}/giorno")
|
||||
print("\n (a questa taglia la differenza fra ×0.6 e ×0.89 sono centesimi al giorno:")
|
||||
print(" il fattore conta per il MURO e per le soglie prop, non per il conto attuale.)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env python
|
||||
"""r0726_deluck_factor.py — DECOMPOSIZIONE del fattore de-luck ×0.6 del 25/07.
|
||||
|
||||
IL PROBLEMA. `r0725_capcurve.py` applica `b = b - 0.4*mean(b)` ("de-luck ×0.6": taglia il 40% del
|
||||
DRIFT, lascia la vol) sopra un book gia' calcolato con SKH01 sulla lente `hourly`. Il ×0.6 fu
|
||||
scelto **a occhio**, e CLAUDE.md registrava gia' il sospetto: *"il ×0.6 sopra il path live rischia
|
||||
di contare DUE VOLTE la degradazione SKH01"*.
|
||||
|
||||
Il fattore mescola due cose che ora si possono separare e misurare:
|
||||
(a) **fortuna d'ancora** — i numeri canonici sono un punto alto dello spazio d'ancora
|
||||
(misurato il 26/07 sul Sharpe: FULL 97.0 pctl, HOLD 99.6 pctl). Qui serve sul **DRIFT**,
|
||||
perche' e' il drift che il ×0.6 taglia.
|
||||
(b) **degradazione del path live** — che per SKH01 e' misurata **NEGATIVA** (il live e' MEGLIO
|
||||
del backtest su entrambi i lati: uscite +0.081 di Sharpe FULL di book su 23/23 offset,
|
||||
ingressi +0.38 di Sharpe di sleeve) e per TP01 e' ~zero (±0.03 su 24 ancore).
|
||||
|
||||
Questo script misura (a) e la riporta accanto a (b), per due book:
|
||||
* il book ATTIVO 5-sleeve (33/15/12/20/20)
|
||||
* il book LIVE Deribit (TP01/SKH01 75/25) — quello che gira davvero
|
||||
|
||||
METODO: identico a `r0726_loo_deluck.py` (stesse serie d'ancora, stesse estrazioni congiunte
|
||||
uniformi indipendenti), ma la metrica e' il **drift annualizzato** (media giornaliera x 365.25) e
|
||||
il CAGR, non il Sharpe. Il fattore onesto e' `mediana / canonico` sul drift.
|
||||
|
||||
NON decide da solo il valore da usare: la componente (b) sugli INGRESSI di SKH01 e' in corso di
|
||||
misura de-luckata (`r0726_skh_sigcache.py` + `r0726_skh_live_book.py`). Qui si stabilisce quanto
|
||||
del ×0.6 e' spiegato dall'ancora — cioe' quanto ne resta da attribuire al live.
|
||||
|
||||
uv run python scripts/research/r0726_deluck_factor.py [--draws N]
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
|
||||
|
||||
import r0726_loo_deluck as L # noqa: E402
|
||||
from src.portfolio.portfolio import HOLDOUT, combine_outer, metrics # noqa: E402
|
||||
|
||||
DPY = 365.25
|
||||
|
||||
BOOKS = {
|
||||
"book ATTIVO 5-sleeve (33/15/12/20/20)": L.WEIGHTS,
|
||||
"book LIVE Deribit (TP01/SKH01 75/25)": {"TP01_trend_1d": 0.75, "SKH01_skyhook": 0.25},
|
||||
}
|
||||
|
||||
|
||||
def drift_and_cagr(cols: dict, weights: dict) -> tuple[float, float, float]:
|
||||
"""(drift annualizzato, CAGR, vol annualizzata) del book a quei pesi."""
|
||||
w = {k: weights[k] for k in cols}
|
||||
s = combine_outer(cols, w)
|
||||
r = np.asarray(s.dropna().values, float)
|
||||
m = metrics(s)
|
||||
return float(r.mean() * DPY), float(m["cagr"]), float(r.std() * np.sqrt(DPY))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--draws", type=int, default=2000)
|
||||
args = ap.parse_args()
|
||||
|
||||
print("=" * 100)
|
||||
print(" r0726 — DECOMPOSIZIONE del fattore de-luck ×0.6 (25/07)")
|
||||
print("=" * 100)
|
||||
print(" Il ×0.6 taglia il DRIFT del book (b - 0.4*mean(b)). Quanto di quel taglio e'")
|
||||
print(" spiegato dalla FORTUNA D'ANCORA, e quanto resterebbe da attribuire al path live?")
|
||||
|
||||
S = L.build_all()
|
||||
rng = np.random.default_rng(L.SEED)
|
||||
cfgs = [{n: int(rng.choice(L.SPEC[n][1])) for n in L.SPEC} for _ in range(args.draws)]
|
||||
canon_cfg = {n: L.SPEC[n][2] for n in L.SPEC}
|
||||
|
||||
for label, W in BOOKS.items():
|
||||
names = list(W)
|
||||
print("\n" + "=" * 100)
|
||||
print(f" {label}")
|
||||
print("=" * 100)
|
||||
|
||||
def cols_at(cfg):
|
||||
return {n: S[n][cfg[n]] for n in names}
|
||||
|
||||
d_can, c_can, v_can = drift_and_cagr(cols_at(canon_cfg), W)
|
||||
rows = np.array([drift_and_cagr(cols_at(c), W) for c in cfgs], float)
|
||||
d_med, c_med, v_med = np.median(rows, axis=0)
|
||||
d_lo, d_hi = np.percentile(rows[:, 0], [10, 90])
|
||||
c_lo, c_hi = np.percentile(rows[:, 1], [10, 90])
|
||||
|
||||
n_anch = int(np.prod([len(L.SPEC[n][1]) for n in names]))
|
||||
print(f" spazio d'ancora di questo book: {n_anch:,} configurazioni "
|
||||
f"({' x '.join(str(len(L.SPEC[n][1])) for n in names)})")
|
||||
print(f"\n {'':<24}{'canonico':>11}{'pctl':>8}{'MEDIANA':>11}{'p10':>10}{'p90':>10}")
|
||||
print(f" {'drift annualizzato':<24}{d_can*100:10.2f}%"
|
||||
f"{float((rows[:, 0] < d_can).mean()*100):7.1f}%{d_med*100:10.2f}%"
|
||||
f"{d_lo*100:9.2f}%{d_hi*100:9.2f}%")
|
||||
print(f" {'CAGR':<24}{c_can*100:10.2f}%"
|
||||
f"{float((rows[:, 1] < c_can).mean()*100):7.1f}%{c_med*100:10.2f}%"
|
||||
f"{c_lo*100:9.2f}%{c_hi*100:9.2f}%")
|
||||
print(f" {'vol annualizzata':<24}{v_can*100:10.2f}%{'':>8}{v_med*100:10.2f}%")
|
||||
|
||||
f_drift = d_med / d_can if d_can else float("nan")
|
||||
f_cagr = c_med / c_can if c_can else float("nan")
|
||||
print(f"\n >>> FATTORE D'ANCORA sul drift : ×{f_drift:.3f} (il ×0.6 ne assume ×0.600)")
|
||||
print(f" >>> FATTORE D'ANCORA sul CAGR : ×{f_cagr:.3f}")
|
||||
residuo = 0.6 / f_drift if f_drift else float("nan")
|
||||
print(f" >>> residuo implicito da attribuire al PATH LIVE: ×{residuo:.3f}")
|
||||
if residuo < 1.0:
|
||||
print(f" cioe' il ×0.6 assume che il live tolga un altro "
|
||||
f"{(1-residuo)*100:.0f}% di drift OLTRE l'ancora.")
|
||||
else:
|
||||
print(f" cioe' il ×0.6 e' CONSERVATIVO anche prima di contare il live.")
|
||||
|
||||
print("\n" + "=" * 100)
|
||||
print(" COMPONENTE (b) — degradazione del path live, gia' misurata")
|
||||
print("=" * 100)
|
||||
print(" SKH01 uscite : +0.081 Sharpe FULL di book, 23/23 offset (26/07, banda appaiata)")
|
||||
print(" SKH01 ingressi: +0.38 Sharpe di sleeve, mediana su 3 offset x 2 asset (26/07)")
|
||||
print(" -> in corso la versione de-luckata a 8 offset, a livello di BOOK")
|
||||
print(" TP01 barra parziale: -0.031 FULL / +0.118 HOLD su 24 ancore = trascurabile")
|
||||
print(" => il segno di (b) e' NON-NEGATIVO su tutto cio' che e' stato misurato.")
|
||||
print(" Un fattore <1 per il live non e' sostenuto da nessuna misura del progetto.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env python
|
||||
"""r0726_skh_live_book.py — IL BOOK SUL PATH LIVE DI SKH01, de-luckato. Chiude il follow-up.
|
||||
|
||||
FOLLOW-UP CHE CHIUDE (portato avanti bloccato dal 26/07, registrato in CLAUDE.md):
|
||||
|
||||
> il book ricalcolato sul path live e' bloccato da una **incompatibilita' di lenti** — il
|
||||
> simulatore SKH01 compone per-trade a nozionale unitario, lo sleeve e' vol-targeted; combinarle
|
||||
> darebbe un numero preciso e falso. Serve la versione vol-targeted del path live di SKH01.
|
||||
|
||||
⚠️ **LA PREMESSA ERA FALSA e il blocco non esisteva.** Verificato in tre modi:
|
||||
1. `sleeves._skyhook_returns` chiama `backtest_signals(..., leverage=1.0, position_size=1.0)`:
|
||||
**nessun vol-targeting**, compounding per-trade a nozionale pieno — la STESSA lente del
|
||||
simulatore. Il 20.7% di vol realizzata dello sleeve e' un prodotto della strategia (uscite
|
||||
% asimmetriche + poco tempo a mercato), non un target;
|
||||
2. `grep` di `target_vol|vol_target|realized_vol` nel sorgente dello sleeve: nessuna occorrenza;
|
||||
3. `r0702_anchor_skh01.sim_equity(mode='canonical')` riproduce `backtest_signals` con
|
||||
**max|diff| = 0.0** — le due lenti sono gia' la stessa cosa.
|
||||
Il follow-up non era bloccato: era mal diagnosticato. Questo script fa la misura.
|
||||
|
||||
COSA MISURA. Due path identici in tutto (livelli, uscite intra-barra, cap `max_per_day`, fee,
|
||||
non-overlap) tranne **quando si valuta l'ingresso**:
|
||||
* LIVE = a ogni confine orario dentro il bin 230m (cio' che il cron fa davvero);
|
||||
* BACKTEST = solo alla chiusura del bin (cio' che il backtest assume).
|
||||
Riusa la macchina a stati fedele gia' scritta e testata il 26/07 (`r0726_skh_partial_entry
|
||||
.simulate`) — che pero' `main()` di quello script **non chiamava mai**: i numeri headline del suo
|
||||
diario (3 offset) vennero da una corsa ad hoc mai committata. Qui il driver e' committato.
|
||||
|
||||
DE-LUCK. Il 26/07 la misura era su **3 offset**; la lezione dello stesso giorno dice che *se si
|
||||
de-lucka una strategia va de-luckato anche il suo DEGRADO*. Qui: **8 offset** (sottocampione
|
||||
uniforme a priori, `r0726_skh_sigcache.OFFSETS_SUB`) x 2 asset, e il salto a livello di BOOK con
|
||||
gli altri 4 sleeve estratti sulle loro ancore -> **mediana delle differenze appaiate**.
|
||||
|
||||
USO: la cache dei segnali va costruita prima (~9 min per asset x offset):
|
||||
uv run python scripts/research/r0726_skh_sigcache.py
|
||||
uv run python scripts/research/r0726_skh_live_book.py [--draws N]
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
|
||||
|
||||
import r0702_anchor_skh01 as r02 # noqa: E402
|
||||
import r0726_loo_deluck as L # noqa: E402
|
||||
import r0726_skh_partial_entry as SP # noqa: E402
|
||||
import r0726_skh_sigcache as SC # noqa: E402
|
||||
from src.portfolio.portfolio import HOLDOUT, combine_outer, metrics, to_daily # noqa: E402
|
||||
|
||||
ASSETS = ("BTC", "ETH")
|
||||
SKH = "SKH01_skyhook"
|
||||
DPY = 365.25
|
||||
|
||||
|
||||
def _sh(s: pd.Series) -> float:
|
||||
r = np.asarray(s.dropna().values, float)
|
||||
return float(r.mean() / r.std() * np.sqrt(DPY)) if len(r) > 2 and r.std() > 0 else 0.0
|
||||
|
||||
|
||||
def paths_for(asset: str, off: int) -> dict:
|
||||
"""(daily LIVE, daily BACKTEST, diagnostica) per un asset a un offset."""
|
||||
tab = SC.get_signal_table(asset, off, verbose=False)
|
||||
_, _, ltf, _ = r02.run_asset(asset, off)
|
||||
df5 = r02.get5m(asset)
|
||||
ts5 = df5["timestamp"].values.astype(np.int64)
|
||||
h5 = df5["high"].values.astype(float)
|
||||
l5 = df5["low"].values.astype(float)
|
||||
c5 = df5["close"].values.astype(float)
|
||||
live = SP.simulate(tab, ltf, ts5, h5, l5, c5, intra_entry=True)
|
||||
bt = SP.simulate(tab, ltf, ts5, h5, l5, c5, intra_entry=False)
|
||||
return dict(live=live["daily"], bt=bt["daily"],
|
||||
n_live=live["n_trades"], n_bt=bt["n_trades"], n_false=live["n_false"])
|
||||
|
||||
|
||||
def skh_book(dailies: dict) -> pd.Series:
|
||||
"""50/50 BTC+ETH, convenzione di _skyhook_returns (inner join sulla griglia giornaliera)."""
|
||||
J = pd.concat(dailies, axis=1, join="inner").fillna(0.0)
|
||||
return pd.Series(0.5 * J[ASSETS[0]].values + 0.5 * J[ASSETS[1]].values, index=J.index)
|
||||
|
||||
|
||||
def book_metrics(cols: dict, weights: dict) -> tuple[float, float, float]:
|
||||
w = {k: weights[k] for k in cols}
|
||||
s = combine_outer(cols, w)
|
||||
m = metrics(s)
|
||||
r = np.asarray(s.dropna().values, float)
|
||||
return m["sharpe"], metrics(s[s.index >= HOLDOUT])["sharpe"], float(r.mean() * DPY)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--draws", type=int, default=200,
|
||||
help="estrazioni delle ancore degli ALTRI 4 sleeve, per offset SKH")
|
||||
args = ap.parse_args()
|
||||
|
||||
have = [o for o in SC.OFFSETS_SUB
|
||||
if all(SC.cache_path(a, o).exists() for a in ASSETS)]
|
||||
if len(have) < 3:
|
||||
print(f"[STOP] servono almeno 3 offset in cache, presenti {len(have)}: {have}")
|
||||
print(" gira prima: uv run python scripts/research/r0726_skh_sigcache.py")
|
||||
sys.exit(1)
|
||||
|
||||
print("=" * 100)
|
||||
print(" r0726 — IL BOOK SUL PATH LIVE DI SKH01 (de-luckato). Chiude il follow-up.")
|
||||
print("=" * 100)
|
||||
print(f" offset in cache: {have} ({len(have)}/{len(SC.OFFSETS_SUB)} del sottocampione "
|
||||
f"a priori, {len(SC.OFFSETS_SUB)}/23 della griglia piena)")
|
||||
|
||||
# ---------------- 0. SANITY: la ricostruzione a fine bin == backtest --------------
|
||||
print("\n" + "-" * 100)
|
||||
print(" 0. SANITY — a chiusura di bin la ricostruzione deve dare il segnale del backtest")
|
||||
print(" (campionata sugli EVENTI: gli ingressi sono ~2% dei bin, a caso il check e' vacuo)")
|
||||
print("-" * 100)
|
||||
for a in ASSETS:
|
||||
r = SP.selfcheck(a, off=0)
|
||||
ok = r["sig_ok"] == r["sig_tot"]
|
||||
print(f" {a}: bin CON ingresso {r['sig_ok']}/{r['sig_tot']} "
|
||||
f"{'OK' if ok else '*** DIVERGE ***'} | flat {r['flat_ok']}/{r['flat_tot']} "
|
||||
f"(+{r['flat_capped']} con segnale soppresso da max_per_day: ATTESO, non divergenza)")
|
||||
if not ok:
|
||||
sys.exit(1)
|
||||
|
||||
# ---------------- 1. livello SLEEVE, per offset -----------------------------------
|
||||
print("\n" + "-" * 100)
|
||||
print(" 1. SLEEVE SKH01 — Sharpe del path LIVE vs BACKTEST, per offset")
|
||||
print("-" * 100)
|
||||
print(f" {'off':>5}{'CANON':>9}{'ShBT':>9}{'ShLIVE':>9}{'dSharpe':>10}"
|
||||
f"{'trade LIVE':>12}{'trade BT':>10}{'falsi':>8}")
|
||||
P = {}
|
||||
rows = []
|
||||
can_v, bt_v, live_v, per_asset = [], [], [], []
|
||||
for off in have:
|
||||
d = {a: paths_for(a, off) for a in ASSETS}
|
||||
live = skh_book({a: d[a]["live"] for a in ASSETS})
|
||||
bt = skh_book({a: d[a]["bt"] for a in ASSETS})
|
||||
P[off] = (live, bt)
|
||||
s_c, s_l, s_b = _sh(r02.skh_port(off)), _sh(live), _sh(bt)
|
||||
rows.append(s_l - s_b)
|
||||
can_v.append(s_c); bt_v.append(s_b); live_v.append(s_l)
|
||||
for a in ASSETS: # per-asset: la grandezza citata il 26/07
|
||||
per_asset.append((off, a, _sh(d[a]["live"]) - _sh(d[a]["bt"])))
|
||||
print(f" {off:>5}{s_c:+9.3f}{s_b:+9.3f}{s_l:+9.3f}{s_l - s_b:+10.3f}"
|
||||
f"{sum(d[a]['n_live'] for a in ASSETS):>12}"
|
||||
f"{sum(d[a]['n_bt'] for a in ASSETS):>10}"
|
||||
f"{sum(d[a]['n_false'] for a in ASSETS):>8}")
|
||||
arr = np.array(rows, float)
|
||||
print(f"\n SLEEVE (50/50) mediana delle differenze APPAIATE: {np.median(arr):+.3f} "
|
||||
f"positive {int((arr > 0).sum())}/{len(arr)} banda [{arr.min():+.3f}, {arr.max():+.3f}]")
|
||||
pa = np.array([x[2] for x in per_asset], float)
|
||||
print(f" PER-ASSET (la grandezza citata il 26/07) : {np.median(pa):+.3f} "
|
||||
f"positive {int((pa > 0).sum())}/{len(pa)} banda [{pa.min():+.3f}, {pa.max():+.3f}]")
|
||||
print(" ⚠️ le due righe NON sono la stessa cosa: il 26/07 cito' la mediana dei Δ")
|
||||
print(" PER-ASSET (+0.38 su 3 offset x 2 asset). Il numero che entra nel BOOK e'")
|
||||
print(" quello dello SLEEVE 50/50, dove la diversificazione BTC/ETH cambia il")
|
||||
print(" denominatore. Vanno confrontati like-with-like.")
|
||||
|
||||
# ---- 1b. DISPERSIONE fra offset: chi e' davvero esposto alla fortuna di griglia? ----
|
||||
print("\n" + "-" * 100)
|
||||
print(" 1b. DISPERSIONE FRA OFFSET — la timing-luck di griglia di SKH01 e' del BACKTEST")
|
||||
print(" o della STRATEGIA? (il live non e' ancorato al confine di bin: valuta ogni ora)")
|
||||
print("-" * 100)
|
||||
print(f" {'path':<28}{'min':>9}{'mediana':>10}{'max':>9}{'range':>9}{'std':>9}")
|
||||
for lab, v in (("CANONICO (chiusura bin)", can_v), ("BT-sim (chiusura bin)", bt_v),
|
||||
("LIVE-sim (intra-bin)", live_v)):
|
||||
v = np.array(v, float)
|
||||
print(f" {lab:<28}{v.min():+9.3f}{np.median(v):+10.3f}{v.max():+9.3f}"
|
||||
f"{v.max()-v.min():9.3f}{v.std(ddof=1):9.3f}")
|
||||
rc = np.array(can_v, float); rl = np.array(live_v, float)
|
||||
if rc.std(ddof=1) > 0:
|
||||
ratio = rl.std(ddof=1) / rc.std(ddof=1)
|
||||
print(f"\n rapporto di dispersione LIVE/CANONICO: {ratio:.2f}x")
|
||||
if ratio < 1.0:
|
||||
print(" <1 -> il path live e' MENO sensibile alla fase della griglia: parte della")
|
||||
print(" 'grid timing-luck' del 02/07 sarebbe un artefatto della lente a")
|
||||
print(" chiusura-di-bin, non una fragilita' della strategia live.")
|
||||
else:
|
||||
print(" >=1 -> IPOTESI REFUTATA. Il path live e' PIU' disperso fra le fasi della")
|
||||
print(" griglia, non meno: la grid timing-luck di SKH01 (audit 02/07) NON e'")
|
||||
print(" un artefatto della lente a chiusura-di-bin. L'audit resta valido")
|
||||
print(" com'e'. (Ipotesi nata su 2 offset, refutata a 4, confermata a 8.)")
|
||||
|
||||
# ---------------- 2. livello BOOK, de-luckato -------------------------------------
|
||||
print("\n" + "-" * 100)
|
||||
print(" 2. BOOK 5-sleeve — effetto degli INGRESSI live, con gli altri 4 sleeve estratti")
|
||||
print("-" * 100)
|
||||
S = L.build_all()
|
||||
others = [n for n in L.SPEC if n != SKH]
|
||||
rng = np.random.default_rng(L.SEED)
|
||||
d_full, d_hold, d_drift = [], [], []
|
||||
for off in have:
|
||||
live, bt = P[off]
|
||||
for _ in range(args.draws):
|
||||
cfg = {n: int(rng.choice(L.SPEC[n][1])) for n in others}
|
||||
base = {n: S[n][cfg[n]] for n in others}
|
||||
fl, hl, dl = book_metrics({**base, SKH: to_daily(live)}, L.WEIGHTS)
|
||||
fb, hb, db = book_metrics({**base, SKH: to_daily(bt)}, L.WEIGHTS)
|
||||
d_full.append(fl - fb)
|
||||
d_hold.append(hl - hb)
|
||||
d_drift.append(dl - db)
|
||||
for lab, v in (("dSharpe FULL", d_full), ("dSharpe HOLD-OUT", d_hold),
|
||||
("d drift annuo", d_drift)):
|
||||
v = np.array(v, float)
|
||||
k = 100.0 if "drift" in lab else 1.0
|
||||
u = "pp" if "drift" in lab else ""
|
||||
print(f" {lab:<20} mediana {np.median(v)*k:+7.3f}{u} "
|
||||
f"p10 {np.percentile(v, 10)*k:+7.3f}{u} p90 {np.percentile(v, 90)*k:+7.3f}{u} "
|
||||
f">0 in {(v > 0).mean()*100:5.1f}%")
|
||||
|
||||
# ---------------- 3. il fattore de-luck ------------------------------------------
|
||||
print("\n" + "=" * 100)
|
||||
print(" 3. IL FATTORE DE-LUCK ×0.6 — decomposto e ricomposto")
|
||||
print("=" * 100)
|
||||
dd = np.median(np.array(d_drift, float))
|
||||
print(" (a) fortuna d'ancora sul drift del book 5-sleeve : ×0.874 (r0726_deluck_factor)")
|
||||
print(f" (b) path live SKH01, componente INGRESSI : {dd*100:+.3f}pp di drift di book")
|
||||
print(" (b) path live SKH01, componente USCITE : +0.081 Sharpe FULL (23/23 off.)")
|
||||
print(" (b) path live TP01, barra parziale : trascurabile (24 ancore)")
|
||||
drift_can = 0.1733 # drift canonico del book 5-sleeve (r0726_deluck_factor)
|
||||
f_lo = 0.874 # ancora sola: ignora il live (conservativo)
|
||||
f_hi = 0.874 * (1.0 + dd / drift_can) # ancora + componente INGRESSI misurata qui
|
||||
print(f"\n Il ×0.6 implica ×0.687 di residuo attribuito al live OLTRE l'ancora.")
|
||||
print(" Nessuna misura del progetto sostiene un residuo <1: tutte le componenti")
|
||||
print(" misurate del path live sono NON-NEGATIVE.")
|
||||
print(f"\n >>> FATTORE ONESTO: ×{f_lo:.2f} - ×{f_hi:.2f} (contro il ×0.60 in uso)")
|
||||
print(f" ×{f_lo:.2f} = sola fortuna d'ancora, ignorando il live (conservativo)")
|
||||
print(f" ×{f_hi:.2f} = ancora + componente INGRESSI ({dd*100:+.2f}pp su {drift_can*100:.2f}%)")
|
||||
print(" La componente USCITE (+0.081 Sharpe) spingerebbe oltre, ma e' misurata in")
|
||||
print(" Sharpe e non in drift: non la converto per non inventare precisione.")
|
||||
print(f" => il ×0.6 e' troppo severo di circa il {(1 - 0.6/f_lo)*100:.0f}-{(1 - 0.6/f_hi)*100:.0f}%.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python
|
||||
"""r0726_skh_sigcache.py — costruisce e mette in CACHE le tabelle di segnale intra-bin di SKH01.
|
||||
|
||||
E' il pezzo caro del follow-up "book sul path live" (`r0726_skh_live_book.py`): per ogni
|
||||
(asset, offset) ricostruisce il segnale che il cron ORARIO vedrebbe a OGNI osservazione dentro
|
||||
ogni bin 230m — ~82.000 righe, ~9 minuti. Isolato qui perche' e' (a) puro, (b) riusabile, e
|
||||
(c) ripartibile: la cache e' su disco, il job si puo' interrompere e riprendere.
|
||||
|
||||
Sottocampione di offset dichiarato A PRIORI: 8 degli 23 della griglia SKH01, uno ogni 90 minuti
|
||||
su [0, 690). NON e' una selezione — e' un sottocampione uniforme scelto per costo (2 core, ~9
|
||||
min per run, cron live da non affamare). Va dichiarato perche' la banda d'ancora che ne esce ha
|
||||
8 punti invece di 23.
|
||||
|
||||
uv run python scripts/research/r0726_skh_sigcache.py [--assets BTC ETH] [--offsets ...]
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
|
||||
|
||||
import r0726_skh_partial_entry as SP # noqa: E402
|
||||
|
||||
# 8 offset uniformi su [0,690), uno ogni 90m. Dichiarati a priori, offset 0 = canonico.
|
||||
OFFSETS_SUB = (0, 90, 180, 270, 360, 450, 540, 630)
|
||||
ASSETS = ("BTC", "ETH")
|
||||
CACHE_DIR = ROOT / "data" / "_cache" / "skh_sigtab"
|
||||
|
||||
|
||||
def cache_path(asset: str, off: int) -> Path:
|
||||
return CACHE_DIR / f"sigtab_{asset.lower()}_off{off:03d}.parquet"
|
||||
|
||||
|
||||
def get_signal_table(asset: str, off: int, verbose: bool = True) -> pd.DataFrame:
|
||||
"""Tabella di segnale intra-bin, da cache se presente."""
|
||||
p = cache_path(asset, off)
|
||||
if p.exists():
|
||||
return pd.read_parquet(p)
|
||||
t0 = time.time()
|
||||
tab = SP.signal_table(asset, off)
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
tab.to_parquet(p, index=False)
|
||||
if verbose:
|
||||
print(f" {asset} off{off:>3} {len(tab):>7} righe "
|
||||
f"segnali {int((tab['dir'] != 0).sum()):>5} in {time.time()-t0:5.0f}s", flush=True)
|
||||
return tab
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--assets", nargs="+", default=list(ASSETS))
|
||||
ap.add_argument("--offsets", nargs="+", type=int, default=list(OFFSETS_SUB))
|
||||
args = ap.parse_args()
|
||||
|
||||
print(f"[SIGCACHE] {len(args.assets)} asset x {len(args.offsets)} offset "
|
||||
f"-> {CACHE_DIR}", flush=True)
|
||||
t0 = time.time()
|
||||
done = 0
|
||||
for off in args.offsets: # offset esterno: cosi' ogni offset e'
|
||||
for a in args.assets: # completo su entrambi gli asset appena finisce
|
||||
p = cache_path(a, off)
|
||||
if p.exists():
|
||||
print(f" {a} off{off:>3} gia' in cache", flush=True)
|
||||
continue
|
||||
get_signal_table(a, off)
|
||||
done += 1
|
||||
print(f"[SIGCACHE] fatto: {done} nuove tabelle in {time.time()-t0:.0f}s", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user