import { useState, useMemo, useRef } from 'react' import { useRunPatternLab, useEvaluatePatternLab, useSaveLabPattern, usePatternLabRuns, useDeleteLabRun, useInstrumentScan, useEvaluateInstrumentScan, useFindMatchingPattern, MatchResult, useDiscoverEvents, DiscoveredEvent, validateTicker, } from '../hooks/useApi' import { FlaskConical, Play, CheckCircle2, XCircle, MinusCircle, Trash2, Save, RefreshCw, Search, CalendarDays, TrendingUp, TrendingDown, Zap, BarChart2, ScanLine, DollarSign, GitMerge, Layers, Plus, Globe, Sparkles, } from 'lucide-react' import clsx from 'clsx' import { INSTRUMENTS, INSTRUMENT_CATEGORIES } from '../constants/instruments' // ── Preset catalogue (2015-2025, 34 events) ──────────────────────────────────── interface Preset { id: string; year: number; label: string; date: string category: string; horizon_days: number; assets: string[]; hint: string } const PRESETS: Preset[] = [ { id: 'china-deval-2015', year: 2015, label: 'China CNY Devaluation', date: '2015-08-11', category: 'fx_macro', horizon_days: 60, assets: ['SPY','FXI','GLD','UUP','EEM'], hint: 'China surprises markets with a yuan devaluation shock. Global risk-off, EM currencies crash, commodities sell. Shanghai composite already down 30% from peak.' }, { id: 'oil-crash-2016', year: 2016, label: 'Oil Crash to $26', date: '2016-01-20', category: 'commodities', horizon_days: 90, assets: ['USO','XLE','HYG','SPY','GLD'], hint: 'WTI crude at 12-year lows near $26. Energy sector destruction, high-yield credit stress from E&P distress. Fed on hold as global deflation spreads.' }, { id: 'brexit-2016', year: 2016, label: 'Brexit Vote Shock', date: '2016-06-24', category: 'geopolitical', horizon_days: 60, assets: ['GBPUSD=X','EWU','SPY','GLD','TLT'], hint: 'UK votes Leave 52-48. GBP flash crash -10% overnight. European bank stocks collapse. Flight to safety — gilts, gold, USD. BoE emergency measures expected.' }, { id: 'trump-elect-2016', year: 2016, label: 'Trump Election 2016', date: '2016-11-09', category: 'macro', horizon_days: 90, assets: ['SPY','TLT','GLD','UUP','XLF'], hint: 'Trump wins US presidential election. Reflation trade ignites: banks surge, bonds sell, USD rallies sharply, gold dumps, infrastructure/defense stocks soar.' }, { id: 'volmageddon-2018', year: 2018, label: 'Volmageddon — XIV Collapse', date: '2018-02-05', category: 'volatility', horizon_days: 30, assets: ['SPY','TLT','GLD','QQQ'], hint: 'VIX spikes 115% intraday. Inverse-vol ETPs (XIV, SVXY) collapse -90%. Market liquidity evaporates. Fed hiking into froth. Flash crash with rapid recovery.' }, { id: 'trade-war-2018', year: 2018, label: 'US-China Trade War Start', date: '2018-03-22', category: 'geopolitical', horizon_days: 90, assets: ['SPY','FXI','QQQ','UUP','EEM'], hint: 'Trump announces $60B tariffs on China under Section 301. Trade war declared. Tech and industrials sell. China threatens retaliation on US agriculture and autos.' }, { id: 'turkey-em-2018', year: 2018, label: 'Turkey / EM Currency Crisis', date: '2018-08-10', category: 'fx_macro', horizon_days: 60, assets: ['TUR','EEM','UUP','GLD','EWZ'], hint: 'TRY crashes 20% in a day. Turkey in full currency crisis — Erdogan conflict with Fed. EM contagion to South Africa, Argentina, Brazil. USD wrecking ball.' }, { id: 'q4-selloff-2018', year: 2018, label: 'Q4 2018 Fed Overtightening', date: '2018-10-10', category: 'macro', horizon_days: 90, assets: ['SPY','QQQ','HYG','TLT','GLD'], hint: "Powell 'long way from neutral' comment. Growth scare, credit spread widening. SPX enters 20% drawdown into Christmas. Fed eventually pivots Jan 2019." }, { id: 'fed-pivot-2019', year: 2019, label: 'Fed Dovish Pivot', date: '2019-01-04', category: 'macro', horizon_days: 90, assets: ['SPY','TLT','GLD','EEM','UUP'], hint: 'Powell signals patience — hiking cycle pause. Risk assets explode higher. USD weakens. EM and gold rally. Goldilocks scenario emerges after Q4 2018 crash.' }, { id: 'yield-inversion-2019', year: 2019, label: 'Yield Curve Inversion', date: '2019-03-22', category: 'macro', horizon_days: 120, assets: ['TLT','XLF','SPY','GLD','IEF'], hint: '3M-10Y US Treasury yield curve inverts for first time since 2007. Recession indicator flashing. Banks sell. Gold and long bonds rally as growth fears build.' }, { id: 'aramco-attack-2019', year: 2019, label: 'Saudi Aramco Facility Attack', date: '2019-09-16', category: 'geopolitical', horizon_days: 30, assets: ['USO','GLD','SPY','XLE','UUP'], hint: 'Drone attacks on Abqaiq and Khurais take out 5% of global oil supply. WTI gaps +15% at open. Geopolitical risk premium spikes. Then fades fast.' }, { id: 'repo-stress-2019', year: 2019, label: 'Repo Market Liquidity Stress', date: '2019-09-17', category: 'credit', horizon_days: 30, assets: ['TLT','SPY','GLD','UUP'], hint: 'Overnight repo rates spike to 10%, overwhelming the Fed funds target. Fed emergency repo operations. Banking system dollar liquidity stress. Precursor to QE4.' }, { id: 'covid-crash-2020', year: 2020, label: 'COVID Pandemic Crash', date: '2020-02-24', category: 'macro', horizon_days: 90, assets: ['SPY','QQQ','GLD','TLT','USO'], hint: 'COVID spreads globally. Italy in lockdown. Markets price pandemic. SPX enters fastest bear market in history — 35% in 33 days. VIX touches 85. Fed emergency cut.' }, { id: 'oil-negative-2020', year: 2020, label: 'Oil Goes Negative (-$37)', date: '2020-04-20', category: 'commodities', horizon_days: 60, assets: ['USO','XLE','SPY','UUP','HYG'], hint: 'WTI May futures settle at -$37.63. Physical storage full. Zero demand from lockdowns. Energy sector existential risk. OPEC+ emergency cut already announced.' }, { id: 'covid-recovery-2020', year: 2020, label: 'COVID Recovery / QE Infinity', date: '2020-04-09', category: 'macro', horizon_days: 90, assets: ['SPY','QQQ','GLD','TLT','EEM'], hint: 'Fed announces unlimited QE + corporate bond buying. $2.2T CARES Act. Markets stage massive recovery. Tech leads. Gold rallies to ATH. Mega-cap dominance begins.' }, { id: 'us-election-2020', year: 2020, label: 'Biden Election / Vaccine Rally', date: '2020-11-04', category: 'macro', horizon_days: 90, assets: ['SPY','QQQ','XLE','GLD','IWM'], hint: 'Biden wins, split Congress. Pfizer vaccine +90% efficacy announced same week. Massive rotation: value vs growth, energy vs tech. Small caps surge. Gold drops.' }, { id: 'gme-squeeze-2021', year: 2021, label: 'GME Short Squeeze / WSB', date: '2021-01-27', category: 'credit', horizon_days: 30, assets: ['SPY','QQQ','IWM','HYG','TLT'], hint: 'WallStreetBets coordinates GME squeeze to $480. Robinhood halts buying. Hedge fund forced deleveraging. Systemic risk fear. Silver squeeze attempt follows.' }, { id: 'reflation-2021', year: 2021, label: 'Reflation Trade / Bond Crash', date: '2021-02-25', category: 'macro', horizon_days: 60, assets: ['TLT','QQQ','XLE','XLF','GLD'], hint: '10Y yields spike to 1.6%. Bond massacre. Tech rotation as rates hurt valuations. Value/cyclicals surge. Energy, banks, materials outperform. Inflation trade.' }, { id: 'taper-2021', year: 2021, label: 'Fed Tapering Announcement', date: '2021-11-03', category: 'macro', horizon_days: 90, assets: ['TLT','GLD','EEM','UUP','SPY'], hint: 'Fed officially announces QE tapering. Hawkish pivot accelerates. EM selloff. USD strengthens. Gold initially weak. Growth stocks start multi-month decline.' }, { id: 'russia-ukraine-2022', year: 2022, label: 'Russia Invades Ukraine', date: '2022-02-24', category: 'geopolitical', horizon_days: 90, assets: ['USO','GLD','WEAT','SPY','EWG'], hint: 'Full-scale Russian invasion. Energy price shock — European gas 10x. Wheat/corn spike +30%. European recession fear. Germany energy vulnerability. NATO unity.' }, { id: 'fed-rate-shock-2022', year: 2022, label: 'Fed Rate Shock — CPI 9.1%', date: '2022-06-13', category: 'macro', horizon_days: 90, assets: ['TLT','SPY','QQQ','GLD','UUP'], hint: 'CPI prints 9.1%. Fed goes 75bps (first time since 1994). Rate shock across all assets. Everything sold — bonds, stocks, gold, crypto. DXY to 114. Bear market.' }, { id: 'energy-crisis-eu-2022', year: 2022, label: 'European Energy Crisis', date: '2022-08-26', category: 'commodities', horizon_days: 90, assets: ['EURUSD=X','EWG','SPY','GLD','TLT'], hint: 'EU nat gas prices at 10x historical average. EUR/USD breaks parity. Germany industry threatens shutdown. ECB caught between inflation and recession.' }, { id: 'gilt-crisis-2022', year: 2022, label: 'UK Gilt Crisis / LDI Implosion', date: '2022-09-23', category: 'credit', horizon_days: 30, assets: ['GBPUSD=X','EWU','TLT','GLD','SPY'], hint: 'Truss mini-budget — unfunded £45B tax cuts. GBP crashes to 1.035. 30Y gilt yields +100bps in hours. LDI pension funds margin calls. BoE emergency QE.' }, { id: 'ftx-2022', year: 2022, label: 'FTX Crypto Exchange Collapse', date: '2022-11-08', category: 'credit', horizon_days: 60, assets: ['BTC-USD','ETH-USD','SPY','GLD','QQQ'], hint: 'FTX halts withdrawals. $8B hole. Alameda Research insolvent. Crypto contagion: Genesis, BlockFi. Bitcoin -75% ytd. Regulatory crackdown imminent. Crypto winter.' }, { id: 'china-zero-covid-2022', year: 2022, label: 'China Zero-COVID Exit', date: '2022-12-07', category: 'macro', horizon_days: 90, assets: ['FXI','EEM','USO','GLD','EWJ'], hint: 'China abruptly abandons zero-COVID after Urumqi protests. Reopening trade ignites. Commodities demand surge expected. But wave of infections causes volatility.' }, { id: 'boj-yyc-2023', year: 2023, label: 'BoJ YCC Band Widening', date: '2023-01-18', category: 'fx_macro', horizon_days: 90, assets: ['USDJPY=X','EWJ','TLT','GLD','SPY'], hint: 'BoJ widens YCC band to ±50bps surprise. JPY surges 3%. JGB yields spike. Global carry trade unwind risk. BoJ forced into bond buying. Yields globally affected.' }, { id: 'svb-2023', year: 2023, label: 'SVB Banking Crisis', date: '2023-03-10', category: 'credit', horizon_days: 30, assets: ['XLF','TLT','GLD','SPY','UUP'], hint: 'Silicon Valley Bank fails — largest US bank failure since 2008. Signature Bank closes. Contagion to Credit Suisse. Fed BTFP emergency facility. Deposit flight fear.' }, { id: 'opec-cut-2023', year: 2023, label: 'OPEC+ Surprise Production Cut', date: '2023-04-02', category: 'commodities', horizon_days: 60, assets: ['USO','XLE','SPY','GLD','UUP'], hint: 'OPEC+ shocks with 1.66Mb/d voluntary cuts beyond existing deal. WTI +6% in a day. Energy stocks surge. Inflation re-acceleration fear. Saudi strategy shift.' }, { id: 'debt-ceiling-2023', year: 2023, label: 'US Debt Ceiling X-Date', date: '2023-05-24', category: 'macro', horizon_days: 30, assets: ['TLT','SPY','GLD','UUP','BIL'], hint: 'Treasury X-date imminent. T-bill rates spike on short-end. Dollar stress. CDS on US sovereign widen. Last-minute Bipartisan deal expected but tail risk real.' }, { id: 'ai-mania-2023', year: 2023, label: 'AI Mania — Nvidia Surge', date: '2023-05-25', category: 'tech', horizon_days: 90, assets: ['QQQ','SPY','SMH','TLT','GLD'], hint: 'Nvidia Q1 blowout — revenue guidance +50%. AI bubble narrative goes mainstream. Mag7 concentration soars. Breadth collapses. Tech leads while rest of market flat.' }, { id: 'iran-israel-2024', year: 2024, label: 'Iran Direct Attack on Israel', date: '2024-04-14', category: 'geopolitical', horizon_days: 30, assets: ['GLD','USO','SPY','TLT','UUP'], hint: 'Iran launches 300+ drones/missiles at Israel in first-ever direct attack. Israel intercepts ~99%. Gold spikes to ATH. Oil +3%. Then rapid de-escalation. Premium collapses.' }, { id: 'carry-unwind-2024', year: 2024, label: 'JPY Carry Trade Unwind', date: '2024-08-05', category: 'fx_macro', horizon_days: 30, assets: ['USDJPY=X','EWJ','SPY','GLD','TLT'], hint: 'BoJ surprise rate hike triggers unwinding of $4T yen carry trade. Nikkei -12% in one session. VIX spikes to 65. Global deleveraging. Rapid recovery within 2 weeks.' }, { id: 'china-stimulus-2024', year: 2024, label: 'China Stimulus Bazooka', date: '2024-09-24', category: 'macro', horizon_days: 60, assets: ['FXI','EEM','KWEB','USO','GLD'], hint: 'PBOC surprises with rate cuts + reserve ratio cut + property support. Chinese equities +25% in 2 weeks. Commodity demand surge expected. Copper, iron ore, oil up.' }, { id: 'trump-elect-2024', year: 2024, label: 'Trump Election 2024 / Red Sweep', date: '2024-11-06', category: 'macro', horizon_days: 90, assets: ['SPY','QQQ','TLT','UUP','BTC-USD'], hint: 'Trump wins presidency + Republican Congress. Reflation 2.0: tax cuts, deregulation, tariffs. USD surges. Crypto explodes. Europe and EM underperform. Bonds sell.' }, { id: 'trump-tariff-2025', year: 2025, label: 'Trump Tariff Liberation Day', date: '2025-04-02', category: 'geopolitical', horizon_days: 90, assets: ['SPY','QQQ','GLD','UUP','EEM'], hint: 'Trump announces 145% tariffs on China + 10% universal tariff. Largest trade war since 1930. Recession fear. SPX -15% in 3 days. Gold ATH $3200. Treasury yields volatile.' }, ] const CATEGORIES: Record = { macro: { label: 'Macro', color: 'bg-blue-600' }, geopolitical:{ label: 'Geopolitical',color: 'bg-red-700' }, commodities: { label: 'Commodities', color: 'bg-amber-600' }, fx_macro: { label: 'FX/Macro', color: 'bg-violet-600' }, credit: { label: 'Credit', color: 'bg-orange-600' }, volatility: { label: 'Volatility', color: 'bg-cyan-600' }, tech: { label: 'Tech', color: 'bg-emerald-600' }, } const YEARS = [...new Set(PRESETS.map(p => p.year))].sort() // ── Small components ─────────────────────────────────────────────────────────── function CategoryBadge({ cat }: { cat: string }) { const c = CATEGORIES[cat] ?? { label: cat, color: 'bg-slate-600' } return {c.label} } function MoveBadge({ move, dir }: { move: number; dir: string }) { const ok = dir === 'up' ? move > 0 : dir === 'down' ? move < 0 : Math.abs(move) >= 3 return ( {move > 0 ? '+' : ''}{move.toFixed(1)}% ) } function OutcomeRow({ out }: { out: any }) { const hitType = out.hit_type ?? (out.hit ? 'full' : 'miss') const borderCls = hitType === 'full' ? 'border-emerald-700/30' : hitType === 'partial' ? 'border-amber-700/30' : 'border-red-700/30' return (
{/* Hit type */} {hitType === 'full' && ( FULL HIT )} {hitType === 'partial' && ( PARTIAL )} {hitType === 'miss' && ( MISS )} {/* Direction */} Dir: {out.direction_correct ? '✓' : '✗'} {out.direction_correct && out.direction_ratio != null && ( ({Math.round(out.direction_ratio * 100)}% of target) )} {/* Actual move */} Actual: vs {out.expected_direction === 'up' ? '+' : out.expected_direction === 'down' ? '-' : '±'}{out.expected_move_pct}% {/* Options P&L estimate */} {out.estimated_options_pnl_pct != null && ( = 0 ? 'text-emerald-400' : 'text-red-400' )}> {out.estimated_options_pnl_pct > 0 ? '+' : ''}{out.estimated_options_pnl_pct.toFixed(0)}% P&L )} {out.entry_date} → {out.exit_date}
) } // ── Main page ────────────────────────────────────────────────────────────────── // ── Shared instrument picker ─────────────────────────────────────────────────── function InstrumentPicker({ onSelect }: { onSelect: (ticker: string, name: string) => void }) { const [search, setSearch] = useState('') const [cat, setCat] = useState(null) const [custom, setCustom] = useState('') const [validating, setValidating] = useState(false) const [tickerError, setTickerError] = useState(null) const seen = new Set() const unique = INSTRUMENTS.filter(i => { if (seen.has(i.ticker)) return false; seen.add(i.ticker); return true }) const filtered = unique.filter(i => { if (cat && i.category !== cat) return false if (search) { const q = search.toLowerCase() return i.ticker.toLowerCase().includes(q) || i.name.toLowerCase().includes(q) } return true }) const handleCustom = async () => { const sym = custom.trim().toUpperCase() if (!sym) return setTickerError(null) setValidating(true) try { const res = await validateTicker(sym) if (res.valid) { onSelect(sym, res.name || sym) setCustom('') } else { setTickerError(res.reason || `Ticker '${sym}' not found`) } } catch { setTickerError(`Could not validate '${sym}' — check your connection`) } finally { setValidating(false) } } return (
{/* Category pills */}
{INSTRUMENT_CATEGORIES.map(c => ( ))}
{/* Search + custom */}
setSearch(e.target.value)} placeholder="Search…" className="w-full pl-6 pr-2 py-1.5 text-xs bg-dark-700 border border-slate-700/50 rounded text-slate-200 placeholder-slate-600 focus:outline-none focus:border-violet-500" />
{ setCustom(e.target.value); setTickerError(null) }} onKeyDown={e => e.key === 'Enter' && handleCustom()} placeholder="Custom ticker" className={clsx( 'w-28 px-2 py-1.5 text-xs bg-dark-700 border rounded font-mono text-slate-200 placeholder-slate-600 focus:outline-none uppercase', tickerError ? 'border-red-500/70 focus:border-red-400' : 'border-slate-700/50 focus:border-violet-500' )} />
{tickerError && (

{tickerError}

)}
{/* Grid */}
{filtered.map(inst => ( ))} {filtered.length === 0 && (
No match
)}
) } // ── Main page ────────────────────────────────────────────────────────────────── export default function PatternLab() { const { data: runsData } = usePatternLabRuns() const { mutateAsync: runBacktest, isPending: running } = useRunPatternLab() const { mutateAsync: evaluateRun, isPending: evaluating } = useEvaluatePatternLab() const { mutateAsync: savePattern } = useSaveLabPattern() const { mutate: deleteRun } = useDeleteLabRun() const { mutateAsync: runInstScan, isPending: scanning } = useInstrumentScan() const { mutateAsync: evaluateInst, isPending: evalInst } = useEvaluateInstrumentScan() const { mutateAsync: findMatching } = useFindMatchingPattern() const { mutateAsync: discoverEvents, isPending: discovering } = useDiscoverEvents() const runs: any[] = runsData ?? [] // ── Mode tabs const [mode, setMode] = useState<'events' | 'instrument'>('events') // ── Left panel tabs: presets vs discover const [leftTab, setLeftTab] = useState<'presets' | 'discover'>('presets') const [discoverQuery, setDiscoverQuery] = useState('') const [discoverResults, setDiscoverResults] = useState([]) const discoverInputRef = useRef(null) // ── Event preset filters const [search, setSearch] = useState('') const [yearFilter, setYearFilter] = useState(null) const [catFilter, setCatFilter] = useState(null) // ── Active experiment (events mode) const [selected, setSelected] = useState(null) const [editDate, setEditDate] = useState('') const [editHorizon, setEditHorizon] = useState(90) const [editAssets, setEditAssets] = useState('') const [editHint, setEditHint] = useState('') // ── Instrument scan mode const [instTicker, setInstTicker] = useState('') const [instName, setInstName] = useState('') const [instStart, setInstStart] = useState('2020-01-01') const [instEnd, setInstEnd] = useState('2023-12-31') const [instHorizon, setInstHorizon] = useState(90) const [instRun, setInstRun] = useState(null) const [instSavedIdx, setInstSavedIdx] = useState>(new Set()) // ── Current run state (events mode) const [activeRun, setActiveRun] = useState(null) const [savedIdx, setSavedIdx] = useState>(new Set()) const [toast, setToast] = useState<{ msg: string; type: 'ok' | 'err' } | null>(null) const [matchResults, setMatchResults] = useState>({}) const [matchLoading, setMatchLoading] = useState>({}) const filteredPresets = useMemo(() => PRESETS.filter(p => { if (yearFilter && p.year !== yearFilter) return false if (catFilter && p.category !== catFilter) return false if (search && !p.label.toLowerCase().includes(search.toLowerCase())) return false return true }), [search, yearFilter, catFilter]) const showToast = (msg: string, type: 'ok' | 'err' = 'ok') => { setToast({ msg, type }) setTimeout(() => setToast(null), 4000) } const handleSelectPreset = (p: Preset) => { setSelected(p) setEditDate(p.date) setEditHorizon(p.horizon_days) setEditAssets(p.assets.join(', ')) setEditHint(p.hint) setActiveRun(null) setSavedIdx(new Set()) setMatchResults({}) } const handleSelectDiscovered = (ev: DiscoveredEvent) => { const synth: Preset = { id: `discovered-${Date.now()}`, year: parseInt(ev.date.slice(0, 4)), label: ev.label, date: ev.date, category: ev.category, horizon_days: ev.horizon_days, assets: ev.assets, hint: ev.hint, } handleSelectPreset(synth) setLeftTab('presets') } const handleDiscover = async () => { if (!discoverQuery.trim()) return try { const res = await discoverEvents({ query: discoverQuery.trim(), n: 6 }) setDiscoverResults(res.events) } catch (e: any) { showToast(`Discover failed: ${e?.response?.data?.detail ?? e?.message}`, 'err') } } const handleRun = async () => { if (!selected && !editDate) return const assets = editAssets.split(',').map(s => s.trim()).filter(Boolean) try { const result = await runBacktest({ preset_id: selected?.id, theme: selected?.label ?? 'Custom', analysis_date: editDate, horizon_days: editHorizon, assets, theme_hint: editHint, }) setActiveRun(result) setSavedIdx(new Set()) } catch (e: any) { showToast(`Error: ${e?.response?.data?.detail ?? e?.message ?? 'unknown'}`, 'err') } } const handleEvaluate = async () => { if (!activeRun?.run_id) return try { const result = await evaluateRun(activeRun.run_id) setActiveRun((prev: any) => ({ ...prev, outcome: result.outcomes })) } catch (e: any) { showToast(`Error: ${e?.response?.data?.detail ?? e?.message ?? 'unknown'}`, 'err') } } const matchKey = (idx: number) => `${activeRun?.run_id}-${idx}` const handleFindMatching = async (idx: number) => { if (!activeRun?.run_id) return const key = matchKey(idx) setMatchLoading(prev => ({ ...prev, [key]: true })) try { const result = await findMatching({ run_id: activeRun.run_id, pattern_index: idx }) setMatchResults(prev => ({ ...prev, [key]: result })) } catch (e: any) { showToast(`Find matching failed: ${e?.response?.data?.detail ?? e?.message}`, 'err') } finally { setMatchLoading(prev => ({ ...prev, [key]: false })) } } const handleSave = async ( idx: number, action: 'new' | 'instance' | 'counter' = 'new', target_id?: string, regime_tag?: string, ) => { if (!activeRun?.run_id) { showToast('Lancez d\'abord un backtest (bouton Run)', 'err') return } const pat = activeRun.ai_result?.patterns?.[idx] if (!pat) return try { await savePattern({ run_id: activeRun.run_id, pattern_index: idx, name: pat.name, category: pat.category, signal_direction: pat.signal_direction, action, target_id, regime_tag, }) setSavedIdx(prev => new Set([...prev, idx])) const actionLabel = action === 'instance' ? 'merged as instance' : action === 'counter' ? 'saved as counter-scenario' : 'saved' showToast(`"${pat.name}" ${actionLabel}`, 'ok') } catch (e: any) { showToast(`Save failed: ${e?.response?.data?.detail ?? e?.message}`, 'err') } } // ── Instrument scan handlers const handleInstScan = async () => { if (!instTicker) return try { const result = await runInstScan({ ticker: instTicker, instrument_name: instName || instTicker, start_date: instStart, end_date: instEnd, horizon_days: instHorizon, }) setInstRun(result) setInstSavedIdx(new Set()) } catch (e: any) { showToast(`Error: ${e?.response?.data?.detail ?? e?.message ?? 'unknown'}`) } } const handleInstEvaluate = async () => { if (!instRun?.run_id) return try { const result = await evaluateInst(instRun.run_id) setInstRun((prev: any) => ({ ...prev, outcome: result.outcomes })) } catch (e: any) { showToast(`Error: ${e?.response?.data?.detail ?? e?.message ?? 'unknown'}`, 'err') } } const handleInstSave = async (idx: number) => { if (!instRun?.run_id) return const pat = instRun.ai_result?.patterns?.[idx] if (!pat) return try { await savePattern({ run_id: instRun.run_id, pattern_index: idx, name: pat.name, category: pat.category, signal_direction: pat.signal_direction }) setInstSavedIdx(prev => new Set([...prev, idx])) showToast(`"${pat.name}" saved to Pattern Library`, 'ok') } catch (e: any) { showToast(`Save failed: ${e?.response?.data?.detail ?? e?.message}`, 'err') } } // ── Event mode helpers const aiPatterns: any[] = activeRun?.ai_result?.patterns ?? [] const outcomes: any[] = activeRun?.outcome ?? [] const hasOutcomes = outcomes.length > 0 const outcomeForPattern = (name: string) => outcomes.find((o: any) => o.pattern_name === name) const scoredOutcomes = outcomes.filter((o: any) => 'hit_type' in o || 'hit' in o) const fullHits = outcomes.filter((o: any) => (o.hit_type ?? (o.hit ? 'full' : 'miss')) === 'full').length const partialHits = outcomes.filter((o: any) => (o.hit_type ?? '') === 'partial').length const hitRate = scoredOutcomes.length > 0 ? fullHits / scoredOutcomes.length : null // ── Instrument mode helpers const instPatterns: any[] = instRun?.ai_result?.patterns ?? [] const instOutcomes: any[] = instRun?.outcome ?? [] const instHasOut = instOutcomes.length > 0 const instOutcomeFor = (name: string) => instOutcomes.find((o: any) => o.pattern_name === name) const instScored = instOutcomes.filter((o: any) => 'hit_type' in o || 'hit' in o) const instFull = instOutcomes.filter((o: any) => (o.hit_type ?? (o.hit ? 'full' : 'miss')) === 'full').length const instPartial = instOutcomes.filter((o: any) => (o.hit_type ?? '') === 'partial').length const instHitRate = instScored.length > 0 ? instFull / instScored.length : null return (
{/* ── Left sidebar ────────────────────────────────────────────────────── */}
Pattern Lab {PRESETS.length} events
{/* Mode tabs */}
{/* Left panel sub-tabs: Presets | Discover */}
{/* ── Presets tab ── */} {leftTab === 'presets' && ( <>
setSearch(e.target.value)} placeholder="Search events..." className="w-full pl-7 pr-2 py-1.5 text-xs bg-dark-700 border border-slate-700/40 rounded text-slate-200 placeholder-slate-500 focus:outline-none focus:border-violet-600" />
{YEARS.map(y => ( ))}
{Object.entries(CATEGORIES).map(([key, { label }]) => ( ))}
{filteredPresets.map(p => ( ))} {filteredPresets.length === 0 && (
No events match filters
)}
)} {/* ── Discover tab ── */} {leftTab === 'discover' && ( <>
setDiscoverQuery(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleDiscover()} placeholder="e.g. Middle East crisis, dollar collapse…" className="w-full pl-6 pr-2 py-1.5 text-xs bg-dark-700 border border-violet-700/40 rounded text-slate-200 placeholder-slate-500 focus:outline-none focus:border-violet-500" />

AI searches its knowledge for matching events → click to pre-fill the lab

{discovering && (
Searching…
)} {!discovering && discoverResults.length === 0 && (
{discoverQuery ? 'No results yet — press Enter or click search' : 'Type a query to discover events'}
)} {!discovering && discoverResults.map((ev, i) => ( ))}
)} {/* Run history count */}
{runs.length} past run{runs.length !== 1 ? 's' : ''} in history
{/* ── Main content ────────────────────────────────────────────────────── */}
{/* ══ INSTRUMENT SCAN MODE ══════════════════════════════════════════ */} {mode === 'instrument' && (

Instrument Historical Scan

Pick an instrument — the AI scans a historical period, identifies the key patterns/events that drove it, and validates them against actual price data.

{/* Instrument picker */}
{instTicker ? Selected: {instTicker} — {instName} : 'Select Instrument'}
{!instTicker && ( { setInstTicker(t); setInstName(n) }} /> )}
{/* Period + horizon */} {instTicker && (
Scan Parameters
setInstStart(e.target.value)} className="w-full px-2 py-1.5 text-xs bg-dark-700 border border-slate-600/50 rounded text-slate-200 focus:outline-none focus:border-teal-500" />
setInstEnd(e.target.value)} className="w-full px-2 py-1.5 text-xs bg-dark-700 border border-slate-600/50 rounded text-slate-200 focus:outline-none focus:border-teal-500" />
setInstHorizon(Number(e.target.value))} className="flex-1 accent-teal-600" /> {instHorizon}d
)} {/* Instrument scan results */} {instPatterns.length > 0 && (
{instPatterns.length} Pattern Instances — {instTicker} {instStart} → {instEnd}
{instHasOut && instHitRate !== null && (
= 0.6 ? 'text-emerald-400' : instHitRate >= 0.4 ? 'text-yellow-400' : 'text-red-400')}> {Math.round(instHitRate * 100)}% full {instPartial > 0 && ( +{instPartial} partial )} ({instFull}/{instScored.length})
)} {!instHasOut && ( )}
{instPatterns.map((pat: any, idx: number) => { const out = instOutcomeFor(pat.name) const isSaved = instSavedIdx.has(idx) const DirIcon = pat.signal_direction === 'bullish' ? TrendingUp : pat.signal_direction === 'bearish' ? TrendingDown : Zap const dirCol = pat.signal_direction === 'bullish' ? 'text-emerald-400' : pat.signal_direction === 'bearish' ? 'text-red-400' : 'text-violet-400' return (
{ const ht = out?.hit_type ?? (out?.hit ? 'full' : out ? 'miss' : null) return ht === 'full' ? 'border-emerald-700/60 bg-emerald-900/10' : ht === 'partial' ? 'border-amber-700/40 bg-amber-900/10' : ht === 'miss' ? 'border-red-700/40 bg-red-900/10' : 'border-slate-700/40 bg-dark-700/30' })())}>
{pat.name} {pat.analysis_date} {pat.category && {pat.category}}

{pat.description}

Strategy: {pat.strategy} Expected: {pat.expected_direction === 'up' ? '+' : pat.expected_direction === 'down' ? '-' : '±'}{pat.expected_move_pct}% Horizon: {pat.horizon_days}d Confidence: = 70 ? 'text-emerald-400' : pat.confidence >= 50 ? 'text-yellow-400' : 'text-slate-400')}>{pat.confidence}
{pat.rationale &&

{pat.rationale}

} {out && }
{isSaved ? Saved : }
) })}
)}
)} {/* ══ EVENT PRESET MODE ════════════════════════════════════════════ */} {mode === 'events' && !selected && (

Select a historical event on the left

The AI will analyse what it would have done at that date using real market data

)} {mode === 'events' && selected && (
{/* ── Header ── */}

{selected.label}

{selected.hint}

{/* ── Setup card ── */}
Experiment Setup
setEditDate(e.target.value)} className="w-full px-2 py-1.5 text-xs bg-dark-700 border border-slate-600/50 rounded text-slate-200 focus:outline-none focus:border-violet-500" />
setEditHorizon(Number(e.target.value))} className="flex-1 accent-violet-600" /> {editHorizon}d
setEditAssets(e.target.value)} className="w-full px-2 py-1.5 text-xs bg-dark-700 border border-slate-600/50 rounded text-slate-200 focus:outline-none focus:border-violet-500 font-mono" />