""" Options Technical Agent — validates each logged trade from a pure options pricing perspective. Checks performed per trade: 1. IV Rank / IV Percentile → is vol cheap or expensive? 2. IV vs Historical Vol ratio → is options premium justified? 3. Skew (put/call) → what is the market hedging against? 4. Term Structure (contango/back) → front-month stress or calm? 5. Options flow (call/put ratio) → smart money direction? 6. Strategy / IV fit → is the strategy appropriate for current vol regime? Verdict: OK | WARN | ALERT """ import logging 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 = { "long_vol": ["long call", "long put", "long straddle", "long strangle"], "short_vol": ["short call", "short put", "short strangle", "iron condor", "covered call", "cash secured put", "cash-secured put"], "spread": ["bull call spread", "bear put spread", "bull put spread", "bear call spread", "call spread", "put spread"], } STRATEGY_REGIME = { # IVR < 30 → buy vol (cheap) "low": {"preferred": ["Long Call", "Long Put", "Long Straddle"], "avoid": ["Iron Condor", "Short Strangle"]}, # IVR 30-60 → spreads (moderate cost) "mid": {"preferred": ["Bull Call Spread", "Bear Put Spread", "Bull Put Spread", "Bear Call Spread"], "avoid": ["Long Straddle", "Long Strangle"]}, # IVR > 60 → sell vol / use spreads (expensive) "high": {"preferred": ["Bull Call Spread", "Bear Put Spread", "Cash-Secured Put", "Iron Condor", "Covered Call", "Short Strangle"], "avoid": ["Long Call", "Long Put", "Long Straddle", "Long Strangle"]}, } def _iv_regime(iv_rank: Optional[float]) -> str: if iv_rank is None: return "unknown" if iv_rank < 30: return "low" if iv_rank < 60: return "mid" return "high" def _infer_direction(strategy: str) -> str: s = strategy.lower() if any(k in s for k in ["call", "bull", "haussier"]): return "bullish" if any(k in s for k in ["put", "bear", "baissier"]): return "bearish" if any(k in s for k in ["straddle", "strangle", "iron condor"]): return "neutral" return "bullish" def _optimal_strategy(direction: str, iv_rank: Optional[float]) -> str: regime = _iv_regime(iv_rank) if direction == "bullish": return {"low": "Long Call", "mid": "Bull Call Spread", "high": "Cash-Secured Put ou Bull Put Spread"}.get(regime, "Bull Call Spread") elif direction == "bearish": return {"low": "Long Put", "mid": "Bear Put Spread", "high": "Covered Call ou Bear Call Spread"}.get(regime, "Bear Put Spread") else: return {"low": "Long Straddle", "mid": "Iron Condor (wing buy)", "high": "Iron Condor ou Short Strangle"}.get(regime, "Iron Condor") def assess_strategy_fit( strategy: str, iv_rank: Optional[float], iv_current_pct: Optional[float], iv_min_52w: Optional[float], iv_max_52w: Optional[float], skew_pct: Optional[float], term_structure: Optional[str], flow_bias: Optional[str], ) -> Dict[str, Any]: """Rule-based assessment of strategy vs options pricing environment.""" issues: List[str] = [] positives: List[str] = [] score = 100 s = strategy.lower() is_long_vol = any(k in s for k in IV_STRATEGY_RULES["long_vol"]) is_short_vol = any(k in s for k in IV_STRATEGY_RULES["short_vol"]) is_spread = any(k in s for k in IV_STRATEGY_RULES["spread"]) direction = _infer_direction(strategy) regime = _iv_regime(iv_rank) # ── 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 >= 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" ) # 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: positives.append(f"IVR {iv_rank:.0f}% — vol bon marché, timing idéal pour acheter des options") score += 5 elif iv_rank <= 35: positives.append(f"IVR {iv_rank:.0f}% — vol modérément bon marché, stratégie long vol pertinente") elif is_short_vol: 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 >= 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 >= 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)") score -= 10 # ── Rule 2: IV vs Historical Vol ratio ────────────────────────────────── if iv_current_pct and iv_min_52w and iv_max_52w: iv_range = iv_max_52w - iv_min_52w if iv_range > 0: iv_normalized = (iv_current_pct - iv_min_52w) / iv_range * 100 # This is essentially IVR recalculated — use for extra context if is_long_vol and iv_normalized >= 90: issues.append( f"IV actuelle {iv_current_pct:.1f}% vs range 52s [{iv_min_52w:.1f}%–{iv_max_52w:.1f}%] " f"— dans le top 10% du range annuel" ) # ── Rule 3: Skew (uses _SKEW_THRESH) ───────────────────────────────────── skew_thresh = _SKEW_THRESH # default 8 if skew_pct is not None: 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 > 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 < -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: positives.append(f"Skew neutre ({skew_pct:+.1f}pts) — pas de biais de protection excessif dans le marché") # ── Rule 4: Term structure ──────────────────────────────────────────────── if term_structure: if term_structure == "backwardation" and is_short_vol: issues.append("Term structure en backwardation — vol front-month > back, vendre la vol est risqué en période de stress") score -= 20 elif term_structure == "backwardation" and is_long_vol and "straddle" in s: positives.append("Backwardation + long straddle : vol front-month élevée, catalyseur événementiel probable") score += 8 elif term_structure == "contango" and is_short_vol: positives.append("Contango : vol croît avec le temps, calendar spread ou vente de vol front-month avantageuse") score += 5 elif term_structure == "contango" and is_long_vol: issues.append("Contango : vol augmente avec l'échéance, payer le temps est coûteux pour les options longues") score -= 5 # ── Rule 5: Options flow ────────────────────────────────────────────────── if flow_bias: if flow_bias == "bearish" and direction == "bullish": issues.append("Flow options bearish (plus de puts achetés) alors que la stratégie est haussière — signal contra") score -= 10 elif flow_bias == "bullish" and direction == "bearish": issues.append("Flow options bullish (plus de calls achetés) alors que la stratégie est baissière — signal contra") score -= 10 elif flow_bias == direction: positives.append(f"Flow options aligné sur la direction ({flow_bias}) — confirmation par le smart money") score += 5 score = max(0, min(100, score)) verdict = "OK" if score >= 70 else "WARN" if score >= 45 else "ALERT" return { "fit_score": score, "verdict": verdict, "issues": issues, "positives": positives, "direction_inferred": direction, "iv_regime": regime, "optimal_strategy": _optimal_strategy(direction, iv_rank), } # ── Main agent function ──────────────────────────────────────────────────────── def assess_logged_trades( scoring_run_id: str, scored: List[Dict], ai_key: str, ) -> Optional[Dict]: """ For each trade logged in this cycle: 1. Fetch IV snapshot (IVR, skew, term structure, flow) 2. Run rule-based assessment 3. GPT-4o narrative for all trades together Returns dict with per-trade assessments + global assessment. """ import os import json as _json os.environ["OPENAI_API_KEY"] = ai_key # ── Get newly logged trades ─────────────────────────────────────────────── try: from services.database import get_conn conn = get_conn() rows = conn.execute( """SELECT id, underlying, strategy, entry_date, score_at_entry, pattern_name, capital_invested FROM trade_entry_prices WHERE run_id=? ORDER BY entry_date DESC""", (scoring_run_id,), ).fetchall() conn.close() trades = [dict(r) for r in rows] except Exception as e: logger.warning(f"[OptionsTech] Failed to fetch logged trades: {e}") return None if not trades: logger.info("[OptionsTech] No trades logged this cycle — skipping assessment") return {"assessments": [], "global_assessment": "", "global_score": None} # ── Fetch IV snapshots per unique ticker ────────────────────────────────── from services.iv_engine import get_full_iv_snapshot iv_snapshots: Dict[str, Dict] = {} unique_tickers = list({t["underlying"] for t in trades if t.get("underlying")}) for ticker in unique_tickers: try: iv_snapshots[ticker] = get_full_iv_snapshot(ticker) logger.debug(f"[OptionsTech] IV snapshot fetched for {ticker}") except Exception as e: logger.debug(f"[OptionsTech] IV snapshot failed for {ticker}: {e}") iv_snapshots[ticker] = {} # ── Rule-based assessment per trade ────────────────────────────────────── assessments: List[Dict] = [] for t in trades: ticker = t.get("underlying") or "" strategy = t.get("strategy") or "Long Call" snap = iv_snapshots.get(ticker, {}) iv_rank = snap.get("iv_rank") iv_current_pct = snap.get("iv_current_pct") iv_min_52w = snap.get("iv_min_52w_pct") iv_max_52w = snap.get("iv_max_52w_pct") skew = snap.get("skew", {}) skew_pct = skew.get("skew_pct") term = snap.get("term_structure", {}) term_structure = term.get("structure") flow = snap.get("options_flow", {}) flow_bias = flow.get("flow_bias") call_put_ratio = flow.get("call_put_ratio") iv_source = snap.get("iv_source", "none") fit = assess_strategy_fit( strategy=strategy, iv_rank=iv_rank, iv_current_pct=iv_current_pct, iv_min_52w=iv_min_52w, iv_max_52w=iv_max_52w, skew_pct=skew_pct, term_structure=term_structure, flow_bias=flow_bias, ) assessments.append({ "trade_id": t.get("id"), "ticker": ticker, "strategy": strategy, "pattern_name": t.get("pattern_name", ""), # IV data "iv_rank": iv_rank, "iv_current_pct": iv_current_pct, "iv_min_52w_pct": iv_min_52w, "iv_max_52w_pct": iv_max_52w, "iv_source": iv_source, "skew_pct": skew_pct, "skew_interpretation": skew.get("interpretation"), "term_structure": term_structure, "term_iv_30d": term.get("iv_30d"), "term_iv_90d": term.get("iv_90d"), "flow_bias": flow_bias, "call_put_ratio": call_put_ratio, # Rule-based verdict "fit_score": fit["fit_score"], "verdict": fit["verdict"], "issues": fit["issues"], "positives": fit["positives"], "iv_regime": fit["iv_regime"], "direction_inferred": fit["direction_inferred"], "optimal_strategy": fit["optimal_strategy"], # GPT-4o analysis filled below "analysis": "", "when_to_enter": "", }) # ── GPT-4o narrative (all trades together) ──────────────────────────────── global_assessment = "" global_score = None try: from services.ai_analyzer import _chat trades_for_prompt = [] for a in assessments: entry = { "ticker": a["ticker"], "strategy": a["strategy"], "pattern": a["pattern_name"], "iv_rank_pct": a["iv_rank"], "iv_current_pct": a["iv_current_pct"], "iv_range_52w": f"{a['iv_min_52w_pct']:.1f}%–{a['iv_max_52w_pct']:.1f}%" if a["iv_min_52w_pct"] and a["iv_max_52w_pct"] else None, "skew_pts": a["skew_pct"], "skew_interp": a["skew_interpretation"], "term_structure": a["term_structure"], "iv_30d": a["term_iv_30d"], "iv_90d": a["term_iv_90d"], "flow_bias": a["flow_bias"], "call_put_ratio": a["call_put_ratio"], "rule_verdict": a["verdict"], "rule_issues": a["issues"], "rule_positives": a["positives"], "optimal_strategy_suggested": a["optimal_strategy"], } trades_for_prompt.append(entry) prompt = f"""Tu es un trader d'options senior avec 20 ans d'expérience en market making et volatilité. Analyse les trades ci-dessous qui viennent d'être loggés dans notre système. Pour chaque trade, fournis une analyse technique options rigoureuse en utilisant TOUS les indicateurs disponibles. TRADES À ANALYSER: {_json.dumps(trades_for_prompt, ensure_ascii=False, indent=2)} Pour chaque trade, analyse: 1. Le TIMING de vol (IVR, range 52s, IV/HV implicite) — est-ce le bon moment pour cette stratégie ? 2. La STRUCTURE (skew, term structure, flow) — que dit le marché options lui-même ? 3. La STRATÉGIE choisie — est-elle optimale pour ce régime de vol ? 4. Le PRICE d'ENTRÉE OPTIMAL — quelle condition améliorerait le timing ? Rappel règles d'or: - IVR > 70% + achat d'option naked = payer la prime maximale = IV crush probable - Skew put élevé = marché en mode protection, puts chers - Backwardation = stress, ne pas vendre la vol - Iron Condor / Short Strangle = uniquement IVR > 65% - Long Straddle = uniquement si catalyseur + IVR < 30% Réponds en JSON EXACT: {{ "trade_assessments": [ {{ "ticker": "", "strategy": "", "technical_score": <0-100>, "analysis": "<3-4 phrases d'analyse technique précise: timing vol, skew, structure>", "when_to_enter": "" }} ], "global_assessment": "<2-3 phrases sur la qualité technique globale des entrées de ce cycle>", "global_score": <0-100, note technique globale du cycle d'entrées> }}""" result = _chat( "Tu es un expert options/volatilité. Analyse technique précise en JSON.", prompt, model="gpt-4o", json_mode=True, max_tokens=1200, ) if result: global_assessment = result.get("global_assessment", "") global_score = result.get("global_score") ai_assessments = {a["ticker"]: a for a in result.get("trade_assessments", [])} for a in assessments: ai = ai_assessments.get(a["ticker"], {}) a["analysis"] = ai.get("analysis", "") a["when_to_enter"] = ai.get("when_to_enter", "") if ai.get("technical_score") is not None: a["fit_score"] = int(ai["technical_score"]) a["verdict"] = "OK" if a["fit_score"] >= 70 else "WARN" if a["fit_score"] >= 45 else "ALERT" logger.info(f"[OptionsTech] GPT-4o assessment complete — {len(assessments)} trades, global_score={global_score}") except Exception as e: logger.warning(f"[OptionsTech] GPT-4o narrative failed: {e}") return { "run_id": scoring_run_id, "assessments": assessments, "global_assessment": global_assessment, "global_score": global_score, "n_alert": sum(1 for a in assessments if a["verdict"] == "ALERT"), "n_warn": sum(1 for a in assessments if a["verdict"] == "WARN"), "n_ok": sum(1 for a in assessments if a["verdict"] == "OK"), } def save_assessments_to_db(assessments: List[Dict], run_id: str) -> None: """Persist per-trade assessments for Journal badge display.""" import json as _json try: from services.database import get_conn conn = get_conn() for a in assessments: conn.execute( """INSERT OR REPLACE INTO options_trade_assessments (run_id, trade_id, ticker, strategy, assessed_at, iv_rank, iv_current_pct, skew_pct, term_structure, fit_score, verdict, issues_json, optimal_strategy, analysis, when_to_enter) VALUES (?, ?, ?, ?, datetime('now'), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", ( run_id, a.get("trade_id"), a.get("ticker"), a.get("strategy"), a.get("iv_rank"), a.get("iv_current_pct"), a.get("skew_pct"), a.get("term_structure"), a.get("fit_score"), a.get("verdict"), _json.dumps(a.get("issues", []) + (["✓ " + p for p in a.get("positives", [])]), ensure_ascii=False), a.get("optimal_strategy"), a.get("analysis", ""), a.get("when_to_enter", ""), ), ) conn.commit() conn.close() except Exception as e: logger.warning(f"[OptionsTech] DB save failed: {e}") # ── IV-strategy injection string for AI prompts ──────────────────────────────── OPTIONS_STRATEGY_RULES = """ ## ⚠️ RÈGLES STRICTES IV → STRATÉGIE (à appliquer pour chaque trade suggéré) Le choix de stratégie doit tenir compte du COÛT de la volatilité implicite (IVR = IV Rank 52 semaines): | IVR | Stratégie AUTORISÉE | Stratégie INTERDITE | |-----|---------------------|---------------------| | < 30% (vol cheap) | Long Call, Long Put, Long Straddle | Iron Condor, Short Strangle | | 30–60% (vol moderate) | Bull Call Spread, Bear Put Spread | Long Straddle, Long Strangle naked | | 60–80% (vol chère) | Spreads débiteurs, Cash-Secured Put | Long Call naked, Long Put naked | | > 80% (vol très chère)| Iron Condor, Short Strangle, Covered Call, Cash-Secured Put | TOUTE option long naked | Règles supplémentaires: - Skew put élevé (> 5 pts) → éviter Long Put (trop cher), préférer Bear Put Spread - Term structure backwardation → ne pas vendre de vol (vendeur piégé si vol monte encore) - Si IVR inconnu → utiliser Bull Call Spread / Bear Put Spread par défaut (neutre au coût de vol) - Long Straddle uniquement si: IVR < 25% ET catalyseur événementiel clairement identifié """