refactor(core): IV-RV adattivo distinct-days policy + backfill Deribit
Sblocca il warmup hard del gate IV-RV adattivo (~21 giorni residui)
permettendo di mischiare cadenze diverse (tick live 15min + backfill
giornaliero) senza assumere il fattore costante 96 tick/giorno.
API change (no backwards-compat shims):
* compute_adaptive_threshold(history, *, n_days, percentile,
absolute_floor): rimossi `min_days`/`target_days`. La selezione
finestra (target_days/min_days/intera storia) si sposta al caller.
Warmup hard quando `n_days == 0`.
* repository: rimosso `iv_rv_history`; aggiunti
`count_iv_rv_distinct_days` (COUNT DISTINCT substr(ts,1,10)) e
`iv_rv_values_for_window`.
* EntryContext aggiunge `iv_rv_n_days: int = 0`. entry_cycle calcola
n_days, sceglie window_days e popola il context. Audit
`iv_rv_n_days` reale (non più len/96).
* GUI Calibrazione: counter giorni distinti tramite set di date.
* Spec aggiornata con errata 2026-05-10 e nuova warmup table.
Backfill (scripts/backfill_iv_rv.py, stdlib-only):
* Fetch DVOL daily + ETH/BTC-PERPETUAL closes da Deribit public REST.
* Calcolo RV30d annualizzato (stdev log-return × √365 × 100).
* INSERT OR REPLACE in market_snapshots con timestamp 12:00 UTC e
fetch_errors_json='{"backfill":true}' per distinzione audit.
* Compute layer testato (9 test): RV su prezzi costanti/monotoni/
alternati, build_records con cutoff e missing data.
Verifica live post-deploy (10 mag 2026 08:50 UTC):
* ETH: n_days=46, P25=2.21 vol pt, IV-RV=10.05 → gate PASS
* BTC: n_days=46, P25=5.69 vol pt, IV-RV=8.60 → gate PASS
509 test passati (500 esistenti + 9 backfill), ruff pulito.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -316,33 +316,14 @@ async def _build_quotes(
|
||||
return out
|
||||
|
||||
|
||||
def _audit_threshold(
|
||||
entry_cfg: object,
|
||||
iv_rv_history: tuple[Decimal, ...],
|
||||
) -> str | None:
|
||||
"""Soglia P_q rolling effettivamente usata dal gate, per il decisions log."""
|
||||
if not getattr(entry_cfg, "iv_minus_rv_filter_enabled", False):
|
||||
return None
|
||||
if not getattr(entry_cfg, "iv_minus_rv_adaptive_enabled", False):
|
||||
return str(getattr(entry_cfg, "iv_minus_rv_min", Decimal("0")))
|
||||
threshold = compute_adaptive_threshold(
|
||||
history=iv_rv_history,
|
||||
percentile=entry_cfg.iv_minus_rv_percentile, # type: ignore[attr-defined]
|
||||
absolute_floor=entry_cfg.iv_minus_rv_min, # type: ignore[attr-defined]
|
||||
min_days=entry_cfg.iv_minus_rv_window_min_days, # type: ignore[attr-defined]
|
||||
target_days=entry_cfg.iv_minus_rv_window_target_days, # type: ignore[attr-defined]
|
||||
)
|
||||
return None if threshold is None else str(threshold)
|
||||
def _select_window_days(entry_cfg: object, n_days: int) -> int:
|
||||
"""Sceglie la finestra in giorni per il gate adattivo dato n_days
|
||||
disponibili.
|
||||
|
||||
|
||||
def _audit_window_days(
|
||||
entry_cfg: object,
|
||||
iv_rv_history: tuple[Decimal, ...],
|
||||
) -> int | None:
|
||||
"""Numero di giorni effettivamente usati dalla finestra rolling."""
|
||||
if not getattr(entry_cfg, "iv_minus_rv_adaptive_enabled", False):
|
||||
return None
|
||||
n_days = len(iv_rv_history) // 96
|
||||
Spec: warmup hard se ``n_days == 0`` → 0; finestra ``target_days``
|
||||
se ``n_days >= target_days``; ``min_days`` se ``n_days >= min_days``;
|
||||
altrimenti tutta la storia disponibile (capped a ``target_days``).
|
||||
"""
|
||||
target = int(getattr(entry_cfg, "iv_minus_rv_window_target_days", 60))
|
||||
min_days = int(getattr(entry_cfg, "iv_minus_rv_window_min_days", 30))
|
||||
if n_days < 1:
|
||||
@@ -351,7 +332,33 @@ def _audit_window_days(
|
||||
return target
|
||||
if n_days >= min_days:
|
||||
return min_days
|
||||
return n_days
|
||||
return target # storia parziale: query fino a target, repository ne ritorna n_days
|
||||
|
||||
|
||||
def _audit_threshold(
|
||||
entry_cfg: object,
|
||||
iv_rv_history: tuple[Decimal, ...],
|
||||
n_days: int,
|
||||
) -> str | None:
|
||||
"""Soglia P_q rolling effettivamente usata dal gate, per il decisions log."""
|
||||
if not getattr(entry_cfg, "iv_minus_rv_filter_enabled", False):
|
||||
return None
|
||||
if not getattr(entry_cfg, "iv_minus_rv_adaptive_enabled", False):
|
||||
return str(getattr(entry_cfg, "iv_minus_rv_min", Decimal("0")))
|
||||
threshold = compute_adaptive_threshold(
|
||||
history=iv_rv_history,
|
||||
n_days=n_days,
|
||||
percentile=entry_cfg.iv_minus_rv_percentile, # type: ignore[attr-defined]
|
||||
absolute_floor=entry_cfg.iv_minus_rv_min, # type: ignore[attr-defined]
|
||||
)
|
||||
return None if threshold is None else str(threshold)
|
||||
|
||||
|
||||
def _audit_window_days(entry_cfg: object, n_days: int) -> int | None:
|
||||
"""Numero di giorni effettivamente usati dalla finestra rolling."""
|
||||
if not getattr(entry_cfg, "iv_minus_rv_adaptive_enabled", False):
|
||||
return None
|
||||
return _select_window_days(entry_cfg, n_days)
|
||||
|
||||
|
||||
def _max_loss_per_contract_usd(short_strike: Decimal, long_strike: Decimal) -> Decimal:
|
||||
@@ -472,18 +479,27 @@ async def run_entry_cycle(
|
||||
asset = cfg.asset.symbol
|
||||
|
||||
iv_rv_history: tuple[Decimal, ...] = ()
|
||||
iv_rv_n_days: int = 0
|
||||
dvol_24h_ago: Decimal | None = None
|
||||
if entry_cfg.iv_minus_rv_filter_enabled and entry_cfg.iv_minus_rv_adaptive_enabled:
|
||||
conn = connect_state(ctx.db_path)
|
||||
try:
|
||||
iv_rv_history = tuple(
|
||||
ctx.repository.iv_rv_history(
|
||||
conn,
|
||||
asset=asset,
|
||||
max_days=entry_cfg.iv_minus_rv_window_target_days,
|
||||
as_of=when,
|
||||
)
|
||||
iv_rv_n_days = ctx.repository.count_iv_rv_distinct_days(
|
||||
conn,
|
||||
asset=asset,
|
||||
max_days=entry_cfg.iv_minus_rv_window_target_days,
|
||||
as_of=when,
|
||||
)
|
||||
window_days = _select_window_days(entry_cfg, iv_rv_n_days)
|
||||
if window_days > 0:
|
||||
iv_rv_history = tuple(
|
||||
ctx.repository.iv_rv_values_for_window(
|
||||
conn,
|
||||
asset=asset,
|
||||
window_days=window_days,
|
||||
as_of=when,
|
||||
)
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
if entry_cfg.vol_of_vol_guard_enabled:
|
||||
@@ -508,6 +524,7 @@ async def run_entry_cycle(
|
||||
iv_minus_rv=snap.iv_minus_rv,
|
||||
liquidation_squeeze_risk_high=snap.liquidation_squeeze_risk_high,
|
||||
iv_rv_history=iv_rv_history,
|
||||
iv_rv_n_days=iv_rv_n_days,
|
||||
dvol_24h_ago=dvol_24h_ago,
|
||||
)
|
||||
decision = validate_entry(entry_ctx, cfg)
|
||||
@@ -529,9 +546,12 @@ async def run_entry_cycle(
|
||||
str(snap.iv_minus_rv) if snap.iv_minus_rv is not None else None
|
||||
),
|
||||
"iv_rv_history_n": len(iv_rv_history),
|
||||
"iv_rv_threshold_used": _audit_threshold(entry_cfg, iv_rv_history),
|
||||
"iv_rv_n_days": iv_rv_n_days,
|
||||
"iv_rv_threshold_used": _audit_threshold(
|
||||
entry_cfg, iv_rv_history, iv_rv_n_days
|
||||
),
|
||||
"iv_rv_window_used_days": _audit_window_days(
|
||||
entry_cfg, iv_rv_history
|
||||
entry_cfg, iv_rv_n_days
|
||||
),
|
||||
"dvol_24h_ago": (
|
||||
str(dvol_24h_ago) if dvol_24h_ago is not None else None
|
||||
|
||||
Reference in New Issue
Block a user