feat: options technical agent — IV/skew/term structure validation per trade

- New options_technical_agent.py: rule engine (IVR, skew, term structure, flow)
  + GPT-4o narrative per trade; verdict OK/WARN/ALERT + fit_score
- options_trade_assessments table in DB for Journal badge persistence
- auto_cycle.py step 5.2: assess newly logged trades after log_trade_entries;
  results embedded in cycle report
- suggest_patterns_from_market_context: +iv_context param + explicit IV→strategy
  rules in prompt (IVR<30%→Long, 30-60%→Spread, >60%→no naked long, >80%→short)
- Pre-fetch iv_context at step 1.9 so suggestion step gets strategy rules
- reports.py: /api/reports/assessments/latest + /assessments/{run_id} endpoints
- RapportIA.tsx: "Validation Technique Options" section with per-trade IVBar,
  VerdictBadge, issues list, GPT-4o analysis, optimal strategy suggestion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-20 09:36:35 +02:00
parent 1aadf98fe4
commit 3ee39d5f08
6 changed files with 783 additions and 8 deletions

View File

@@ -300,6 +300,24 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
dominant = scenarios.get("dominant", "incertain")
summary["dominant_regime"] = dominant
# ── Step 1.9: Pre-fetch IV context for strategy suggestion rules ──────
iv_context = ""
try:
from services.iv_engine import get_iv_context_for_prompt, IV_WATCHLIST
from services.database import get_mtm_trades_with_traces
_mtm_pre = get_mtm_trades_with_traces(days=90)
_trade_tickers_pre = list({
(t.get("underlying") or "").upper()
for t in _mtm_pre.get("all_trades", [])
if t.get("underlying")
})
_iv_tickers_pre = (_trade_tickers_pre + IV_WATCHLIST[:6])[:10]
iv_context = get_iv_context_for_prompt(_iv_tickers_pre)
if iv_context:
logger.info(f"[Cycle {run_id[:16]}] IV context pre-fetched for {len(_iv_tickers_pre)} tickers (suggestion step)")
except Exception as _e:
logger.warning(f"[Cycle] IV context pre-fetch failed (non-blocking): {_e}")
# ── Step 2: Suggest new patterns ──────────────────────────────────────
logger.info(f"[Cycle {run_id[:16]}] Step 2: suggesting patterns")
_reliability_map = {}
@@ -317,6 +335,7 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
news, quotes, calendar, macro_regime=macro_regime, geo_score=geo_score_obj,
portfolio_lessons=portfolio_lessons,
reliability_map=_reliability_map or None,
iv_context=iv_context,
)
except Exception as e:
logger.warning(f"[Cycle] Suggestion step failed: {e}")
@@ -389,8 +408,7 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
except Exception as _re:
logger.warning(f"[Cycle] Risk cluster context failed (non-blocking): {_re}")
# ── Step 3.6: Collect IV context ─────────────────────────────────────
iv_context = ""
# ── Step 3.6: Refresh IV context (with full trade+watchlist scope) ──────
try:
from services.iv_engine import get_iv_context_for_prompt, IV_WATCHLIST
# Collect underlyings from current trade journal + default watchlist
@@ -521,8 +539,32 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
log_geo_alert(geo_score=geo_score_val, top_patterns=top_patterns_log,
news_count=len(news), run_id=scoring_run_id)
_options_assessment = None
log_trade_entries(run_id=scoring_run_id, scored_patterns=scored, quotes=quotes)
# ── Step 5.2: Options Technical Agent — validate newly logged trades ──
try:
from services.options_technical_agent import assess_logged_trades, save_assessments_to_db
_options_assessment = assess_logged_trades(
scoring_run_id=scoring_run_id,
scored=scored,
ai_key=ai_key,
)
if _options_assessment and _options_assessment.get("assessments"):
save_assessments_to_db(_options_assessment["assessments"], scoring_run_id)
summary["options_assessment"] = {
"n_ok": _options_assessment.get("n_ok", 0),
"n_warn": _options_assessment.get("n_warn", 0),
"n_alert": _options_assessment.get("n_alert", 0),
"global_score": _options_assessment.get("global_score"),
}
logger.info(
f"[OptionsTech] Assessment done — OK={_options_assessment.get('n_ok')} "
f"WARN={_options_assessment.get('n_warn')} ALERT={_options_assessment.get('n_alert')}"
)
except Exception as _ota:
logger.warning(f"[OptionsTech] Agent failed (non-blocking): {_ota}")
# ── Step 5.1: Portfolio monitor — conflict & concentration check ──────
try:
from services.portfolio_risk import analyze_simulation_portfolio
@@ -651,6 +693,7 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
scoring_run_id=scoring_run_id,
portfolio_monitor=summary.get("portfolio_monitor"),
commentary=commentary,
options_assessment=_options_assessment,
)
if _cycle_report:
from services.database import save_cycle_report
@@ -780,6 +823,7 @@ def _generate_cycle_report(
scoring_run_id: str,
portfolio_monitor: Optional[Dict],
commentary: Optional[str],
options_assessment: Optional[Dict] = None,
) -> Optional[Dict]:
"""
Build the full cycle report dict:
@@ -987,6 +1031,15 @@ Réponds en JSON avec ce schéma EXACT:
"summary": (s.get("summary") or "")[:120], "key_catalyst": s.get("key_catalyst", "")}
for s in sorted(scored, key=lambda x: -(x.get("score") or 0))[:5]
],
# Options Technical Agent assessment
"options_technical": {
"global_assessment": (options_assessment or {}).get("global_assessment", ""),
"global_score": (options_assessment or {}).get("global_score"),
"n_ok": (options_assessment or {}).get("n_ok", 0),
"n_warn": (options_assessment or {}).get("n_warn", 0),
"n_alert": (options_assessment or {}).get("n_alert", 0),
"assessments": (options_assessment or {}).get("assessments", []),
} if options_assessment is not None else None,
}
return report