feat: pattern calibration — progressive AI→observed expected_move blending
DB (database.py): - 3 new columns on custom_patterns: calibrated_expected_move, calibration_weight, observed_avg_win_pct - update_bayesian_posteriors() now also computes credibility blend w=n/(n+5): calibrated = (1-w)*ai_estimate + w*observed_avg_win_pct (only when wins exist) - log_trade_entries() prefers calibrated_expected_move when w>10% - get_calibration_summary() returns per-pattern state (source: pure_ai/early/mixed/data_driven) Backend (patterns.py, auto_cycle.py): - GET /api/patterns/calibration endpoint - calibration_report block in cycle report: counts by source, avg weight, per-pattern detail Frontend (PatternExplorer.tsx, RapportIA.tsx, useApi.ts): - MaturityBadge on each PatternCard: blend bar (AI→observed), win rate, AI estimate vs calibrated - usePatternCalibration hook - Cycle report: calibration section with global bar + per-pattern table (weight%, n_trades, WR, AI→calibrated) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,8 +14,10 @@ import {
|
||||
usePatchPattern,
|
||||
useFindSimilarPattern,
|
||||
useMergePatterns,
|
||||
usePatternCalibration,
|
||||
validateTicker,
|
||||
type SimilarityResult,
|
||||
type PatternCalibration,
|
||||
} from '../hooks/useApi'
|
||||
import { INSTRUMENTS, INSTRUMENT_CATEGORIES } from '../constants/instruments'
|
||||
|
||||
@@ -135,14 +137,87 @@ function CategoryBadge({ category }: { category: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
// ── 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 (
|
||||
<div className="border-t border-slate-700/30 pt-2 mt-1 space-y-1.5">
|
||||
<div className="flex items-center justify-between text-[10px]">
|
||||
<span className={clsx('font-semibold px-1.5 py-0.5 rounded border', sourceColor)}>
|
||||
{sourceLabel}
|
||||
</span>
|
||||
<span className="text-slate-500">
|
||||
{hasData ? `${n} trade${n > 1 ? 's' : ''} matures` : 'Aucun trade mature'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Blend bar */}
|
||||
<div className="space-y-0.5">
|
||||
<div className="flex justify-between text-[10px] text-slate-500">
|
||||
<span>IA pure</span>
|
||||
<span className="text-slate-400 font-mono">{w.toFixed(0)}% observé</span>
|
||||
<span>Observé</span>
|
||||
</div>
|
||||
<div className="h-1.5 rounded-full bg-slate-700/60 overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full bg-gradient-to-r from-slate-500 to-violet-500 transition-all duration-500"
|
||||
style={{ width: `${Math.max(w, 2)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasData && (
|
||||
<div className="grid grid-cols-3 gap-1 text-[10px]">
|
||||
<div className="bg-dark-700/60 rounded px-1.5 py-1 text-center">
|
||||
<div className="text-slate-400 font-mono">{(cal.bayes_win_rate * 100).toFixed(0)}%</div>
|
||||
<div className="text-slate-600">Win rate</div>
|
||||
</div>
|
||||
<div className="bg-dark-700/60 rounded px-1.5 py-1 text-center">
|
||||
<div className="text-slate-400 font-mono">
|
||||
{cal.ai_estimate != null ? `${cal.ai_estimate.toFixed(0)}%` : '—'}
|
||||
</div>
|
||||
<div className="text-slate-600">IA estim.</div>
|
||||
</div>
|
||||
<div className={clsx('rounded px-1.5 py-1 text-center', cal.calibrated_expected_move ? 'bg-violet-900/30' : 'bg-dark-700/60')}>
|
||||
<div className={clsx('font-mono', cal.calibrated_expected_move ? 'text-violet-300' : 'text-slate-600')}>
|
||||
{cal.calibrated_expected_move != null ? `${cal.calibrated_expected_move.toFixed(0)}%` : '—'}
|
||||
</div>
|
||||
<div className="text-slate-600">Calibré</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Pattern Card ──────────────────────────────────────────────────────────────
|
||||
|
||||
function PatternCard({
|
||||
pattern,
|
||||
highlightTrades,
|
||||
calibration,
|
||||
}: {
|
||||
pattern: Pattern
|
||||
highlightTrades?: string[]
|
||||
calibration?: PatternCalibration
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [confirmDel, setConfirmDel] = useState(false)
|
||||
@@ -373,6 +448,9 @@ function PatternCard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Maturity & calibration */}
|
||||
{calibration && <MaturityBadge cal={calibration} />}
|
||||
|
||||
{/* Similarity result panel */}
|
||||
{simResult && (
|
||||
<div className={clsx(
|
||||
@@ -503,7 +581,7 @@ const DIR_COLORS: Record<string, string> = {
|
||||
neutral: 'bg-amber-900/40 border-amber-600/50 text-amber-300',
|
||||
}
|
||||
|
||||
function FacetedSearch({ patterns }: { patterns: Pattern[] }) {
|
||||
function FacetedSearch({ patterns, calibrationMap = {} }: { patterns: Pattern[]; calibrationMap?: Record<string, PatternCalibration> }) {
|
||||
const [query, setQuery] = useState('')
|
||||
const [direction, setDirection] = useState('')
|
||||
const [assetClass, setAssetClass] = useState('')
|
||||
@@ -658,7 +736,7 @@ function FacetedSearch({ patterns }: { patterns: Pattern[] }) {
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-3">
|
||||
{filtered.map(p => <PatternCard key={p.id} pattern={p} />)}
|
||||
{filtered.map(p => <PatternCard key={p.id} pattern={p} calibration={calibrationMap[p.id]} />)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -1025,6 +1103,12 @@ export default function PatternExplorer() {
|
||||
const [view, setView] = useState<ViewMode>('search')
|
||||
const { data: patternsData, isLoading } = useAllPatterns()
|
||||
const { data: _scores } = useLastScores()
|
||||
const { data: calibrationData } = usePatternCalibration()
|
||||
|
||||
const calibrationMap = useMemo<Record<string, PatternCalibration>>(() => {
|
||||
if (!calibrationData) return {}
|
||||
return Object.fromEntries(calibrationData.map(c => [c.pattern_id, c]))
|
||||
}, [calibrationData])
|
||||
|
||||
const patterns = useMemo<Pattern[]>(() => {
|
||||
if (!Array.isArray(patternsData)) return []
|
||||
@@ -1101,7 +1185,7 @@ export default function PatternExplorer() {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{view === 'search' && <FacetedSearch patterns={patterns} />}
|
||||
{view === 'search' && <FacetedSearch patterns={patterns} calibrationMap={calibrationMap} />}
|
||||
{view === 'instrument' && <InstrumentLens patterns={patterns} />}
|
||||
{view === 'regime' && <RegimeView patterns={patterns} />}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user