feat(gui): pannello informativo Gate IV-RV adattivo in Calibrazione
Mostra status (warmup/attivo), soglia P25 rolling corrente, IV-RV ultimo tick, floor assoluto, decisione hypothetical e sezione Vol-of-Vol guard. Read-only: i percentili statici esistenti restano per analisi. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ from __future__ import annotations
|
|||||||
import os
|
import os
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
from decimal import Decimal
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@@ -23,6 +24,7 @@ import plotly.graph_objects as go
|
|||||||
import streamlit as st
|
import streamlit as st
|
||||||
|
|
||||||
from cerbero_bite.config.loader import load_strategy
|
from cerbero_bite.config.loader import load_strategy
|
||||||
|
from cerbero_bite.core.adaptive_threshold import compute_adaptive_threshold
|
||||||
from cerbero_bite.gui.data_layer import (
|
from cerbero_bite.gui.data_layer import (
|
||||||
DEFAULT_DB_PATH,
|
DEFAULT_DB_PATH,
|
||||||
humanize_dt,
|
humanize_dt,
|
||||||
@@ -242,6 +244,131 @@ def _render_metric(spec: MetricSpec, records: list[MarketSnapshotRecord]) -> Non
|
|||||||
_percentiles_strip(s)
|
_percentiles_strip(s)
|
||||||
|
|
||||||
|
|
||||||
|
def _render_adaptive_gate_panel(
|
||||||
|
strategy: object | None,
|
||||||
|
records: list[MarketSnapshotRecord],
|
||||||
|
) -> None:
|
||||||
|
"""Pannello informativo sul gate IV-RV adattivo (read-only)."""
|
||||||
|
if strategy is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
entry = strategy.entry # type: ignore[attr-defined]
|
||||||
|
except AttributeError:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not getattr(entry, "iv_minus_rv_filter_enabled", False):
|
||||||
|
st.subheader("🎯 Gate IV-RV adattivo")
|
||||||
|
st.info("Gate IV-RV disabilitato nel profilo corrente.")
|
||||||
|
st.divider()
|
||||||
|
return
|
||||||
|
|
||||||
|
st.subheader("🎯 Gate IV-RV adattivo")
|
||||||
|
|
||||||
|
# records DESC (newest first) → history ASC con NULL/fetch_ok=0 esclusi
|
||||||
|
iv_rv_history: list[Decimal] = []
|
||||||
|
for r in reversed(records):
|
||||||
|
if r.fetch_ok and r.iv_minus_rv is not None:
|
||||||
|
iv_rv_history.append(r.iv_minus_rv)
|
||||||
|
|
||||||
|
n_ticks = len(iv_rv_history)
|
||||||
|
target = int(getattr(entry, "iv_minus_rv_window_target_days", 60))
|
||||||
|
min_days = int(getattr(entry, "iv_minus_rv_window_min_days", 30))
|
||||||
|
n_days = n_ticks // 96
|
||||||
|
|
||||||
|
if n_days < 1:
|
||||||
|
status = f"🟡 Warmup hard ({n_ticks}/96 tick)"
|
||||||
|
elif n_days < min_days:
|
||||||
|
status = f"🟡 Warmup ({n_days}/{min_days}g — finestra crescente)"
|
||||||
|
elif n_days < target:
|
||||||
|
status = f"🟢 Attivo (finestra {min_days}g, target {target}g)"
|
||||||
|
else:
|
||||||
|
status = f"🟢 Attivo (finestra stabile {target}g)"
|
||||||
|
st.markdown(f"**Status:** {status}")
|
||||||
|
|
||||||
|
# Latest tick
|
||||||
|
iv_rv_now: Decimal | None = None
|
||||||
|
dvol_now: Decimal | None = None
|
||||||
|
latest_ts: datetime | None = None
|
||||||
|
for r in records: # records DESC
|
||||||
|
if r.fetch_ok:
|
||||||
|
iv_rv_now = r.iv_minus_rv
|
||||||
|
dvol_now = r.dvol
|
||||||
|
latest_ts = r.timestamp
|
||||||
|
break
|
||||||
|
|
||||||
|
adaptive_on = bool(getattr(entry, "iv_minus_rv_adaptive_enabled", False))
|
||||||
|
floor = Decimal(str(getattr(entry, "iv_minus_rv_min", "0")))
|
||||||
|
|
||||||
|
if adaptive_on:
|
||||||
|
percentile = Decimal(
|
||||||
|
str(getattr(entry, "iv_minus_rv_percentile", "0.25"))
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
threshold = compute_adaptive_threshold(
|
||||||
|
history=iv_rv_history,
|
||||||
|
percentile=percentile,
|
||||||
|
absolute_floor=floor,
|
||||||
|
min_days=min_days,
|
||||||
|
target_days=target,
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
st.warning(f"Configurazione gate non valida: {exc}")
|
||||||
|
threshold = None
|
||||||
|
|
||||||
|
c1, c2, c3 = st.columns(3)
|
||||||
|
pct_label = int(percentile * 100)
|
||||||
|
c1.metric(
|
||||||
|
f"Soglia P{pct_label} rolling",
|
||||||
|
f"{threshold:.2f}" if threshold is not None else "—",
|
||||||
|
help="Soglia adattiva = max(percentile, floor)",
|
||||||
|
)
|
||||||
|
c2.metric(
|
||||||
|
"IV-RV ultimo tick",
|
||||||
|
f"{iv_rv_now:.2f}" if iv_rv_now is not None else "—",
|
||||||
|
)
|
||||||
|
c3.metric("Floor assoluto", f"{floor:.2f}")
|
||||||
|
|
||||||
|
if threshold is not None and iv_rv_now is not None:
|
||||||
|
verdict = "✅ PASS" if iv_rv_now >= threshold else "❌ SKIP"
|
||||||
|
st.markdown(f"**Decisione hypothetical:** {verdict}")
|
||||||
|
else:
|
||||||
|
st.write(f"Modalità statica: floor = {floor} vol pts")
|
||||||
|
|
||||||
|
if bool(getattr(entry, "vol_of_vol_guard_enabled", False)):
|
||||||
|
st.markdown("---")
|
||||||
|
st.markdown("**Vol-of-Vol guard**")
|
||||||
|
threshold_pt = Decimal(
|
||||||
|
str(getattr(entry, "vol_of_vol_threshold_pt", "5"))
|
||||||
|
)
|
||||||
|
lookback_h = int(getattr(entry, "vol_of_vol_lookback_hours", 24))
|
||||||
|
|
||||||
|
# Find tick closest to latest_ts - lookback hours, tolerance 15 min
|
||||||
|
dvol_lookback: Decimal | None = None
|
||||||
|
if latest_ts is not None and dvol_now is not None:
|
||||||
|
target_ts = latest_ts - timedelta(hours=lookback_h)
|
||||||
|
best_delta = timedelta(minutes=15)
|
||||||
|
for r in records:
|
||||||
|
if not r.fetch_ok or r.dvol is None:
|
||||||
|
continue
|
||||||
|
d = abs(r.timestamp - target_ts)
|
||||||
|
if d <= best_delta:
|
||||||
|
best_delta = d
|
||||||
|
dvol_lookback = r.dvol
|
||||||
|
|
||||||
|
if dvol_lookback is not None and dvol_now is not None:
|
||||||
|
delta = abs(dvol_now - dvol_lookback)
|
||||||
|
c1, c2 = st.columns(2)
|
||||||
|
c1.metric(f"|ΔDVOL {lookback_h}h|", f"{delta:.2f}")
|
||||||
|
c2.metric("Soglia VoV", f"{threshold_pt:.2f}")
|
||||||
|
verdict = "✅ PASS" if delta < threshold_pt else "❌ SKIP"
|
||||||
|
st.markdown(f"**Verdict:** {verdict}")
|
||||||
|
else:
|
||||||
|
st.info(f"Lookback {lookback_h}h non disponibile (gap dati).")
|
||||||
|
|
||||||
|
st.divider()
|
||||||
|
|
||||||
|
|
||||||
def render() -> None:
|
def render() -> None:
|
||||||
st.title("📐 Calibrazione")
|
st.title("📐 Calibrazione")
|
||||||
st.caption(
|
st.caption(
|
||||||
@@ -313,6 +440,8 @@ def render() -> None:
|
|||||||
|
|
||||||
specs = _metric_specs(strategy)
|
specs = _metric_specs(strategy)
|
||||||
|
|
||||||
|
_render_adaptive_gate_panel(strategy, records)
|
||||||
|
|
||||||
for spec in specs:
|
for spec in specs:
|
||||||
_render_metric(spec, records)
|
_render_metric(spec, records)
|
||||||
st.divider()
|
st.divider()
|
||||||
|
|||||||
Reference in New Issue
Block a user