import { useState, useMemo, useCallback, useEffect, useRef } from 'react' import { Link } from 'react-router-dom' import clsx from 'clsx' import { TreePine, Search, ChevronRight, ChevronDown, TrendingUp, TrendingDown, Zap, BookOpen, ExternalLink, RefreshCw, Layers, GitMerge, Tag, Trash2, Pencil, Check, X, ScanSearch, AlertTriangle, ArrowRightLeft, } from 'lucide-react' import { useAllPatterns, usePatternsByInstrument, useLastScores, useDeletePattern, usePatchPattern, useFindSimilarPattern, useMergePatterns, usePatternCalibration, validateTicker, type SimilarityResult, type PatternCalibration, } from '../hooks/useApi' import { INSTRUMENTS, INSTRUMENT_CATEGORIES } from '../constants/instruments' // ── Local types ─────────────────────────────────────────────────────────────── interface SuggestedTrade { strategy: string underlying: string rationale: string strike_guidance?: string expected_move_pct?: number } interface HistoricalInstance { date: string event: string [key: string]: unknown } interface Pattern { id: string name: string description: string triggers: string[] keywords: string[] historical_instances: HistoricalInstance[] suggested_trades: SuggestedTrade[] asset_class: string expected_move_pct: number probability: number horizon_days: number source: string category?: string signal_direction?: string taxonomy_path?: string[] ai_quality_score?: number is_active?: number regime_tag?: string counter_of?: string backtest_hits?: number backtest_runs_count?: number } interface ScoreResult { pattern_id: string score?: number [key: string]: unknown } // ── Constants ───────────────────────────────────────────────────────────────── const ASSET_CLASS_LABELS: Record = { energy: '⚡ Energy', metals: '🥇 Metals', agriculture: '🌾 Agri', equities: '📈 Equities', indices: '📊 Indices', forex: '💱 Forex', rates: '📉 Rates', } // ── Helpers ─────────────────────────────────────────────────────────────────── function probStars(prob: number): string { const n = prob >= 0.8 ? 3 : prob >= 0.6 ? 2 : 1 return '★'.repeat(n) + '☆'.repeat(3 - n) } function formatTaxPath(path: string[] | undefined): string { if (!path || path.length === 0) return '' return path.join(' › ') } // ── Signal Direction Badge ──────────────────────────────────────────────────── function SignalBadge({ direction }: { direction?: string }) { if (direction === 'bullish') { return ( Bullish ) } if (direction === 'bearish') { return ( Bearish ) } return ( Vol ) } // ── Category Badge ──────────────────────────────────────────────────────────── const CATEGORY_COLORS: Record = { geopolitical: 'bg-red-900/40 border-red-700/40 text-red-300', macro: 'bg-blue-900/40 border-blue-700/40 text-blue-300', volatility: 'bg-amber-900/40 border-amber-700/40 text-amber-300', 'risk-off': 'bg-orange-900/40 border-orange-700/40 text-orange-300', supply: 'bg-teal-900/40 border-teal-700/40 text-teal-300', credit: 'bg-purple-900/40 border-purple-700/40 text-purple-300', 'geo-eco': 'bg-emerald-900/40 border-emerald-700/40 text-emerald-300', sentiment: 'bg-yellow-900/40 border-yellow-700/40 text-yellow-300', } function CategoryBadge({ category }: { category: string }) { const cls = CATEGORY_COLORS[category] ?? 'bg-violet-900/40 border-violet-700/40 text-violet-300' return ( {category} ) } // ── Maturity / Calibration Badge ───────────────────────────────────────────── function MaturityBadge({ cal }: { cal: PatternCalibration }) { const w = cal.calibration_weight_pct const n = cal.n_mature_trades const hasData = n > 0 const sourceColor = { pure_ai: 'text-slate-500 border-slate-700/40', early: 'text-sky-400 border-sky-700/40', mixed: 'text-violet-400 border-violet-700/40', data_driven: 'text-emerald-400 border-emerald-700/40', }[cal.source] const sourceLabel = { pure_ai: 'Pure AI', early: 'Early data', mixed: 'Mixed', data_driven: 'Data-driven', }[cal.source] return (
{sourceLabel} {hasData ? `${n} trade${n > 1 ? 's' : ''} matures` : 'Aucun trade mature'}
{/* Blend bar */}
IA pure {w.toFixed(0)}% observé Observé
{hasData && (
{(cal.bayes_win_rate * 100).toFixed(0)}%
Win rate
{cal.ai_estimate != null ? `${cal.ai_estimate.toFixed(0)}%` : '—'}
IA estim.
{cal.calibrated_expected_move != null ? `${cal.calibrated_expected_move.toFixed(0)}%` : '—'}
Calibré
)}
) } // ── Pattern Card ────────────────────────────────────────────────────────────── function PatternCard({ pattern, highlightTrades, calibration, }: { pattern: Pattern highlightTrades?: string[] calibration?: PatternCalibration }) { const [expanded, setExpanded] = useState(false) const [confirmDel, setConfirmDel] = useState(false) const [editing, setEditing] = useState(false) const [editName, setEditName] = useState(pattern.name) const [editDesc, setEditDesc] = useState(pattern.description) const [editCat, setEditCat] = useState(pattern.category ?? '') const [editDir, setEditDir] = useState(pattern.signal_direction ?? '') const [editRegime, setEditRegime] = useState(pattern.regime_tag ?? '') const [simResult, setSimResult] = useState(null) const [confirmMerge, setConfirmMerge] = useState(false) const { mutate: deletePattern, isPending: deleting } = useDeletePattern() const { mutate: patchPattern, isPending: saving } = usePatchPattern() const { mutate: findSimilar, isPending: scanning } = useFindSimilarPattern() const { mutate: mergePatterns, isPending: merging } = useMergePatterns() const trades = pattern.suggested_trades ?? [] const instances = pattern.historical_instances ?? [] const assetLabel = ASSET_CLASS_LABELS[pattern.asset_class] ?? pattern.asset_class const isEditable = pattern.source !== 'builtin' const moveSign = pattern.expected_move_pct >= 0 ? '+' : '' const startEdit = () => { setEditName(pattern.name) setEditDesc(pattern.description) setEditCat(pattern.category ?? '') setEditDir(pattern.signal_direction ?? '') setEditRegime(pattern.regime_tag ?? '') setEditing(true) } const saveEdit = () => { patchPattern({ id: pattern.id, name: editName.trim() || pattern.name, description: editDesc.trim() || pattern.description, category: editCat || undefined, signal_direction: editDir || undefined, regime_tag: editRegime.trim() || undefined, }, { onSuccess: () => setEditing(false) }) } return (
{/* Header row */}
{pattern.id} {probStars(pattern.probability)} {!editing && pattern.category && } {!editing && pattern.regime_tag && ( #{pattern.regime_tag} )}
{pattern.ai_quality_score != null && !editing && ( AI {pattern.ai_quality_score}/100 )} {isEditable && !editing && !confirmDel && ( <> )} {isEditable && editing && ( <> )} {isEditable && !editing && ( confirmDel ? (
) : ( ) )}
{/* Edit mode fields */} {editing ? (
setEditName(e.target.value)} className="input text-sm font-semibold py-1.5 w-full" placeholder="Pattern name" />