feat: IV gate — block ALERT trades before logging + configurable thresholds

- auto_cycle.py: pre-fetch IV snapshots at step 1.9; _apply_iv_gate() runs
  before log_trade_entries, removes ALERT-verdict trades (not just reports)
- options_technical_agent.py: _IVR_HIGH/_IVR_EXTREME/_SKEW_THRESH as
  module-level vars; straddle/strangle penalty -60 (vs -56 naked) at extreme IVR
  so Long Straddle at IVR ≥ 80% → ALERT; thresholds respected in rule engine
- database.py: seed 4 iv_gate config keys (iv_gate_enabled, iv_gate_ivr_high=60,
  iv_gate_ivr_extreme=80, iv_gate_skew_threshold=8) — editable from Config page
- Blocked trades logged as skipped_trades with [IV_GATE] detail + optimal strategy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-20 10:15:45 +02:00
parent 3ee39d5f08
commit c7ccf237d7
3 changed files with 189 additions and 14 deletions

View File

@@ -12,10 +12,19 @@ Checks performed per trade:
Verdict: OK | WARN | ALERT
"""
import logging
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Tuple
logger = logging.getLogger(__name__)
# ── Configurable thresholds ────────────────────────────────────────────────────
# These can be overridden by auto_cycle._apply_iv_gate() using values from
# the app_config DB table (keys: iv_gate_ivr_high, iv_gate_ivr_extreme,
# iv_gate_skew_threshold). Edit them via the Config page in the UI.
_IVR_HIGH: float = 60.0 # IVR above this → no naked long vol
_IVR_EXTREME: float = 80.0 # IVR above this → sell vol only
_SKEW_THRESH: float = 8.0 # Put skew above this → puts very expensive
# ── IV regime → strategy mapping ───────────────────────────────────────────────
IV_STRATEGY_RULES = {
@@ -93,18 +102,22 @@ def assess_strategy_fit(
direction = _infer_direction(strategy)
regime = _iv_regime(iv_rank)
# ── Rule 1: IV Rank fit ──────────────────────────────────────────────────
# ── Rule 1: IV Rank fit (uses configurable thresholds _IVR_HIGH / _IVR_EXTREME) ──
ivr_high = _IVR_HIGH # default 60, configurable via app_config
ivr_extreme = _IVR_EXTREME # default 80
if iv_rank is not None:
if is_long_vol:
if iv_rank >= 80:
if iv_rank >= ivr_extreme:
is_double_sided = any(k in s for k in ["straddle", "strangle"])
issues.append(
f"IVR {iv_rank:.0f}% — achat de vol au pic annuel (IV crush quasi-certain). "
f"Min 52s={iv_min_52w:.1f}% | Max={iv_max_52w:.1f}%"
if iv_min_52w and iv_max_52w else
f"IVR {iv_rank:.0f}% — achat de vol au pic annuel, IV crush probable"
)
score -= 45
elif iv_rank >= 60:
# Straddles/strangles buy BOTH sides at peak IV → double vega exposure
score -= 60 if is_double_sided else 56
elif iv_rank >= ivr_high:
issues.append(f"IVR {iv_rank:.0f}% — vol chère, un spread débiteur réduirait le coût de vega de ~40-60%")
score -= 25
elif iv_rank <= 20:
@@ -116,11 +129,11 @@ def assess_strategy_fit(
if iv_rank <= 25:
issues.append(f"IVR {iv_rank:.0f}% — prime collectée faible, risque/rendement défavorable pour vendeur")
score -= 25
elif iv_rank >= 65:
elif iv_rank >= ivr_high + 5:
positives.append(f"IVR {iv_rank:.0f}% — vol chère, timing favorable pour la vente de prime")
score += 10
elif is_spread:
if iv_rank >= 50:
if iv_rank >= ivr_high - 10:
positives.append(f"IVR {iv_rank:.0f}% — spread adapté : coût vega réduit, convient au régime de vol élevée")
elif iv_rank <= 20:
issues.append(f"IVR {iv_rank:.0f}% — vol bon marché, option pure plus efficace qu'un spread (gain plafonné inutilement)")
@@ -138,15 +151,16 @@ def assess_strategy_fit(
f"— dans le top 10% du range annuel"
)
# ── Rule 3: Skew ──────────────────────────────────────────────────────────
# ── Rule 3: Skew (uses _SKEW_THRESH) ─────────────────────────────────────
skew_thresh = _SKEW_THRESH # default 8
if skew_pct is not None:
if skew_pct > 8 and "put" in s and is_long_vol:
if skew_pct > skew_thresh and "put" in s and is_long_vol:
issues.append(f"Skew put élevé ({skew_pct:+.1f}pts) — protection déjà très chère, marché en mode hedge")
score -= 15
elif skew_pct > 5:
elif skew_pct > skew_thresh * 0.6:
issues.append(f"Skew put positif ({skew_pct:+.1f}pts) — demande de protection élevée, marché anxieux")
score -= 8
elif skew_pct < -5 and "call" in s and is_long_vol:
elif skew_pct < -skew_thresh * 0.6 and "call" in s and is_long_vol:
issues.append(f"Skew call négatif ({skew_pct:+.1f}pts) — demande de calls élevée, options call chères relativement")
score -= 8
elif abs(skew_pct) <= 3: