feat: Phase 2 — Pattern Reliability, Contre-thèses & Calibration probabiliste
Sprint 2.1 — Pattern Reliability Score - database.py: get_pattern_reliability() — win_rate × log(n+1) sur trades matures (≥35% horizon) - database.py: get_all_pattern_reliability_map() pour injection rapide dans les prompts - ai_analyzer.py: inject reliability_map dans suggest_patterns (patterns fiables mis en avant) - auto_cycle.py: charge reliability_map avant suggestion et le passe au suggéreur - routers/analytics.py: GET /api/analytics/reliability - PatternEditor.tsx: ReliabilityBadge sur chaque card + usePatternReliability hook - useApi.ts: usePatternReliability, useCalibration hooks Sprint 2.2 — Contre-thèses & Invalidation Triggers - database.py: migration ALTER TABLE — counter_thesis, invalidation_trigger, invalidation_probability - database.py: save_custom_pattern() persiste les 3 nouveaux champs - ai_analyzer.py: counter_thesis + invalidation_trigger + invalidation_probability dans le JSON schema - auto_cycle.py: détection automatique des triggers d'invalidation contre les news (keyword match) - routers/analytics.py: GET /api/analytics/invalidation-alerts - PatternEditor.tsx: affichage contre-thèse dans les cards + champs dans le formulaire - PatternEditor.tsx: affichage dans AiSuggestModal (suggestions IA) - routers/patterns.py: PatternRequest inclut les 3 nouveaux champs Sprint 2.3 — Calibration probabiliste & Demi-vie KB - database.py: migration — predicted_probability sur pattern_score_history - database.py: save_pattern_scores() stocke probability du pattern à chaque scoring run - database.py: get_calibration_data() — Brier score + buckets de calibration par décile - database.py: expires_at + confidence_decay_days sur knowledge_base - database.py: decay_kb_confidence() — decay automatique + archivage à 0 - auto_cycle.py: decay_kb_confidence() appelé au début de chaque cycle (non-bloquant) - routers/analytics.py: GET /api/analytics/calibration + POST /api/analytics/kb/decay - frontend/src/pages/Analytics.tsx: nouvelle page — tableau fiabilité + calibration Brier - App.tsx + Sidebar.tsx: route /analytics + entrée menu Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useState, useMemo } from 'react'
|
||||
import { useAllPatterns, useSavePattern, useDeletePattern, useEvaluatePattern, useSuggestPattern, useAiStatus, useSuggestNewPatterns, useTogglePattern, usePatternSimilarity, useLastScores } from '../hooks/useApi'
|
||||
import { useAllPatterns, useSavePattern, useDeletePattern, useEvaluatePattern, useSuggestPattern, useAiStatus, useSuggestNewPatterns, useTogglePattern, usePatternSimilarity, useLastScores, usePatternReliability } from '../hooks/useApi'
|
||||
import clsx from 'clsx'
|
||||
import { Zap, Plus, Trash2, Edit3, Brain, Save, RotateCcw, Sparkles, X, Check, Eye, EyeOff } from 'lucide-react'
|
||||
import { Zap, Plus, Trash2, Edit3, Brain, Save, RotateCcw, Sparkles, X, Check, Eye, EyeOff, ShieldAlert } from 'lucide-react'
|
||||
|
||||
function jaccard(a: string[], b: string[]): number {
|
||||
if (!a.length && !b.length) return 0
|
||||
@@ -26,6 +26,7 @@ const EMPTY_PATTERN = {
|
||||
name: '', description: '', triggers: [] as string[], keywords: [] as string[],
|
||||
historical_instances: [] as any[], suggested_trades: [] as any[],
|
||||
asset_class: 'energy', expected_move_pct: 10, probability: 0.6, horizon_days: 30,
|
||||
counter_thesis: '', invalidation_trigger: '', invalidation_probability: undefined as number | undefined,
|
||||
}
|
||||
|
||||
function QualityBadge({ score }: { score: number }) {
|
||||
@@ -34,10 +35,24 @@ function QualityBadge({ score }: { score: number }) {
|
||||
return <span className={clsx('badge', color)}>{score}/100 — {label}</span>
|
||||
}
|
||||
|
||||
function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore }: {
|
||||
function ReliabilityBadge({ rel }: { rel: any }) {
|
||||
if (!rel || rel.trade_count < 3) return null
|
||||
const wr = rel.win_rate_pct
|
||||
const color = wr >= 60 ? 'text-emerald-400 border-emerald-700/40 bg-emerald-900/20'
|
||||
: wr >= 40 ? 'text-amber-400 border-amber-700/40 bg-amber-900/20'
|
||||
: 'text-red-400 border-red-700/40 bg-red-900/20'
|
||||
return (
|
||||
<span className={clsx('inline-flex items-center gap-1 text-[10px] border rounded px-1.5 py-0.5 font-mono', color)}>
|
||||
📊 {wr}% WR ({rel.trade_count}t · ⌀{rel.avg_pnl_pct > 0 ? '+' : ''}{rel.avg_pnl_pct}%)
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliability }: {
|
||||
p: any; onEdit: () => void; onDelete: () => void; onToggle: () => void
|
||||
similarTo?: Array<{ name: string; similarity: number }>
|
||||
aiScore?: number | null
|
||||
reliability?: any
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const isCustom = p.source === 'custom'
|
||||
@@ -55,6 +70,7 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore }: {
|
||||
{isCustom && <span className="badge badge-purple text-xs">Custom</span>}
|
||||
{!isActive && <span className="badge badge-red text-xs">Désactivé</span>}
|
||||
{p.ai_quality_score && <QualityBadge score={p.ai_quality_score} />}
|
||||
<ReliabilityBadge rel={reliability} />
|
||||
</div>
|
||||
<div className="text-xs text-slate-500">{p.description}</div>
|
||||
</div>
|
||||
@@ -142,6 +158,26 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore }: {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{(p.counter_thesis || p.invalidation_trigger) && (
|
||||
<div className="card-sm border-red-700/30 bg-red-900/10">
|
||||
<div className="text-xs text-red-400 font-semibold mb-1 flex items-center gap-1">
|
||||
<ShieldAlert className="w-3 h-3" /> Contre-thèse & Invalidation
|
||||
</div>
|
||||
{p.counter_thesis && (
|
||||
<div className="text-xs text-slate-300 mb-1">
|
||||
<span className="text-slate-500">Contre-thèse :</span> {p.counter_thesis}
|
||||
</div>
|
||||
)}
|
||||
{p.invalidation_trigger && (
|
||||
<div className="text-xs text-orange-300/80">
|
||||
<span className="text-slate-500">Trigger :</span> {p.invalidation_trigger}
|
||||
{p.invalidation_probability != null && (
|
||||
<span className="ml-2 font-mono text-slate-500">({Math.round(p.invalidation_probability * 100)}% prob.)</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{p.ai_evaluation && Object.keys(p.ai_evaluation).length > 0 && (
|
||||
<div className="card-sm border-blue-700/30">
|
||||
<div className="text-xs text-blue-400 font-semibold mb-1">Évaluation IA</div>
|
||||
@@ -269,6 +305,16 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
|
||||
<span>{p.macro_fit}</span>
|
||||
</div>
|
||||
)}
|
||||
{(p.counter_thesis || p.invalidation_trigger) && (
|
||||
<div className="mb-2 text-[11px] bg-red-900/10 border border-red-700/20 rounded px-2 py-1.5 space-y-0.5">
|
||||
{p.counter_thesis && (
|
||||
<div className="text-slate-300"><span className="text-red-400/80">⚠ Contre-thèse :</span> {p.counter_thesis}</div>
|
||||
)}
|
||||
{p.invalidation_trigger && (
|
||||
<div className="text-orange-300/70"><span className="text-slate-500">Trigger :</span> {p.invalidation_trigger}{p.invalidation_probability != null ? ` (${Math.round(p.invalidation_probability * 100)}%)` : ''}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{p.suggested_trades?.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
{p.suggested_trades.map((t: any, ti: number) => (
|
||||
@@ -477,6 +523,33 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contre-thèse & Invalidation */}
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block flex items-center gap-1">
|
||||
<ShieldAlert className="w-3 h-3 text-red-400" /> Contre-thèse (scénario adverse principal)
|
||||
</label>
|
||||
<textarea value={form.counter_thesis || ''} onChange={e => set('counter_thesis', e.target.value)}
|
||||
rows={2} placeholder="Ex: accord de paix inattendu, données inflation sous 3%, pivot Fed…"
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-red-500/50 resize-none" />
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="col-span-2">
|
||||
<label className="text-xs text-slate-500 mb-1 block">Trigger d'invalidation (événement précis & mesurable)</label>
|
||||
<input value={form.invalidation_trigger || ''} onChange={e => set('invalidation_trigger', e.target.value)}
|
||||
placeholder="Ex: prix pétrole < 70$/b 3j consécutifs"
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-red-500/50" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Prob. invalidation (0-1)</label>
|
||||
<input type="number" step="0.05" min="0" max="1" value={form.invalidation_probability ?? ''}
|
||||
onChange={e => set('invalidation_probability', e.target.value ? Number(e.target.value) : undefined)}
|
||||
placeholder="0.20"
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-red-500/50" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI Evaluation result */}
|
||||
{aiResult && (
|
||||
<div className="card border-blue-700/40">
|
||||
@@ -557,6 +630,7 @@ export default function PatternEditor() {
|
||||
const { data: aiStatus } = useAiStatus()
|
||||
const { data: simData } = usePatternSimilarity()
|
||||
const { data: lastScoresData } = useLastScores()
|
||||
const { data: reliabilityData } = usePatternReliability()
|
||||
const [editing, setEditing] = useState<any>(null)
|
||||
const [creating, setCreating] = useState(false)
|
||||
const [showAiSuggest, setShowAiSuggest] = useState(false)
|
||||
@@ -576,6 +650,15 @@ export default function PatternEditor() {
|
||||
return map
|
||||
}, [lastScoresData])
|
||||
|
||||
// Build reliability map: patternId → reliability record
|
||||
const reliabilityMap = useMemo(() => {
|
||||
const map: Record<string, any> = {}
|
||||
for (const r of (reliabilityData as any)?.reliability ?? []) {
|
||||
map[r.pattern_id] = r
|
||||
}
|
||||
return map
|
||||
}, [reliabilityData])
|
||||
|
||||
// Build a map: patternId → [{name, similarity}] for each side of a similar pair
|
||||
const similarityMap = useMemo(() => {
|
||||
const pairs: any[] = (simData as any)?.pairs ?? []
|
||||
@@ -674,6 +757,7 @@ export default function PatternEditor() {
|
||||
onToggle={() => togglePattern(p.id)}
|
||||
similarTo={similarityMap[p.id]}
|
||||
aiScore={aiScoreMap[p.id] ?? null}
|
||||
reliability={reliabilityMap[p.id]}
|
||||
/>
|
||||
))}
|
||||
{displayPatterns.length === 0 && (
|
||||
|
||||
Reference in New Issue
Block a user