research(tp01): DVOL vol-targeting non migliora TP01 (de-levering, non timing)
Angolo ESEGUIBILE (tocca il book live): DVOL (vol implicita forward-looking) come denominatore del vol-target invece della realizzata. Finestra comune 2021-2026. Le varianti DVOL abbassano DD (12.3->9.2%) ma anche Sharpe FULL (0.75->0.70) e CAGR. Controllo decisivo: realized @ vol-tgt 15% eguaglia quel DD (9.4%) a Sharpe piu' alto (0.75) -> il taglio di DD del DVOL e' solo DE-LEVERING, replicabile meglio con un target_vol piu' basso. Hold-out +0.06 = single-window (storia DVOL <5y), sotto la soglia multi-cut. Gate DVOL-spike ridondante col trend (TP01 gia' flat nei crash). Lezione: per meno DD sul live la leva e' target_vol, non un overlay DVOL. - scripts/research/tp01_dvol_overlay.py (realized/dvol/blend/max/derisk + controllo target_vol) - tests/test_tp01_dvol_overlay.py - docs/diary/2026-06-26-tp01-dvol-overlay.md - CLAUDE.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -122,6 +122,16 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
|
|||||||
**LEAD da rivedere a scala (~20k+ e venue con funding eseguibile), non uno sleeve.** Sottoprodotto:
|
**LEAD da rivedere a scala (~20k+ e venue con funding eseguibile), non uno sleeve.** Sottoprodotto:
|
||||||
CC01 passa OGNI gate del marginal scorer → **punto cieco** (manca un gate "Sharpe implausibile →
|
CC01 passa OGNI gate del marginal scorer → **punto cieco** (manca un gate "Sharpe implausibile →
|
||||||
rischio nascosto"; prossima indurita raccomandata). Diario `2026-06-26-cash-carry-hl.md`.
|
rischio nascosto"; prossima indurita raccomandata). Diario `2026-06-26-cash-carry-hl.md`.
|
||||||
|
- **TP01 × DVOL vol-targeting — NON migliora (2026-06-26)** — `scripts/research/tp01_dvol_overlay.py`,
|
||||||
|
test `tests/test_tp01_dvol_overlay.py`. Angolo ESEGUIBILE (tocca il book live, non STAT-MODE):
|
||||||
|
usare il DVOL (vol implicita forward-looking) come denominatore del vol-target di TP01 invece
|
||||||
|
della vol realizzata. Su finestra comune 2021-2026: le varianti DVOL abbassano il DD (12.3%→9.2%)
|
||||||
|
ma anche Sharpe FULL (0.75→0.70) e CAGR (8%→6%). **Controllo decisivo:** il realized a target_vol
|
||||||
|
RIDOTTO (15%) eguaglia quel DD (9.4%) a Sharpe **più alto** (0.75) → il taglio di DD del DVOL è
|
||||||
|
solo **de-levering**, replicabile meglio con un semplice target_vol più basso. L'unico residuo
|
||||||
|
(hold-out +0.06) è single-window (storia DVOL <5 anni) → sotto la soglia multi-cut. Il gate
|
||||||
|
DVOL-spike de-risk è **ridondante col trend** (TP01 già flat nei crash, Δ 0.00). **Lezione: per meno
|
||||||
|
DD sul live la leva è `target_vol`, non un overlay DVOL** (20% resta canonico). Diario `2026-06-26-tp01-dvol-overlay.md`.
|
||||||
- **Soffitto strutturale BTC/ETH-direzionale ~1.3** superato SOLO espandendo a un meccanismo diverso:
|
- **Soffitto strutturale BTC/ETH-direzionale ~1.3** superato SOLO espandendo a un meccanismo diverso:
|
||||||
cross-sectional su universo Hyperliquid certificato (XS01) → portafoglio Sharpe ~1.55.
|
cross-sectional su universo Hyperliquid certificato (XS01) → portafoglio Sharpe ~1.55.
|
||||||
- **Sweep "strategie alternative" (2026-06-20) — 104 ipotesi / 153 agenti / NIENTE di nuovo regge.**
|
- **Sweep "strategie alternative" (2026-06-20) — 104 ipotesi / 153 agenti / NIENTE di nuovo regge.**
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# 2026-06-26 — TP01 × DVOL vol-targeting: NON migliora (il taglio di DD è solo de-levering)
|
||||||
|
|
||||||
|
**Goal utente:** "continua" ad analizzare strategie. Dopo i tre angoli di diversificazione tutti
|
||||||
|
STAT-MODE (gamma scalp scartato, funding cross-sectional già morto, cash-and-carry lead non
|
||||||
|
deployabile), ho puntato sull'angolo a **più alto valore perché ESEGUIBILE**: migliorare il
|
||||||
|
risk-sizing del book **live** TP01 (BTC/ETH perp Deribit, già armato). Branch
|
||||||
|
`research/gamma-scalp-options`, script `scripts/research/tp01_dvol_overlay.py`.
|
||||||
|
|
||||||
|
## Ipotesi
|
||||||
|
|
||||||
|
TP01 vol-targeta sulla vol **realizzata** 30g (backward-looking). Il **DVOL** (vol implicita 30g
|
||||||
|
Deribit, forward-looking, che spesso anticipa i salti di vol) come denominatore → de-risking più
|
||||||
|
tempestivo prima dei crash → DD più basso / hold-out migliore senza affondare il FULL.
|
||||||
|
|
||||||
|
Onestà: DVOL parte 2021-03 → confronto TUTTE le varianti sulla **finestra comune 2021-04 → 2026**
|
||||||
|
(1891g, perdo il toro 2019-2020 pre-DVOL). Causale, fee 0.10% RT, config canonica TP01.
|
||||||
|
|
||||||
|
## Risultati (finestra comune, hold-out 2025+)
|
||||||
|
|
||||||
|
| variante (denominatore vol) | FULL Sh | FULL DD | CAGR | HOLD Sh |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **realized** (baseline) | **+0.75** | 12.3% | +8% | +0.30 |
|
||||||
|
| dvol (implicita) | +0.70 | 9.3% | +6% | +0.33 |
|
||||||
|
| blend 50/50 | +0.72 | 10.4% | +7% | +0.32 |
|
||||||
|
| max(realiz, dvol) | +0.70 | 9.2% | +6% | +0.36 |
|
||||||
|
| derisk (gate DVOL>p90 ×0.5) | +0.75 | 12.3% | +8% | +0.30 |
|
||||||
|
|
||||||
|
Le varianti DVOL **abbassano il DD** (12.3% → ~9.2%) ma **anche** Sharpe FULL (−0.04/−0.05) e CAGR
|
||||||
|
(8% → 6%). Lo Sharpe è scale-invariant → sospetto: il taglio di DD è solo *posizioni più piccole*
|
||||||
|
(l'IV > RV per il VRP → denominatore più grande → leva minore), non vero timing.
|
||||||
|
|
||||||
|
## Il controllo decisivo
|
||||||
|
|
||||||
|
Confronto le varianti DVOL col **realized a target_vol RIDOTTO** (stesso de-levering, senza DVOL):
|
||||||
|
|
||||||
|
| | FULL Sh | DD | CAGR | HOLD Sh |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **max-DVOL** | +0.70 | 9.2% | +6% | +0.36 |
|
||||||
|
| **realized @ vol-tgt 15%** | **+0.75** | 9.4% | +6% | +0.30 |
|
||||||
|
| **realized @ vol-tgt 13%** | +0.75 | **8.1%** | +5% | +0.30 |
|
||||||
|
|
||||||
|
**Conclusivo:** il realized a 15% eguaglia il DD del max-DVOL (9.4% ≈ 9.2%) a Sharpe **più alto**
|
||||||
|
(0.75 > 0.70) e stesso CAGR; a 13% fa DD ancora più basso (8.1%), sempre a 0.75. **Il taglio di DD
|
||||||
|
del DVOL è interamente "leva minore", replicabile MEGLIO con un semplice target_vol più basso.**
|
||||||
|
|
||||||
|
## Verdetto
|
||||||
|
|
||||||
|
**DVOL vol-targeting NON migliora TP01.** Risk-adjusted è marginalmente *peggiore* (FULL 0.70 vs
|
||||||
|
0.75 a parità di DD). L'unico residuo a favore — hold-out +0.06 (max 0.36 vs 0.30) — è **single-window**
|
||||||
|
su 5 anni scarsi di storia DVOL: sotto la soglia di **persistenza multi-cut** del progetto (la lezione
|
||||||
|
che ha ucciso 13/14 falsi positivi nello sweep alt). Il gate **DVOL-spike de-risk è RIDONDANTE col
|
||||||
|
trend**: quando il DVOL esplode (crash) TP01 è già flat (momentum negativo → posizione 0) → il gate
|
||||||
|
non tocca nulla (Δ 0.00 su tutto). Niente da deployare.
|
||||||
|
|
||||||
|
## Lezioni
|
||||||
|
|
||||||
|
1. **Per meno DD sul book live, la leva giusta è `target_vol`, non un overlay DVOL.** Abbassare TP01
|
||||||
|
da vol-tgt 20% → 15% taglia il DD 12.3% → ~9.4% allo **stesso Sharpe** (0.75). Dial pulito e noto,
|
||||||
|
se mai si volesse un profilo più difensivo (oggi 20% è la scelta canonica — invariata).
|
||||||
|
2. **La vol implicita non batte la realizzata come denominatore di sizing per un trend long-flat**:
|
||||||
|
il segnale di trend gestisce già il de-risking direzionale; il denominatore fissa solo la
|
||||||
|
magnitudine, e la realizzata lo fa marginalmente meglio per unità di rischio.
|
||||||
|
3. Conferma indiretta del soffitto: anche un raffinamento "ovvio" e *eseguibile* su TP01 non produce
|
||||||
|
alpha; al massimo sposta il punto sulla stessa frontiera rischio/rendimento.
|
||||||
|
|
||||||
|
SCARTATO come miglioria. Nessun impatto sul book live (branch separato, config canonica invariata).
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
"""TP01 × DVOL — la vol IMPLICITA (forward-looking) migliora il risk-sizing di TP01? (ESEGUIBILE)
|
||||||
|
|
||||||
|
A differenza degli sleeve diversificatori (XS01/VRP01/carry = STAT-MODE, non eseguibili a $600),
|
||||||
|
questo TOCCA il book live: TP01 è BTC/ETH perp su Deribit, già armato. Oggi vol-targeta sulla vol
|
||||||
|
REALIZZATA 30g (backward-looking). Ipotesi: il DVOL (vol implicita 30g Deribit, forward-looking,
|
||||||
|
che spesso ANTICIPA i salti di vol realizzata) come denominatore del vol-target → de-risking più
|
||||||
|
tempestivo prima dei crash → hold-out migliore e/o DD più basso, SENZA peggiorare il FULL.
|
||||||
|
|
||||||
|
Onestà: DVOL parte 2021-03 → confronto TUTTE le varianti sulla FINESTRA COMUNE 2021-2026 (perdo
|
||||||
|
2018-2021, incluso il toro 2021 pre-DVOL). Baseline ricalcolato sulla stessa finestra. Hold-out 2025+.
|
||||||
|
Tutto causale (vol/segnale ≤ close[i]), fee 0.10% RT, long-flat, leva cap 2x — config CANONICA TP01.
|
||||||
|
|
||||||
|
VARIANTI (denominatore del vol-target):
|
||||||
|
REALIZED -> 30g realizzata (baseline canonica)
|
||||||
|
DVOL -> DVOL/100 (implicita)
|
||||||
|
BLEND -> 0.5·realizzata + 0.5·DVOL
|
||||||
|
MAX -> max(realizzata, DVOL) (sizing più difensivo: la più alta delle due)
|
||||||
|
DERISK -> realizzata, ma posizione ×0.5 quando DVOL > pctl espandente causale (gate crash)
|
||||||
|
|
||||||
|
uv run python scripts/research/tp01_dvol_overlay.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
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))
|
||||||
|
|
||||||
|
from src.data.downloader import load_data
|
||||||
|
from src.strategies.trend_portfolio import (
|
||||||
|
resample_1d, simple_returns, realized_vol, tsmom_blend, CANONICAL,
|
||||||
|
)
|
||||||
|
|
||||||
|
RAW = ROOT / "data" / "raw"
|
||||||
|
SQ = np.sqrt(365.25)
|
||||||
|
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
|
||||||
|
TGT_VOL = CANONICAL["target_vol"]; LEV = CANONICAL["leverage"]; FEE = CANONICAL["fee_side"]
|
||||||
|
HZ = CANONICAL["horizons_days"]; VW = CANONICAL["vol_win_days"]
|
||||||
|
|
||||||
|
|
||||||
|
def _components(asset: str):
|
||||||
|
df = resample_1d(load_data(asset, "1h"))
|
||||||
|
c = df["close"].values.astype(float)
|
||||||
|
idx = pd.to_datetime(df["datetime"])
|
||||||
|
if idx.dt.tz is None:
|
||||||
|
idx = idx.dt.tz_localize("UTC")
|
||||||
|
idx = pd.DatetimeIndex(idx) # tz-aware (UTC)
|
||||||
|
r = simple_returns(c)
|
||||||
|
rv = realized_vol(r, VW, 365.25) # 30g realizzata annualizzata (bpd=1)
|
||||||
|
direction = np.clip(tsmom_blend(c, HZ), 0, None) # long-flat
|
||||||
|
dv = pd.read_parquet(RAW / f"dvol_{asset.lower()}.parquet")
|
||||||
|
dser = pd.Series(dv["close"].values.astype(float) / 100.0,
|
||||||
|
index=pd.to_datetime(dv["timestamp"], unit="ms", utc=True)).sort_index()
|
||||||
|
dvol = dser.reindex(idx, method="ffill").values
|
||||||
|
return c, r, idx, rv, direction, dvol
|
||||||
|
|
||||||
|
|
||||||
|
def _net_returns(asset: str, mode: str, tvol: float = TGT_VOL) -> pd.Series:
|
||||||
|
c, r, idx, rv, direction, dvol = _components(asset)
|
||||||
|
derisk = np.ones(len(c))
|
||||||
|
if mode == "realized":
|
||||||
|
vol = rv
|
||||||
|
elif mode == "dvol":
|
||||||
|
vol = dvol
|
||||||
|
elif mode == "blend":
|
||||||
|
vol = 0.5 * rv + 0.5 * dvol
|
||||||
|
elif mode == "max":
|
||||||
|
vol = np.fmax(rv, dvol)
|
||||||
|
elif mode == "derisk":
|
||||||
|
vol = rv
|
||||||
|
# gate crash causale: DVOL sopra il suo percentile espandente (90%) -> dimezza l'esposizione
|
||||||
|
dd = pd.Series(dvol, index=idx)
|
||||||
|
rank = dd.expanding(min_periods=60).apply(lambda x: (x[:-1] < x[-1]).mean() if len(x) > 1 else 0.5, raw=True)
|
||||||
|
derisk = np.where(rank.values > 0.90, 0.5, 1.0)
|
||||||
|
else:
|
||||||
|
raise ValueError(mode)
|
||||||
|
with np.errstate(divide="ignore", invalid="ignore"):
|
||||||
|
scal = np.where((vol > 0) & np.isfinite(vol), tvol / vol, 0.0)
|
||||||
|
tgt = np.clip(direction * scal * derisk, -LEV, LEV)
|
||||||
|
tgt[~np.isfinite(tgt)] = 0.0
|
||||||
|
pos = np.zeros(len(tgt)); pos[1:] = tgt[:-1] # decisa a close[t-1], tenuta in t
|
||||||
|
gross = pos * r
|
||||||
|
turn = np.abs(np.diff(pos, prepend=0.0))
|
||||||
|
net = np.clip(gross - FEE * turn, -0.99, None); net[0] = 0.0
|
||||||
|
return pd.Series(net, index=idx)
|
||||||
|
|
||||||
|
|
||||||
|
def portfolio(mode: str, tvol: float = TGT_VOL) -> pd.Series:
|
||||||
|
b = _net_returns("BTC", mode, tvol); e = _net_returns("ETH", mode, tvol)
|
||||||
|
J = pd.concat({"B": b, "E": e}, axis=1, join="inner").fillna(0.0)
|
||||||
|
return 0.5 * J["B"] + 0.5 * J["E"]
|
||||||
|
|
||||||
|
|
||||||
|
def metrics(daily: pd.Series, lo=None) -> dict:
|
||||||
|
if lo is not None:
|
||||||
|
daily = daily[daily.index >= lo]
|
||||||
|
r = daily.values
|
||||||
|
sh = float(np.mean(r) / np.std(r) * SQ) if np.std(r) > 0 else 0.0
|
||||||
|
eq = np.cumprod(1.0 + r); pk = np.maximum.accumulate(eq)
|
||||||
|
dd = float(np.max((pk - eq) / pk)) if len(eq) else 0.0
|
||||||
|
yrs = (daily.index[-1] - daily.index[0]).days / 365.25 if len(daily) > 1 else 1.0
|
||||||
|
cagr = eq[-1] ** (1 / yrs) - 1 if yrs > 0 and len(eq) and eq[-1] > 0 else -1.0
|
||||||
|
s = pd.Series(eq, index=daily.index); yearly = {}
|
||||||
|
for y, g in s.groupby(s.index.year):
|
||||||
|
if len(g) > 1:
|
||||||
|
yearly[int(y)] = float(g.iloc[-1] / g.iloc[0] - 1)
|
||||||
|
return dict(sharpe=sh, dd=dd, cagr=cagr, tot=float(eq[-1] - 1) if len(eq) else 0.0, yearly=yearly)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
modes = ["realized", "dvol", "blend", "max", "derisk"]
|
||||||
|
series = {m: portfolio(m) for m in modes}
|
||||||
|
# vero inizio DVOL (dove TUTTE le varianti hanno dati validi) — non il primo indice del prezzo
|
||||||
|
dstart = max(pd.read_parquet(RAW / f"dvol_{a.lower()}.parquet")["timestamp"].min() for a in ("BTC", "ETH"))
|
||||||
|
dstart = pd.Timestamp(dstart, unit="ms", tz="UTC") + pd.Timedelta(days=VW) # +warmup vol-win
|
||||||
|
series = {m: s[s.index >= dstart] for m, s in series.items()}
|
||||||
|
common = series["realized"].index
|
||||||
|
base = series["realized"]
|
||||||
|
|
||||||
|
print("=" * 96)
|
||||||
|
print(f" TP01 × DVOL — vol-target con denominatore di vol diverso. Finestra COMUNE "
|
||||||
|
f"{common[0].date()} -> {common[-1].date()} ({len(base)}g). Hold-out 2025+.")
|
||||||
|
print("=" * 96)
|
||||||
|
print(f" {'variante':<10} {'FULL Sh':>8} {'FULL DD':>8} {'CAGR':>7} | "
|
||||||
|
f"{'HOLD Sh':>8} {'HOLD ret':>9} {'HOLD DD':>8} | per-anno PnL")
|
||||||
|
for m in modes:
|
||||||
|
s = series[m]; f = metrics(s); h = metrics(s, lo=HOLDOUT)
|
||||||
|
ys = " ".join(f"{y}:{p*100:+.0f}" for y, p in sorted(f['yearly'].items()))
|
||||||
|
tag = " (baseline)" if m == "realized" else ""
|
||||||
|
print(f" {m:<10} {f['sharpe']:>+8.2f} {f['dd']*100:>7.1f}% {f['cagr']*100:>+6.0f}% | "
|
||||||
|
f"{h['sharpe']:>+8.2f} {h['tot']*100:>+8.1f}% {h['dd']*100:>7.1f}% | {ys}{tag}")
|
||||||
|
|
||||||
|
print("\n DELTA vs baseline (realized) sulla stessa finestra:")
|
||||||
|
bf = metrics(base); bh = metrics(base, lo=HOLDOUT)
|
||||||
|
for m in modes:
|
||||||
|
if m == "realized":
|
||||||
|
continue
|
||||||
|
f = metrics(series[m]); h = metrics(series[m], lo=HOLDOUT)
|
||||||
|
print(f" {m:<8}: ΔFULL Sh {f['sharpe']-bf['sharpe']:+.2f} ΔFULL DD {(f['dd']-bf['dd'])*100:+.1f}pp "
|
||||||
|
f"ΔHOLD Sh {h['sharpe']-bh['sharpe']:+.2f} ΔHOLD ret {(h['tot']-bh['tot'])*100:+.1f}pp")
|
||||||
|
|
||||||
|
print("\n CONTROLLO DECISIVO — il taglio di DD del DVOL è 'posizioni più piccole' o vero timing?")
|
||||||
|
print(" Confronto le varianti DVOL con il realized a target_vol RIDOTTO (stesso de-levering, ma")
|
||||||
|
print(" senza DVOL). Se realized-ridotto eguaglia/batte il DVOL a parità di DD → DVOL non aggiunge.")
|
||||||
|
for tv in (0.15, 0.13):
|
||||||
|
s = portfolio("realized", tvol=tv); s = s[s.index >= dstart]
|
||||||
|
f = metrics(s); h = metrics(s, lo=HOLDOUT)
|
||||||
|
print(f" realized @ vol-tgt {tv*100:.0f}%: FULL Sh {f['sharpe']:+.2f} DD {f['dd']*100:.1f}% "
|
||||||
|
f"CAGR {f['cagr']*100:+.0f}% | HOLD Sh {h['sharpe']:+.2f}")
|
||||||
|
mx = metrics(series["max"]); mxh = metrics(series["max"], lo=HOLDOUT)
|
||||||
|
print(f" (vs max-DVOL: FULL Sh {mx['sharpe']:+.2f} DD {mx['dd']*100:.1f}% "
|
||||||
|
f"CAGR {mx['cagr']*100:+.0f}% | HOLD Sh {mxh['sharpe']:+.2f})")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"""Lock della conclusione 2026-06-26: il DVOL come denominatore del vol-target NON migliora TP01
|
||||||
|
risk-adjusted. Il taglio di DD delle varianti DVOL è solo de-levering, replicabile MEGLIO con un
|
||||||
|
target_vol più basso sul realizzato. Diario docs/diary/2026-06-26-tp01-dvol-overlay.md."""
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||||
|
|
||||||
|
from tp01_dvol_overlay import portfolio, metrics, RAW, VW # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def _dstart():
|
||||||
|
s = max(pd.read_parquet(RAW / f"dvol_{a.lower()}.parquet")["timestamp"].min() for a in ("BTC", "ETH"))
|
||||||
|
return pd.Timestamp(s, unit="ms", tz="UTC") + pd.Timedelta(days=VW)
|
||||||
|
|
||||||
|
|
||||||
|
def test_dvol_does_not_beat_realized_risk_adjusted():
|
||||||
|
"""Sulla finestra comune, il realized batte (o eguaglia) le varianti DVOL sullo Sharpe FULL."""
|
||||||
|
lo = _dstart()
|
||||||
|
base = metrics(portfolio("realized")[portfolio("realized").index >= lo])
|
||||||
|
mx = metrics(portfolio("max")[portfolio("max").index >= lo])
|
||||||
|
dv = metrics(portfolio("dvol")[portfolio("dvol").index >= lo])
|
||||||
|
assert base["sharpe"] >= mx["sharpe"] - 1e-9
|
||||||
|
assert base["sharpe"] >= dv["sharpe"] - 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
def test_lower_target_vol_replicates_dd_cut_at_better_sharpe():
|
||||||
|
"""Il taglio di DD del DVOL è solo de-levering: realized @ vol-tgt 15% eguaglia il DD del
|
||||||
|
max-DVOL a Sharpe FULL non inferiore (anzi superiore)."""
|
||||||
|
lo = _dstart()
|
||||||
|
mx = metrics(portfolio("max")[portfolio("max").index >= lo])
|
||||||
|
lo_tv = metrics(portfolio("realized", tvol=0.15)[portfolio("realized", tvol=0.15).index >= lo])
|
||||||
|
assert lo_tv["dd"] <= mx["dd"] + 0.01 # stesso DD (entro 1pp)
|
||||||
|
assert lo_tv["sharpe"] >= mx["sharpe"] # a Sharpe non inferiore
|
||||||
|
|
||||||
|
|
||||||
|
def test_dvol_spike_gate_is_redundant_with_trend():
|
||||||
|
"""Il gate DVOL-spike de-risk non cambia nulla: TP01 è già flat nei crash (momentum<0)."""
|
||||||
|
lo = _dstart()
|
||||||
|
base = portfolio("realized"); dr = portfolio("derisk")
|
||||||
|
base = base[base.index >= lo]; dr = dr[dr.index >= lo]
|
||||||
|
assert abs(metrics(base)["sharpe"] - metrics(dr)["sharpe"]) < 0.02
|
||||||
Reference in New Issue
Block a user