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

@@ -4,6 +4,7 @@ import clsx from 'clsx'
import {
Brain, TrendingUp, TrendingDown, RefreshCw, ShieldAlert,
GitCompare, Layers, Zap, BookOpen, Clock, ChevronRight,
CheckCircle2, AlertTriangle, XCircle, BarChart2,
} from 'lucide-react'
const SCENARIO_META: Record<string, { label: string; color: string; emoji: string }> = {
@@ -43,6 +44,37 @@ function Section({ icon, title, children }: { icon: React.ReactNode; title: stri
)
}
const VERDICT_META = {
OK: { color: 'text-emerald-400', bg: 'bg-emerald-900/10 border-emerald-700/30', icon: CheckCircle2 },
WARN: { color: 'text-yellow-400', bg: 'bg-yellow-900/10 border-yellow-700/30', icon: AlertTriangle },
ALERT: { color: 'text-red-400', bg: 'bg-red-900/10 border-red-700/30', icon: XCircle },
}
function VerdictBadge({ verdict }: { verdict: 'OK' | 'WARN' | 'ALERT' }) {
const m = VERDICT_META[verdict] ?? VERDICT_META.WARN
const Icon = m.icon
return (
<span className={clsx('inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] font-bold border', m.bg, m.color)}>
<Icon className="w-2.5 h-2.5" /> {verdict}
</span>
)
}
function IVBar({ ivRank }: { ivRank?: number | null }) {
if (ivRank == null) return <span className="text-slate-600 text-[10px]">IVR </span>
const color = ivRank >= 60 ? 'bg-red-500' : ivRank >= 30 ? 'bg-yellow-500' : 'bg-emerald-500'
return (
<div className="flex items-center gap-1.5">
<div className="w-16 h-1 bg-slate-700 rounded-full overflow-hidden">
<div className={clsx('h-full rounded-full', color)} style={{ width: `${Math.min(ivRank, 100)}%` }} />
</div>
<span className={clsx('text-[10px] font-mono', ivRank >= 60 ? 'text-red-400' : ivRank >= 30 ? 'text-yellow-400' : 'text-emerald-400')}>
IVR {ivRank.toFixed(0)}%
</span>
</div>
)
}
function DeltaRow({ emoji, label, count, items, colorClass }: {
emoji: string; label: string; count: number;
items: any[]; colorClass: string;
@@ -412,6 +444,123 @@ export default function RapportCycle() {
</Section>
)}
{/* Options Technical Agent */}
{report.options_technical && (report.options_technical.assessments?.length > 0 || report.options_technical.global_assessment) && (
<Section icon={<BarChart2 className="w-4 h-4" />} title="Validation Technique Options">
{/* Global score + summary */}
<div className="flex items-start gap-3 mb-4">
{report.options_technical.global_score != null && (
<div className="shrink-0 text-center">
<div className={clsx('text-2xl font-bold font-mono',
report.options_technical.global_score >= 70 ? 'text-emerald-400'
: report.options_technical.global_score >= 45 ? 'text-yellow-400'
: 'text-red-400'
)}>
{report.options_technical.global_score}
</div>
<div className="text-[9px] text-slate-600">Score vol</div>
</div>
)}
<div className="flex-1 min-w-0">
{report.options_technical.global_assessment && (
<p className="text-xs text-slate-300 leading-relaxed">
{report.options_technical.global_assessment}
</p>
)}
<div className="flex gap-3 mt-2 text-[10px]">
{report.options_technical.n_ok > 0 && (
<span className="text-emerald-400 flex items-center gap-0.5">
<CheckCircle2 className="w-3 h-3" /> {report.options_technical.n_ok} OK
</span>
)}
{report.options_technical.n_warn > 0 && (
<span className="text-yellow-400 flex items-center gap-0.5">
<AlertTriangle className="w-3 h-3" /> {report.options_technical.n_warn} WARN
</span>
)}
{report.options_technical.n_alert > 0 && (
<span className="text-red-400 flex items-center gap-0.5">
<XCircle className="w-3 h-3" /> {report.options_technical.n_alert} ALERT
</span>
)}
</div>
</div>
</div>
{/* Per-trade assessments */}
<div className="space-y-3">
{(report.options_technical.assessments as any[]).map((a: any, i: number) => (
<div key={i} className={clsx(
'rounded border px-3 py-2.5',
VERDICT_META[a.verdict as 'OK' | 'WARN' | 'ALERT']?.bg ?? 'border-slate-700/30 bg-dark-700/40'
)}>
{/* Trade header */}
<div className="flex items-center gap-2 mb-2">
<VerdictBadge verdict={a.verdict} />
<span className="text-xs font-semibold text-slate-200">{a.ticker}</span>
<span className="text-[10px] text-slate-500">{a.strategy}</span>
<div className="ml-auto">
<IVBar ivRank={a.iv_rank} />
</div>
</div>
{/* IV data row */}
<div className="flex gap-3 text-[10px] text-slate-500 mb-2">
{a.skew_pct != null && (
<span>Skew: <span className={clsx('font-mono', a.skew_pct > 5 ? 'text-orange-400' : 'text-slate-400')}>
{a.skew_pct > 0 ? '+' : ''}{a.skew_pct.toFixed(1)}pts
</span></span>
)}
{a.term_structure && (
<span>Structure: <span className={clsx('font-mono',
a.term_structure === 'backwardation' ? 'text-red-400'
: a.term_structure === 'contango' ? 'text-emerald-400' : 'text-slate-400'
)}>{a.term_structure}</span></span>
)}
{a.flow_bias && (
<span>Flow: <span className="font-mono text-slate-400">{a.flow_bias}</span></span>
)}
<span className="ml-auto text-slate-600">Score: <span className="font-mono text-slate-400">{a.fit_score}</span></span>
</div>
{/* Issues + positives */}
{((a.issues ?? []).length > 0 || (a.positives ?? []).length > 0) && (
<div className="space-y-0.5 mb-2">
{(a.issues as string[]).map((issue: string, j: number) => (
<div key={j} className="flex items-start gap-1.5 text-[10px] text-orange-300">
<span className="shrink-0 mt-0.5">⚠</span> {issue}
</div>
))}
{(a.positives as string[]).map((pos: string, j: number) => (
<div key={j} className="flex items-start gap-1.5 text-[10px] text-emerald-400">
<span className="shrink-0 mt-0.5">✓</span> {pos}
</div>
))}
</div>
)}
{/* GPT-4o analysis */}
{a.analysis && (
<p className="text-[11px] text-slate-400 leading-relaxed mb-1">{a.analysis}</p>
)}
{/* Optimal strategy + when to enter */}
<div className="flex items-start gap-3 mt-1.5 pt-1.5 border-t border-slate-700/20 text-[10px]">
{a.optimal_strategy && a.optimal_strategy !== a.strategy && (
<span className="text-slate-500">
Stratégie optimale: <span className="text-blue-400 font-medium">{a.optimal_strategy}</span>
</span>
)}
{a.when_to_enter && (
<span className="text-slate-600 italic">{a.when_to_enter}</span>
)}
</div>
</div>
))}
</div>
</Section>
)}
{/* Risk / portfolio monitor */}
{report.portfolio_monitor && (
<Section icon={<ShieldAlert className="w-4 h-4" />} title="Risque & Alertes portefeuille">