feat: regime system — Find Matching + By Regime view
- Pattern Lab: "Find Matching" button per pattern uses GPT-4o-mini to classify against library (merge_as_instance / counter_scenario / new_pattern); shows match badge + confidence + suggested #regime_tag; conditional action buttons (Merge / Save counter / Save new) - save_pattern_from_run handles action='instance' (appends to historical_instances + updates stats), action='counter' (new pattern with counter_of link, tags parent regime_tag), action='new' (unchanged + regime_tag support) - useApi.ts: extended useSaveLabPattern type + new useFindMatchingPattern mutation + MatchResult export type - DB migrations: regime_tag + counter_of columns on custom_patterns - PatternExplorer: new "By Regime" view groups saved patterns by regime_tag; RegimeCard shows historical instances, hit rate, counter-of link (orange); untagged group at bottom Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,12 +3,14 @@ import {
|
||||
useRunPatternLab, useEvaluatePatternLab, useSaveLabPattern,
|
||||
usePatternLabRuns, useDeleteLabRun,
|
||||
useInstrumentScan, useEvaluateInstrumentScan,
|
||||
useFindMatchingPattern, MatchResult,
|
||||
validateTicker,
|
||||
} from '../hooks/useApi'
|
||||
import {
|
||||
FlaskConical, Play, CheckCircle2, XCircle, MinusCircle,
|
||||
Trash2, Save, RefreshCw, Search, CalendarDays,
|
||||
TrendingUp, TrendingDown, Zap, BarChart2, ScanLine, DollarSign,
|
||||
GitMerge, Layers, Plus,
|
||||
} from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
import { INSTRUMENTS, INSTRUMENT_CATEGORIES } from '../constants/instruments'
|
||||
@@ -255,6 +257,7 @@ export default function PatternLab() {
|
||||
const { mutate: deleteRun } = useDeleteLabRun()
|
||||
const { mutateAsync: runInstScan, isPending: scanning } = useInstrumentScan()
|
||||
const { mutateAsync: evaluateInst, isPending: evalInst } = useEvaluateInstrumentScan()
|
||||
const { mutateAsync: findMatching } = useFindMatchingPattern()
|
||||
|
||||
const runs: any[] = runsData ?? []
|
||||
|
||||
@@ -283,9 +286,11 @@ export default function PatternLab() {
|
||||
const [instSavedIdx, setInstSavedIdx] = useState<Set<number>>(new Set())
|
||||
|
||||
// ── Current run state (events mode)
|
||||
const [activeRun, setActiveRun] = useState<any | null>(null)
|
||||
const [savedIdx, setSavedIdx] = useState<Set<number>>(new Set())
|
||||
const [toast, setToast] = useState<string | null>(null)
|
||||
const [activeRun, setActiveRun] = useState<any | null>(null)
|
||||
const [savedIdx, setSavedIdx] = useState<Set<number>>(new Set())
|
||||
const [toast, setToast] = useState<string | null>(null)
|
||||
const [matchResults, setMatchResults] = useState<Record<string, MatchResult>>({})
|
||||
const [matchLoading, setMatchLoading] = useState<Record<string, boolean>>({})
|
||||
|
||||
const filteredPresets = useMemo(() => PRESETS.filter(p => {
|
||||
if (yearFilter && p.year !== yearFilter) return false
|
||||
@@ -338,7 +343,28 @@ export default function PatternLab() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async (idx: number) => {
|
||||
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}`)
|
||||
} 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) return
|
||||
const pat = activeRun.ai_result?.patterns?.[idx]
|
||||
if (!pat) return
|
||||
@@ -349,9 +375,13 @@ export default function PatternLab() {
|
||||
name: pat.name,
|
||||
category: pat.category,
|
||||
signal_direction: pat.signal_direction,
|
||||
action,
|
||||
target_id,
|
||||
regime_tag,
|
||||
})
|
||||
setSavedIdx(prev => new Set([...prev, idx]))
|
||||
showToast(`"${pat.name}" saved to Pattern Library`)
|
||||
const actionLabel = action === 'instance' ? 'merged as instance' : action === 'counter' ? 'saved as counter-scenario' : 'saved'
|
||||
showToast(`"${pat.name}" ${actionLabel}`)
|
||||
} catch (e: any) {
|
||||
showToast(`Save failed: ${e?.response?.data?.detail ?? e?.message}`)
|
||||
}
|
||||
@@ -853,18 +883,65 @@ export default function PatternLab() {
|
||||
{out && <OutcomeRow out={out} />}
|
||||
</div>
|
||||
|
||||
{/* Save button */}
|
||||
<div className="flex-shrink-0">
|
||||
{/* Save / Find Matching buttons */}
|
||||
<div className="flex-shrink-0 flex flex-col items-end gap-2">
|
||||
{isSaved ? (
|
||||
<span className="flex items-center gap-1 text-emerald-400 text-xs">
|
||||
<CheckCircle2 className="w-3.5 h-3.5" /> Saved
|
||||
</span>
|
||||
) : (
|
||||
<button onClick={() => handleSave(idx)}
|
||||
className="flex items-center gap-1.5 border border-slate-600 hover:border-violet-500 hover:text-violet-300 text-slate-400 px-2.5 py-1.5 rounded text-xs transition-colors">
|
||||
<Save className="w-3 h-3" /> Save pattern
|
||||
</button>
|
||||
)}
|
||||
) : (() => {
|
||||
const key = matchKey(idx)
|
||||
const mr = matchResults[key]
|
||||
const loading = matchLoading[key]
|
||||
if (mr) {
|
||||
const isInstance = mr.recommendation === 'merge_as_instance'
|
||||
const isCounter = mr.recommendation === 'counter_scenario'
|
||||
const badgeColor = isInstance ? 'text-emerald-400 border-emerald-600' : isCounter ? 'text-orange-400 border-orange-600' : 'text-slate-400 border-slate-600'
|
||||
return (
|
||||
<div className="flex flex-col items-end gap-1.5">
|
||||
<div className={`text-[10px] border rounded px-1.5 py-0.5 ${badgeColor}`}>
|
||||
{isInstance ? <><GitMerge className="w-3 h-3 inline mr-1" />Match: {mr.match_name} ({mr.confidence}%)</>
|
||||
: isCounter ? <><Layers className="w-3 h-3 inline mr-1" />Counter of: {mr.match_name} ({mr.confidence}%)</>
|
||||
: <><Plus className="w-3 h-3 inline mr-1" />New pattern</>}
|
||||
</div>
|
||||
{mr.suggested_regime_tag && (
|
||||
<div className="text-[10px] text-violet-400 font-mono">#{mr.suggested_regime_tag}</div>
|
||||
)}
|
||||
<div className="flex gap-1.5">
|
||||
{isInstance && mr.match_id && (
|
||||
<button onClick={() => handleSave(idx, 'instance', mr.match_id, mr.suggested_regime_tag)}
|
||||
className="flex items-center gap-1 border border-emerald-600 hover:border-emerald-400 text-emerald-400 px-2 py-1 rounded text-[10px] transition-colors">
|
||||
<GitMerge className="w-3 h-3" /> Merge
|
||||
</button>
|
||||
)}
|
||||
{isCounter && mr.match_id && (
|
||||
<button onClick={() => handleSave(idx, 'counter', mr.match_id, mr.suggested_regime_tag)}
|
||||
className="flex items-center gap-1 border border-orange-600 hover:border-orange-400 text-orange-400 px-2 py-1 rounded text-[10px] transition-colors">
|
||||
<Layers className="w-3 h-3" /> Save counter
|
||||
</button>
|
||||
)}
|
||||
<button onClick={() => handleSave(idx, 'new', undefined, mr.suggested_regime_tag)}
|
||||
className="flex items-center gap-1 border border-slate-600 hover:border-slate-400 text-slate-400 px-2 py-1 rounded text-[10px] transition-colors">
|
||||
<Save className="w-3 h-3" /> Save new
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="flex gap-1.5">
|
||||
<button onClick={() => handleFindMatching(idx)} disabled={loading}
|
||||
className="flex items-center gap-1.5 border border-violet-700 hover:border-violet-400 text-violet-400 px-2.5 py-1.5 rounded text-xs transition-colors disabled:opacity-50">
|
||||
{loading ? <RefreshCw className="w-3 h-3 animate-spin" /> : <GitMerge className="w-3 h-3" />}
|
||||
Find matching
|
||||
</button>
|
||||
<button onClick={() => handleSave(idx)}
|
||||
className="flex items-center gap-1.5 border border-slate-600 hover:border-violet-500 hover:text-violet-300 text-slate-400 px-2.5 py-1.5 rounded text-xs transition-colors">
|
||||
<Save className="w-3 h-3" /> Save
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user