research(live): follow-up SKH chiuso — peso 0.25 e cron orario CONFERMATI de-luckati sul path live; + snapshot point-in-time stablecoin (sblocco WATCH a 12 mesi)
- r0724_skh_live_weight.py: sweep w 0-0.50 sul path hourly x 23 offset; w*=0.25 (argmax mediana-IS di banda, plateau 0.20-0.30); w=0.30 passa weights_tilt_null solo a off0 (ancora fortunata), fallisce a offset mediano -> INVARIATO. Cadenza 230m = rumore (+0.01/+0.02 Sh med); il degrado live e' il fill-al-livello (~+0.35 Sh) che nessun cron recupera. Book e cron INVARIATI. - r0724_stable_snapshot.py: cattura giornaliera point-in-time supply stablecoin (DefiLlama, tokenless, idempotente) -> data/external/stable_snapshots/ (gitignored). Criterio di rivisita del lead STABLE: >=12 mesi di serie propria. - CLAUDE.md: bullet SKH01 aggiornato (follow-up chiuso). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -106,7 +106,12 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
|
||||
(min +0.18, med +0.42) + corr 0.05-0.11 → ADDS sopravvive ridimensionato. **LIVE (SKH=25% del book
|
||||
Deribit):** path reale cron orario + exit software → book 50/50 FULL 1.46→1.19 / HOLD 1.64→1.15 /
|
||||
DD 18→25%; nei crash gap-through-stop reale (sl2% modellato → −11/−23% realizzato). Pesi/book
|
||||
INVARIATI (ogni cambio passa weights_tilt_null); follow-up: cadenza 230m, peso live da rivedere.
|
||||
INVARIATI (ogni cambio passa weights_tilt_null). **Follow-up CHIUSO 2026-07-24**
|
||||
(`scripts/research/r0724_skh_live_weight.py`): sul path live (lente hourly, 23 offset) il peso
|
||||
ottimale de-luckato È 0.25 (argmax mediana-IS di banda, plateau 0.20-0.30; w=0.30 passa il gate
|
||||
solo a off0 = ancora fortunata, fallisce a offset mediano) e la cadenza 230m vale ~+0.01/+0.02 Sh
|
||||
mediano (rumore: il degrado live è il fill-al-livello, ~+0.35 Sh, che nessun cron recupera) →
|
||||
**book 75/25 e cron orario CONFERMATI**; diario `2026-07-24-skh-live-weight.md`.
|
||||
Script `scripts/research/r0702_anchor_skh01.py`; diario `2026-07-02-anchor-audit-xs01-skh01.md`.
|
||||
- **VRP01 Options Short-Vol — DIVERSIFICATORE da FinanceOld/OptionsAgent** — `src/portfolio/sleeves._vrp_combo_returns`.
|
||||
Put credit spread settimanale (vendi put -0.28, compra put -0.10) gated su IV-rank. Idee portate da
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
# 2026-07-24 — SKH01 sul PATH LIVE: peso del book Deribit a 2 sleeve + valore della cadenza 230m
|
||||
|
||||
**Chiude il follow-up aperto dall'audit anchor 2026-07-02** (`2026-07-02-anchor-audit-xs01-skh01.md`:
|
||||
"cadenza 230m, peso live da rivedere"). Il book live Deribit è TP01 0.75 + SKH01 0.25
|
||||
(`sleeves.deribit_book_sleeves`); l'audit aveva mostrato che il path live reale (cron ORARIO
|
||||
`0 * * * *` + exit software, non on-book al livello) degrada SKH01 (50/50: FULL 1.46→1.19,
|
||||
HOLD 1.64→1.15, DD 18→25%) e che l'offset 0 della griglia 230m/690m è best-of-23. Domanda:
|
||||
**dato il path live vero, qual è il peso SKH ottimale, e quanto vale allineare il cron a 230m?**
|
||||
|
||||
**Script:** `scripts/research/r0724_skh_live_weight.py` — riusa la macchineria di
|
||||
`r0702_anchor_skh01.py` (import diretto di `run_asset`/`sim_equity`/`get5m`/`sanity`, non
|
||||
riscritta). Nessun file di produzione toccato (config/sleeves/live/cron INVARIATI).
|
||||
|
||||
## Verdetto in una riga
|
||||
|
||||
**INVARIATO su entrambi i fronti**: il peso SKH ottimale sul path live, scelto onestamente
|
||||
(mediana de-luckata sui 23 offset, IN-SAMPLE, mai su hold-out), è **0.25 = il peso corrente**
|
||||
(plateau 0.20–0.30); e **allineare il cron ai confini 230m non vale nulla** (Δmediana di banda
|
||||
~+0.01/+0.02 Sharpe): il degrado live è quasi tutto **fill-al-livello vs exit software**,
|
||||
che nessuna cadenza recupera.
|
||||
|
||||
## Setup (onestà dichiarata a priori)
|
||||
|
||||
- **Serie SKH live-path**: tutti i **23 offset** a priori della griglia (ogni 30m su [0,690)) ×
|
||||
3 modi di `sim_equity`: `canonical` (fill al livello sl/tp = backtest), `barclose` (fill al
|
||||
close della barra 230m = ciò che otterrebbe un cron allineato ai confini), `hourly` (fill al
|
||||
close 5m del prossimo multiplo orario = **path del cron reale**). Sanity replica bit-exact
|
||||
del canonico prima di ogni numero.
|
||||
- **Sweep peso**: book = (1−w)·TP01 + w·SKH_live, w ∈ {0, .10, .15, .20, .25, .30, .40, .50}
|
||||
(**8 valori = multiple-testing leggero, dichiarato**; nel gate k_seen=8). TP01 =
|
||||
`al.tp01_baseline_daily()` canonico (come nell'audit).
|
||||
- **Regola di selezione dichiarata PRIMA di vedere i numeri**: w* = argmax della **mediana
|
||||
sui 23 offset dello Sharpe IN-SAMPLE** (pre-2025), lente `hourly`; a parità (<0.03) vince
|
||||
il w più vicino al corrente 0.25. **L'hold-out serve solo a verificare, mai a scegliere.**
|
||||
- **Gate**: `weights_tilt_null` (src/portfolio/portfolio.py) adattato al book a 2 sleeve
|
||||
(null = 500 tilt casuali floor 0.05 / cap SKH 0.50 attorno al 75/25, sui daily del path
|
||||
live hourly).
|
||||
|
||||
## Sanity — riconciliazione con l'audit r0702
|
||||
|
||||
Off0 == `_skyhook_returns()` **bit-exact** (max|Δ|=0.0 su 2688 giorni; l'audit ne aveva 2666 —
|
||||
22 giorni di feed in più, deriva-dati attesa). DD harness BTC 21.4% / ETH 27.4% e n. trade
|
||||
identici al diario di ammissione. Book 50/50 off0:
|
||||
|
||||
| modo | ShFULL | ShIS | ShHOLD | maxDD | audit r0702 |
|
||||
|---|---|---|---|---|---|
|
||||
| canonical | 1.447 | 1.418 | 1.578 | 18.1% | 1.46 / — / 1.64 / 18.1% |
|
||||
| barclose | 1.100 | 1.080 | 1.228 | 23.0% | (non riportato) |
|
||||
| hourly | 1.179 | 1.201 | 1.103 | 24.6% | 1.19 / — / 1.15 / 24.6% |
|
||||
|
||||
TP01 baseline: FULL 1.290 / IS 1.488 / HOLD 0.297 / DD 14.3% (audit ~1.30/~0.31). Blend 75/25
|
||||
canonical off0 HOLD 1.125 (audit ~1.16). **Riconcilia** (Δ ≈ deriva-dati).
|
||||
|
||||
## Sweep peso — lente HOURLY (path live, quella della decisione)
|
||||
|
||||
| w | off0 FULL/IS/HOLD/DD | mediana banda FULL/IS/HOLD/DD | HOLD min..max | DD min..max |
|
||||
|---|---|---|---|---|
|
||||
| 0.00 | 1.29 / 1.49 / 0.30 / 14.3% | 1.29 / 1.49 / 0.30 / 14.3% | — | — |
|
||||
| 0.10 | 1.48 / 1.67 / 0.54 / 11.8% | 1.42 / 1.63 / 0.43 / 11.9% | 0.33..0.56 | 11.3..13.1% |
|
||||
| 0.15 | 1.56 / 1.74 / 0.66 / 11.6% | 1.47 / 1.68 / 0.50 / 11.6% | 0.35..0.70 | 10.7..12.6% |
|
||||
| 0.20 | 1.61 / 1.79 / 0.77 / 11.3% | 1.51 / 1.71 / 0.56 / 11.3% | 0.36..0.82 | 10.1..12.7% |
|
||||
| **0.25** | **1.65 / 1.82 / 0.86 / 11.1%** | **1.53 / 1.72 / 0.62 / 11.2%** | 0.36..0.94 | 9.6..13.6% |
|
||||
| 0.30 | 1.67 / 1.82 / 0.94 / 11.0% | 1.53 / 1.71 / 0.67 / 11.0% | 0.37..1.05 | 9.4..14.6% |
|
||||
| 0.40 | 1.65 / 1.78 / 1.04 / 11.9% | 1.48 / 1.65 / 0.73 / 11.9% | 0.35..1.20 | 10.4..17.3% |
|
||||
| 0.50 | 1.58 / 1.69 / 1.09 / 12.9% | 1.39 / 1.53 / 0.76 / 13.4% | 0.33..1.30 | 11.2..19.9% |
|
||||
|
||||
(La tabella `canonical` di riferimento è nell'output dello script: stessa forma, picco a
|
||||
0.30–0.40 su off0 — l'off0 canonico resta la lente gonfiata già nota.)
|
||||
|
||||
- **Mediana-IS (hourly) per w**: 0.00:1.488 · 0.10:1.627 · 0.15:1.678 · 0.20:1.710 ·
|
||||
**0.25:1.721** · 0.30:1.712 · 0.40:1.645 · 0.50:1.534 → argmax = **0.25**, plateau
|
||||
{0.20, 0.25, 0.30} dentro la tolleranza 0.03 → **w\* = 0.25 = corrente**.
|
||||
- Anche sul path live onesto la gamba SKH **paga**: a w=0.25 la mediana di banda migliora
|
||||
TP01-solo su tutto (FULL 1.29→1.53, IS 1.49→1.72, HOLD 0.30→0.62, DD 14.3%→11.2%) —
|
||||
la diversificazione sopravvive al path orario. Il sizing 25%, deciso a suo tempo su numeri
|
||||
best-of-23, **risulta a posteriori difendibile anche de-luckato sul path vero**.
|
||||
- **Tentazione rifiutata per regola**: su off0 l'HOLD cresce monotono fino a w=0.50 (1.09).
|
||||
Sceglierlo sarebbe selezione-sull'hold-out + fortuna d'ancora (off0 = 98° pctl HOLD
|
||||
nell'audit). La banda IS dice 0.25 e il DD di banda peggiora oltre 0.30.
|
||||
|
||||
## Gate `weights_tilt_null`
|
||||
|
||||
w\* == corrente → **nessun cambio proposto: INVARIATO** (gate non necessario). Eseguito
|
||||
comunque, a titolo informativo, sul miglior w ≠ corrente (0.30):
|
||||
|
||||
- **off0 (griglia live)**: delta_insample +0.007, delta_hold +0.076, pctl_hold 53.6 (<88.9
|
||||
best-of-8) → gate_pass **True ma marginale** (frac_random_beat_hold 0.562: più di metà dei
|
||||
tilt casuali batte il 75/25 sull'hold-out — il claim "SKH aiuta l'hold-out" è generico,
|
||||
non specifico di 0.30).
|
||||
- **off30 (offset IS-mediano)**: delta_insample **−0.009** → gate_pass **False**. Il pass di
|
||||
off0 è (di nuovo) figlio dell'ancora fortunata: il tick +0.01 IS a 0.30 non è robusto
|
||||
alla banda. → 0.30 NON supera la barra onesta; **75/25 resta**.
|
||||
|
||||
## Cadenza 230m — barclose vs hourly (a w=0.25)
|
||||
|
||||
- off0: ΔShFULL **−0.040**, ΔShHOLD +0.068, ΔDD −0.9pt. Banda 23 offset: ΔShFULL med
|
||||
**+0.010** [−0.042,+0.105], ΔShHOLD med **+0.020** [−0.109,+0.162], ΔDD med −0.3pt.
|
||||
- SKH standalone 50/50: barclose−hourly ΔShFULL off0 −0.080 (med +0.015), ΔShHOLD off0
|
||||
+0.125 (med +0.041), ΔDD off0 −1.5pt (med −1.3pt).
|
||||
- **Sorpresa onesta**: a off0 il barclose è perfino PEGGIO dell'hourly su FULL — il ritardo
|
||||
0–50 min del cron orario è **rumore, non un costo sistematico**. Il vero gap è il
|
||||
**fill-al-livello** (canonical−barclose: ΔShFULL off0 +0.347, ΔShHOLD +0.350, ΔDD −4.9pt),
|
||||
cioè l'assunzione del backtest che l'exit avvenga esattamente a sl/tp — e quello **nessun
|
||||
cron lo recupera** (l'exit software in una barra che gappa prende il gap comunque; già
|
||||
misurato dall'audit: sl2% → −11/−23% realizzato nei crash).
|
||||
- Fattibilità (SOLO stima, non cablata): 230=23×10 → i confini 230m cadono sempre su multipli
|
||||
di 10 min dall'epoch, quindi un cron `*/10` che esegue solo quando `now % 230min == 0`
|
||||
colpirebbe ogni confine con ritardo ~0. **Ma non vale la pena**: guadagno mediano ~+0.01/+0.02
|
||||
Sharpe, dentro il rumore, contro 6× invocazioni cron e complessità sul path live certificato.
|
||||
|
||||
## Caveat
|
||||
|
||||
- Exit software nei crash = gap reale (la sim `hourly` è già la stima pessimistica giusta);
|
||||
niente slippage/parziali modellati; equity daily-step (lens Sharpe) come canonico e audit.
|
||||
- 8 valori di w provati e dichiarati (k_seen=8 nel gate); selezione mai sull'hold-out.
|
||||
- La banda 23-offset è la lente de-luckata; live gira UNA sola griglia (off 0).
|
||||
- Deriva-dati vs audit (22 giorni di feed in più): riconciliazione "circa", riportata sopra.
|
||||
- A $600 la granularità del peso non vincola (gamba SKH a w=0.10 ≈ $60/trade ≫ min-order $5).
|
||||
|
||||
## Raccomandazione finale (decisione utente per ogni cambio config)
|
||||
|
||||
1. **Peso SKH del book live Deribit: INVARIATO a 0.25.** È l'ottimo di banda sul path live
|
||||
(plateau 0.20–0.30); nessun w alternativo passa il gate in modo robusto.
|
||||
2. **Cadenza cron: INVARIATA (oraria).** L'allineamento 230m vale ~+0.01/+0.02 Sharpe mediano
|
||||
(rumore); il degrado live è strutturale (fill-al-livello), non di cadenza. Non cablare il
|
||||
cron `*/10`.
|
||||
3. L'aspettativa onesta della gamba SKH live resta quella dell'audit: blend ~fase-mediana +
|
||||
path orario (HOLD di banda ~0.62 a w=0.25, non 1.17), con gap-through-stop nei crash.
|
||||
Il **follow-up dell'audit 2026-07-02 è CHIUSO** (entrambi i punti, esito nullo).
|
||||
|
||||
**Stato:** config/live.json, sleeves.py, scripts/live, cron **INVARIATI** — questo è ricerca;
|
||||
ogni eventuale cambio resta una decisione esplicita dell'utente. Script committabile dopo review.
|
||||
@@ -0,0 +1,281 @@
|
||||
#!/usr/bin/env python
|
||||
"""r0724_skh_live_weight.py — FOLLOW-UP dell'audit anchor 2026-07-02 su SKH01:
|
||||
"cadenza 230m, peso live da rivedere" (diario 2026-07-02-anchor-audit-xs01-skh01.md).
|
||||
|
||||
CONTESTO. Il book live Deribit e' TP01 0.75 + SKH01 0.25 (sleeves.deribit_book_sleeves).
|
||||
L'audit r0702 ha mostrato che il PATH LIVE reale (cron ORARIO `0 * * * *` + exit software,
|
||||
non on-book al livello) degrada SKH01: alla lente 50/50 il book passa FULL 1.46->1.19,
|
||||
HOLD 1.64->1.15, DD 18->25%; nei crash gap-through-stop (sl 2% modellato -> -11/-23%
|
||||
realizzato). In piu' l'offset 0 della griglia 230m/690m e' il 93-98 pctl dei 23 offset a
|
||||
priori (best-of-23). DOMANDA: dato il path live vero, qual e' il peso SKH ottimale del book
|
||||
a 2 sleeve, e quanto vale allineare il cron alla cadenza 230m?
|
||||
|
||||
COSA FA (riusa la macchineria di r0702_anchor_skh01 — import diretto, NON reinventata):
|
||||
1. SANITY: r02.sanity() (off0 == _skyhook_returns bit-exact) + riconciliazione con i
|
||||
numeri dell'audit (50/50 canonical off0 FULL ~1.46 / HOLD ~1.64 / DD ~18.1%; hourly
|
||||
~1.19 / ~1.15 / ~24.6%; blend 75/25 canonical HOLD ~1.16). NB: il feed live aggiunge
|
||||
barre ogni giorno -> attesa una piccola deriva-dati vs i numeri del 2026-07-02.
|
||||
2. Serie SKH live-path: per TUTTI i 23 offset a priori x {canonical, barclose, hourly}
|
||||
(sim_equity di r0702; 'hourly' = fill al close 5m del prossimo multiplo orario = path
|
||||
del cron reale; 'barclose' = fill al close della barra 230m = cio' che otterrebbe un
|
||||
cron allineato ai confini 230m; 'canonical' = fill-al-livello del backtest, riferimento).
|
||||
3. SWEEP PESO: book = (1-w)*TP01 + w*SKH_live, w in {0,.10,.15,.20,.25,.30,.40,.50}
|
||||
(8 valori = multiple-testing leggero, DICHIARATO; nel gate k_seen=8). TP01 =
|
||||
al.tp01_baseline_daily() (canonico, come nell'audit). Per ogni w: Sharpe FULL / IS
|
||||
(pre-2025) / HOLD (2025+) / maxDD, su off0 E min/med/max sui 23 offset.
|
||||
REGOLA DI SELEZIONE (dichiarata A PRIORI, prima di vedere i numeri): w* = argmax
|
||||
della MEDIANA sugli offset dello Sharpe IN-SAMPLE (pre-holdout, lente hourly =
|
||||
path live, de-luckata). A parita' (<0.03 di Sharpe) si preferisce il w piu' VICINO
|
||||
al corrente 0.25 (conservativo). L'hold-out serve SOLO a verificare, non a scegliere.
|
||||
4. GATE weights_tilt_null (src/portfolio/portfolio.py) adattato al book a 2 sleeve:
|
||||
null = tilt casuali cap-respecting (floor 0.05, cap SKH 0.50 = estremo dello sweep)
|
||||
attorno al 75/25 corrente, sui daily del PATH LIVE (hourly, off0 = griglia che gira
|
||||
davvero). k_seen=8 (i w guardati sull'hold-out in questo studio). Se il w* non passa
|
||||
il gate -> verdetto onesto "INVARIATO".
|
||||
5. CADENZA 230m: valore di allineare l'esecuzione ai confini 230m = barclose vs hourly
|
||||
al peso corrente 0.25 (dSharpe FULL/HOLD, dDD), off0 + banda sugli offset. Fattibilita'
|
||||
SOLO STIMATA (non cablata): 230 = 23x10 -> i confini 230m cadono SEMPRE su multipli di
|
||||
10 minuti dall'epoch => un cron a passo 10m che triggera solo quando
|
||||
(now % 230min) == 0 colpisce ogni confine con ritardo ~0 (vs 0-50 min, media 25, del
|
||||
cron orario) -> 'barclose' e' esattamente il suo modello (al netto di runtime/slippage).
|
||||
|
||||
CAVEAT DICHIARATI (onesta'):
|
||||
- l'exit software nei crash prende il GAP (gia' misurato dall'audit: sl2% -> -11/-23%
|
||||
realizzato); la sim 'hourly' e' gia' la stima pessimistica giusta del path live e la
|
||||
cadenza 230m NON recupera il gap canonical->barclose (fill-al-livello: nessun cron
|
||||
riporta l'exit software sul livello SL dentro una barra che gappa);
|
||||
- equity daily-step (lens Sharpe), stessa convenzione del canonico e dell'audit;
|
||||
- nessuna selezione sull'hold-out: w* scelto sull'IS mediano-di-banda, hold-out solo
|
||||
verifica; 8 valori di w provati e dichiarati (k_seen=8 nel gate);
|
||||
- la banda sugli offset e' la lente de-luckata; live gira UNA sola griglia (off 0);
|
||||
- deriva-dati vs audit (il feed cresce ogni giorno): riconciliazione "circa", riportata.
|
||||
|
||||
VINCOLI: nessun file di produzione toccato (config/live.json, sleeves.py, scripts/live,
|
||||
cron INVARIATI); niente commit; dati SOLO dal feed certificato (get5m di r0702 -> load_data).
|
||||
Runtime ~4-8 min (46 run skyhook cachati + 138 sim_equity leggere + sweep + gate).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
ROOT = Path("/opt/docker/PythagorasGoal")
|
||||
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
import r0702_anchor_skh01 as r02 # noqa: E402 (macchineria audit: run_asset/sim_equity/get5m/sanity)
|
||||
import altlib as al # noqa: E402
|
||||
from src.portfolio.portfolio import combine_outer, weights_tilt_null # noqa: E402
|
||||
|
||||
HOLDOUT = r02.HOLDOUT
|
||||
ASSETS = r02.ASSETS
|
||||
OFFSETS = r02.OFFSETS # 23 offset a priori (ogni 30m su [0,690)), 0 = canonico
|
||||
MODES = ("canonical", "barclose", "hourly")
|
||||
W_GRID = (0.0, 0.10, 0.15, 0.20, 0.25, 0.30, 0.40, 0.50) # 8 valori — dichiarati (k_seen=8)
|
||||
W_CURRENT = 0.25
|
||||
TIE_TOL = 0.03 # regola a priori: parita' se |dSh IS| < 0.03 -> w più vicino a 0.25
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Serie daily per (asset, offset, mode) — riusa run_asset + sim_equity di r0702
|
||||
# ---------------------------------------------------------------------------
|
||||
_D: dict = {}
|
||||
|
||||
|
||||
def skh_daily_asset(a: str, off: int, mode: str) -> pd.Series:
|
||||
key = (a, off, mode)
|
||||
if key in _D:
|
||||
return _D[key]
|
||||
_, m0, ltf, ent = r02.run_asset(a, off)
|
||||
df5 = r02.get5m(a)
|
||||
ts5 = df5["timestamp"].values.astype(np.int64) + r02.MS5
|
||||
c5 = df5["close"].values.astype(float)
|
||||
eq = r02.sim_equity(ltf, ent, mode, ts5_close=ts5, c5=c5)
|
||||
if mode == "canonical": # sanity strutturale: re-sim == harness bit-exact
|
||||
dmax = float(np.max(np.abs(eq - m0.equity)))
|
||||
assert dmax < 1e-6, f"re-sim canonica != harness ({a}, off={off}): max|dif|={dmax:.2e}"
|
||||
s = pd.Series(eq, index=pd.DatetimeIndex(pd.to_datetime(ltf["datetime"], utc=True)))
|
||||
_D[key] = s.resample("1D").last().ffill().pct_change().dropna()
|
||||
return _D[key]
|
||||
|
||||
|
||||
def skh_book(off: int, mode: str) -> pd.Series:
|
||||
"""Book 50/50 BTC+ETH daily-step (convenzione di _skyhook_returns / audit)."""
|
||||
J = pd.concat({a: skh_daily_asset(a, off, mode) for a in ASSETS},
|
||||
axis=1, join="inner").fillna(0.0)
|
||||
return pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)
|
||||
|
||||
|
||||
def stats4(s: pd.Series) -> tuple[float, float, float, float]:
|
||||
f, i_, h = r02.sh3(s)
|
||||
return f, i_, h, al._dd_ret(s)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
def main() -> None:
|
||||
t0 = time.time()
|
||||
print("=" * 100)
|
||||
print("r0724 — SKH01 sul PATH LIVE (cron orario): peso ottimale del book Deribit a 2 sleeve")
|
||||
print(f"+ valore della cadenza 230m. 23 offset a priori, fee 0.10% RT, HOLD-OUT >= {HOLDOUT.date()}")
|
||||
print("=" * 100)
|
||||
|
||||
# ---- (1) SANITY + riconciliazione con l'audit r0702 --------------------
|
||||
r02.sanity()
|
||||
b0 = {m: skh_book(0, m) for m in MODES}
|
||||
print("\n[SANITY] riconciliazione col r0702 (2026-07-02; attesa deriva-dati: il feed cresce):")
|
||||
print(f"{'modo':<10} {'ShFULL':>7} {'ShIS':>7} {'ShHOLD':>7} {'maxDD':>7} audit r0702")
|
||||
ref = {"canonical": "FULL 1.46 / HOLD 1.64 / DD 18.1%",
|
||||
"barclose": "(intermedio, isola il fill-al-livello)",
|
||||
"hourly": "FULL 1.19 / HOLD 1.15 / DD 24.6%"}
|
||||
for m in MODES:
|
||||
f, i_, h, dd = stats4(b0[m])
|
||||
print(f"{m:<10} {f:>7.3f} {i_:>7.3f} {h:>7.3f} {dd:>6.1%} {ref[m]}")
|
||||
|
||||
B = al.tp01_baseline_daily()
|
||||
fB, iB, hB, ddB = stats4(B)
|
||||
print(f"\nTP01 baseline canonico (al.tp01_baseline_daily): FULL {fB:.3f} / IS {iB:.3f} / "
|
||||
f"HOLD {hB:.3f} / DD {ddB:.1%} (audit: FULL ~1.30, HOLD ~0.31)")
|
||||
|
||||
def blend(skh: pd.Series, w: float) -> pd.Series:
|
||||
if w == 0.0:
|
||||
return B
|
||||
bl = combine_outer({"TP": B, "SKH": skh}, {"TP": 1.0 - w, "SKH": w})
|
||||
return bl[bl.index >= B.index.min()]
|
||||
|
||||
f, i_, h, dd = stats4(blend(b0["canonical"], 0.25))
|
||||
print(f"blend 75/25 canonical off0: FULL {f:.3f} / HOLD {h:.3f} (audit: HOLD ~1.16)")
|
||||
|
||||
# ---- (2) tutte le serie: 23 offset x 3 modi ----------------------------
|
||||
print(f"\n[{time.time()-t0:5.0f}s] genero i 23 offset x 3 modi (run skyhook cachati)...")
|
||||
SKB: dict[str, dict[int, pd.Series]] = {m: {} for m in MODES}
|
||||
for off in OFFSETS:
|
||||
for m in MODES:
|
||||
SKB[m][off] = b0[m] if off == 0 else skh_book(off, m)
|
||||
fh, ih, hh, ddh = stats4(SKB["hourly"][off])
|
||||
print(f" [{time.time()-t0:5.0f}s] off {off:>3}m: hourly FULL {fh:+.2f} IS {ih:+.2f} "
|
||||
f"HOLD {hh:+.2f} DD {ddh:.0%}")
|
||||
|
||||
# ---- (3) SWEEP PESO ----------------------------------------------------
|
||||
print("\n" + "=" * 100)
|
||||
print(f"(3) SWEEP PESO — book (1-w)*TP01 + w*SKH_live, w in {list(W_GRID)}")
|
||||
print(" (8 valori = multiple-testing leggero, dichiarato; selezione su IS mediano, lente hourly)")
|
||||
print("=" * 100)
|
||||
ST: dict[str, dict[float, dict[int, tuple]]] = {m: {w: {} for w in W_GRID} for m in MODES}
|
||||
for m in MODES:
|
||||
for w in W_GRID:
|
||||
for off in OFFSETS:
|
||||
ST[m][w][off] = stats4(blend(SKB[m][off], w))
|
||||
|
||||
def band(m: str, w: float, k: int) -> tuple[float, float, float]:
|
||||
v = np.array([ST[m][w][off][k] for off in OFFSETS])
|
||||
return float(v.min()), float(np.median(v)), float(v.max())
|
||||
|
||||
for m in ("hourly", "canonical"):
|
||||
lens = "PATH LIVE (lente della decisione)" if m == "hourly" else "riferimento backtest"
|
||||
print(f"\n--- modo {m.upper()} — {lens} ---")
|
||||
print(f"{'w':>5} | {'off0:':<6}{'FULL':>6} {'IS':>6} {'HOLD':>6} {'DD':>6} | "
|
||||
f"{'med:':<5}{'FULL':>6} {'IS':>6} {'HOLD':>6} {'DD':>6} | "
|
||||
f"{'HOLD min..max':>14} | {'DD min..max':>13}")
|
||||
for w in W_GRID:
|
||||
f0, i0, h0, d0 = ST[m][w][0]
|
||||
fm = band(m, w, 0)[1]; im = band(m, w, 1)[1]
|
||||
hmn, hmd, hmx = band(m, w, 2)
|
||||
dmn, dmd, dmx = band(m, w, 3)
|
||||
cur = " <- corrente" if w == W_CURRENT else ""
|
||||
print(f"{w:>5.2f} | {'':<6}{f0:>6.2f} {i0:>6.2f} {h0:>6.2f} {d0:>5.1%} | "
|
||||
f"{'':<5}{fm:>6.2f} {im:>6.2f} {hmd:>6.2f} {dmd:>5.1%} | "
|
||||
f"{hmn:>6.2f}..{hmx:<6.2f} | {dmn:>5.1%}..{dmx:<5.1%}{cur}")
|
||||
|
||||
# ---- (4) selezione (regola a priori) -----------------------------------
|
||||
print("\n--- (4) SELEZIONE del w* (regola dichiarata a priori) ---")
|
||||
med_is = {w: band("hourly", w, 1)[1] for w in W_GRID}
|
||||
best_val = max(med_is.values())
|
||||
ties = [w for w in W_GRID if best_val - med_is[w] < TIE_TOL]
|
||||
wstar = min(ties, key=lambda w: (abs(w - W_CURRENT), w))
|
||||
print("mediana-IS (hourly) per w: " +
|
||||
", ".join(f"{w:.2f}:{med_is[w]:.3f}" for w in W_GRID))
|
||||
print(f"argmax = {max(med_is, key=med_is.get):.2f} ({best_val:.3f}); "
|
||||
f"in parita' (<{TIE_TOL}): {[f'{w:.2f}' for w in ties]} -> "
|
||||
f"w* = {wstar:.2f} (il piu' vicino al corrente {W_CURRENT:.2f})")
|
||||
h_star_med = band("hourly", wstar, 2)[1]; h_cur_med = band("hourly", W_CURRENT, 2)[1]
|
||||
d_star_med = band("hourly", wstar, 3)[1]; d_cur_med = band("hourly", W_CURRENT, 3)[1]
|
||||
print(f"verifica hold-out (mediana banda): w*={wstar:.2f} HOLD {h_star_med:.2f} / DD {d_star_med:.1%}"
|
||||
f" vs corrente 0.25: HOLD {h_cur_med:.2f} / DD {d_cur_med:.1%}")
|
||||
|
||||
# ---- (5) GATE weights_tilt_null ---------------------------------------
|
||||
print("\n--- (5) GATE weights_tilt_null (book a 2 sleeve, daily del PATH LIVE hourly) ---")
|
||||
print("null = tilt casuali floor 0.05 / cap SKH 0.50 attorno al 75/25 corrente; k_seen=8")
|
||||
if wstar == W_CURRENT:
|
||||
print(f"w* == corrente ({W_CURRENT:.2f}) -> nessun cambio proposto: verdetto INVARIATO "
|
||||
f"(gate non necessario). Lo eseguo comunque sul miglior w != corrente, a titolo informativo.")
|
||||
alt = [w for w in W_GRID if w != W_CURRENT and w >= 0.05]
|
||||
w_gate = max(alt, key=lambda w: med_is[w])
|
||||
else:
|
||||
w_gate = wstar
|
||||
# primario: off0 (la griglia che gira davvero live); robustezza: offset con IS mediano a w corrente
|
||||
is_cur = {off: ST["hourly"][W_CURRENT][off][1] for off in OFFSETS}
|
||||
off_med = sorted(OFFSETS, key=lambda o: is_cur[o])[len(OFFSETS) // 2]
|
||||
for tag, off in (("off0 (griglia live)", 0), (f"off{off_med} (offset IS-mediano)", off_med)):
|
||||
cols = {"TP01": B, "SKH01": SKB["hourly"][off]}
|
||||
res = weights_tilt_null(cols, {"TP01": 1 - W_CURRENT, "SKH01": W_CURRENT},
|
||||
{"TP01": 1 - w_gate, "SKH01": w_gate},
|
||||
caps={"SKH01": 0.50}, floor=0.05, n=500, k_seen=8)
|
||||
res.pop("samples", None)
|
||||
print(f" [{tag}] proposto SKH {w_gate:.2f}: " +
|
||||
", ".join(f"{k}={v}" for k, v in res.items()))
|
||||
|
||||
# ---- (6) CADENZA 230m --------------------------------------------------
|
||||
print("\n--- (6) CADENZA 230m — barclose (cron allineato ai confini) vs hourly (cron 0 * * * *) ---")
|
||||
print(f"al peso corrente w={W_CURRENT:.2f}:")
|
||||
rows = []
|
||||
for off in OFFSETS:
|
||||
fb, ib, hb, db = ST["barclose"][W_CURRENT][off]
|
||||
fh, ih, hh, dh = ST["hourly"][W_CURRENT][off]
|
||||
rows.append((fb - fh, hb - hh, db - dh))
|
||||
A = np.array(rows)
|
||||
print(f" off0: dShFULL {A[0,0]:+.3f}, dShHOLD {A[0,1]:+.3f}, dDD {A[0,2]:+.1%}")
|
||||
print(f" banda: dShFULL med {np.median(A[:,0]):+.3f} [{A[:,0].min():+.3f},{A[:,0].max():+.3f}], "
|
||||
f"dShHOLD med {np.median(A[:,1]):+.3f} [{A[:,1].min():+.3f},{A[:,1].max():+.3f}], "
|
||||
f"dDD med {np.median(A[:,2]):+.1%} [{A[:,2].min():+.1%},{A[:,2].max():+.1%}]")
|
||||
# riferimento: SKH standalone 50/50 e quota NON recuperabile (canonical - barclose)
|
||||
rows_s = []
|
||||
for off in OFFSETS:
|
||||
fb, _, hb, db = stats4(SKB["barclose"][off])
|
||||
fh, _, hh, dh = stats4(SKB["hourly"][off])
|
||||
fc, _, hc, dc = stats4(SKB["canonical"][off])
|
||||
rows_s.append((fb - fh, hb - hh, db - dh, fc - fb, hc - hb, dc - db))
|
||||
S = np.array(rows_s)
|
||||
print(f" SKH standalone 50/50: barclose-hourly dShFULL off0 {S[0,0]:+.3f} "
|
||||
f"(med {np.median(S[:,0]):+.3f}), dShHOLD off0 {S[0,1]:+.3f} (med {np.median(S[:,1]):+.3f}), "
|
||||
f"dDD off0 {S[0,2]:+.1%} (med {np.median(S[:,2]):+.1%})")
|
||||
print(f" quota NON recuperabile da nessun cron (canonical-barclose = fill-al-livello): "
|
||||
f"dShFULL off0 {S[0,3]:+.3f} (med {np.median(S[:,3]):+.3f}), "
|
||||
f"dShHOLD off0 {S[0,4]:+.3f} (med {np.median(S[:,4]):+.3f}), "
|
||||
f"dDD off0 {S[0,5]:+.1%} (med {np.median(S[:,5]):+.1%})")
|
||||
print(" fattibilita' (SOLO STIMA, non cablata): 230 = 23x10 -> i confini 230m cadono sempre su")
|
||||
print(" multipli di 10 min dall'epoch => un cron `*/10 * * * *` che esegue solo quando")
|
||||
print(" (now % 230min)==0 colpisce ogni confine con ritardo ~0 (vs 0-50 min, media 25, del cron")
|
||||
print(" orario): 'barclose' e' esattamente il suo modello (al netto di runtime job e slippage).")
|
||||
print(" NB: la cadenza NON recupera il gap fill-al-livello (exit software in barre che gappano).")
|
||||
|
||||
# ---- (7) caveat --------------------------------------------------------
|
||||
print("\n--- (7) CAVEAT (onesta') ---")
|
||||
print("- exit software nei crash = GAP reale (audit: sl2% -> -11/-23% realizzato); 'hourly' e'")
|
||||
print(" la stima pessimistica giusta del path live; niente slippage/parziali modellati;")
|
||||
print("- equity daily-step (lens Sharpe), convenzione identica a canonico e audit r0702;")
|
||||
print("- selezione: w* su IS mediano-di-banda (mai su hold-out); 8 w provati, k_seen=8 nel gate;")
|
||||
print("- banda 23-offset = lente de-luckata; live gira UNA griglia sola (off 0);")
|
||||
print("- deriva-dati vs audit del 2026-07-02 (il feed cresce ogni giorno): riconciliazione 'circa';")
|
||||
print("- a $600 la gamba SKH a w=0.10 vale ~$60/trade (>> min-order $5): la granularita' non vincola.")
|
||||
print(f"\nFatto in {time.time()-t0:.0f}s.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,102 @@
|
||||
"""SNAPSHOT point-in-time supply stablecoin (DefiLlama) — sblocca il lead STABLE fra 12 mesi.
|
||||
|
||||
CONTESTO (2026-07-24, ondata on-chain). STABLE-supply-growth (gate risk-on su crescita 30g
|
||||
della supply stablecoin totale, thr 10% ann.) e' stato l'unico candidato dell'ondata sopra
|
||||
quasi tutti i gate (DSR 0.998, marginale ADDS, non-hedge) ma e' declassato a WATCH perche'
|
||||
la storia DefiLlama e' RICOSTRUITA retroattivamente (chain/coin aggiunte nel tempo): il
|
||||
"totale" visto oggi per il 2019-21 non era osservabile allora -> vintage-risk non sanabile.
|
||||
|
||||
Criterio di rivisita scritto a diario (2026-07-24-onchain-sentiment-wave.md): quando una
|
||||
fonte POINT-IN-TIME della supply accumula >=12 mesi, ritestare il gate thr=10%. Questo
|
||||
script E' quella fonte: cattura oggi cio' che e' osservabile oggi.
|
||||
|
||||
COSA FA (sola lettura, API pubblica tokenless, nessun ordine):
|
||||
- GET https://stablecoins.llama.fi/stablecoins (lista coin + circolante corrente);
|
||||
- 1 riga JSON per run APPESA a data/external/stable_snapshots/snapshots.jsonl:
|
||||
totale USD-peg, totale all-peg, breakdown top-15 per simbolo, n_coins.
|
||||
~1 KB/riga -> ~400 KB/anno. Idempotente per giorno: se esiste gia' una riga con la
|
||||
stessa data UTC esce senza scrivere (safe da ri-run in cron).
|
||||
|
||||
CADENZA RACCOMANDATA (quando/se si decide di cablarla — NON e' in cron adesso):
|
||||
1 run/giorno dopo mezzanotte UTC (es. 00:15), accanto agli altri snapshot.
|
||||
|
||||
USO:
|
||||
uv run python scripts/research/r0724_stable_snapshot.py
|
||||
|
||||
NB ONESTO: questo script MISURA, non decide. Fra >=12 mesi la serie risponde a: il segnale
|
||||
crescita-30g calcolato su dati point-in-time replica quello calcolato sulla storia
|
||||
ricostruita? Se no, il lead STABLE era un artefatto di vintage e muore li'.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
OUT_DIR = PROJECT_ROOT / "data" / "external" / "stable_snapshots"
|
||||
OUT_FILE = OUT_DIR / "snapshots.jsonl"
|
||||
API = "https://stablecoins.llama.fi/stablecoins?includePrices=true"
|
||||
|
||||
|
||||
def fetch() -> dict:
|
||||
last = None
|
||||
for _ in range(4):
|
||||
try:
|
||||
r = requests.get(API, timeout=30)
|
||||
j = r.json()
|
||||
if "peggedAssets" in j:
|
||||
return j
|
||||
last = str(j)[:200]
|
||||
except Exception as e:
|
||||
last = str(e)
|
||||
time.sleep(2.0)
|
||||
raise RuntimeError(f"DefiLlama fail: {last}")
|
||||
|
||||
|
||||
def circ_usd(asset: dict) -> float:
|
||||
c = asset.get("circulating") or {}
|
||||
return float(sum(v for v in c.values() if isinstance(v, (int, float))))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
||||
if OUT_FILE.exists():
|
||||
for line in OUT_FILE.read_text().splitlines():
|
||||
try:
|
||||
if json.loads(line).get("date") == today:
|
||||
print(f"SKIP: snapshot per {today} gia' presente")
|
||||
return
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
data = fetch()
|
||||
assets = data["peggedAssets"]
|
||||
usd_peg = [a for a in assets if a.get("pegType") == "peggedUSD"]
|
||||
tot_usd = sum(circ_usd(a) for a in usd_peg)
|
||||
tot_all = sum(circ_usd(a) for a in assets)
|
||||
top = sorted(usd_peg, key=circ_usd, reverse=True)[:15]
|
||||
|
||||
row = {
|
||||
"date": today,
|
||||
"snap_ts": int(time.time() * 1000),
|
||||
"total_usd_peg": round(tot_usd, 2),
|
||||
"total_all_peg": round(tot_all, 2),
|
||||
"n_coins": len(assets),
|
||||
"n_usd_peg": len(usd_peg),
|
||||
"top15": {a.get("symbol", "?"): round(circ_usd(a), 2) for a in top},
|
||||
"source": "stablecoins.llama.fi/stablecoins",
|
||||
}
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
with OUT_FILE.open("a") as f:
|
||||
f.write(json.dumps(row, separators=(",", ":")) + "\n")
|
||||
print(f"OK {today}: totale USD-peg ${tot_usd/1e9:.2f}B ({len(usd_peg)} coin, "
|
||||
f"{len(assets)} totali) -> {OUT_FILE}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user