feat: instrument model
This commit is contained in:
@@ -7,7 +7,7 @@ import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
|
||||
import {
|
||||
RefreshCw, Edit3, X, Trash2, ChevronDown, ChevronUp,
|
||||
TrendingUp, TrendingDown, Minus, LineChart, Table2, Network, Activity,
|
||||
Plus, Zap,
|
||||
Plus, Zap, Settings2, Check,
|
||||
} from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
import axios from 'axios'
|
||||
@@ -29,6 +29,7 @@ interface ModelNode {
|
||||
event_category?: string
|
||||
formula?: string
|
||||
display_col: number
|
||||
macro_key?: string
|
||||
// computed
|
||||
computed_value: number
|
||||
pip_contribution: number
|
||||
@@ -153,10 +154,43 @@ interface CalendarEvent {
|
||||
surprise_delta: number | null
|
||||
}
|
||||
|
||||
interface MacroGuidanceItem {
|
||||
node_id: string
|
||||
node_label: string
|
||||
macro_key: string
|
||||
unit: string
|
||||
current_value: number | null
|
||||
next_event: {
|
||||
date: string
|
||||
name: string
|
||||
forecast: number | null
|
||||
days_until: number
|
||||
} | null
|
||||
}
|
||||
|
||||
// ── Constants ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const INSTRUMENTS = ['EURUSD','USDJPY','XAUUSD','SP500','TLT','GBPUSD','EEM','QQQ']
|
||||
|
||||
const MACRO_KEY_OPTIONS = [
|
||||
{ value: '', label: '— aucune clé macro —' },
|
||||
{ value: 'fed_rate', label: 'Taux Fed (USD)' },
|
||||
{ value: 'ecb_rate', label: 'Taux BCE (EUR)' },
|
||||
{ value: 'boe_rate', label: 'Taux BoE (GBP)' },
|
||||
{ value: 'boj_rate', label: 'Taux BoJ (JPY)' },
|
||||
{ value: 'us_cpi', label: 'CPI US MoM' },
|
||||
{ value: 'us_cpi_yoy', label: 'CPI US YoY' },
|
||||
{ value: 'eu_cpi_yoy', label: 'HICP Eurozone YoY' },
|
||||
{ value: 'us_nfp', label: 'NFP US (emplois K)' },
|
||||
{ value: 'us_pmi', label: 'PMI US (ISM)' },
|
||||
{ value: 'eu_pmi', label: 'PMI Eurozone' },
|
||||
{ value: 'us_gdp', label: 'GDP US QoQ %' },
|
||||
{ value: 'eu_gdp', label: 'GDP Eurozone %' },
|
||||
{ value: 'us_unemployment', label: 'Chômage US %' },
|
||||
{ value: 'eu_unemployment', label: 'Chômage Eurozone %' },
|
||||
{ value: 'us_retail_sales', label: 'Ventes détail US MoM' },
|
||||
]
|
||||
|
||||
const NODE_TYPE_META: Record<NodeType, { label: string; color: string; bg: string }> = {
|
||||
input_event: { label: 'Events', color: 'text-sky-400', bg: 'bg-sky-900/30 border-sky-700/40' },
|
||||
input_manual: { label: 'Manuel', color: 'text-violet-400', bg: 'bg-violet-900/30 border-violet-700/40' },
|
||||
@@ -1954,9 +1988,143 @@ function CalibrationView({ instrument, eventDetails }: {
|
||||
)
|
||||
}
|
||||
|
||||
// ── MacroConfigView ────────────────────────────────────────────────────────────
|
||||
|
||||
function MacroConfigView({ instrument, nodes }: { instrument: string; nodes: ModelNode[] }) {
|
||||
const [guidance, setGuidance] = useState<MacroGuidanceItem[]>([])
|
||||
const [localKeys, setLocalKeys] = useState<Record<string, string>>({})
|
||||
const [saved, setSaved] = useState<Record<string, boolean>>({})
|
||||
const [saving, setSaving] = useState<string | null>(null)
|
||||
|
||||
const manualNodes = nodes.filter(n => n.node_type === 'input_manual')
|
||||
|
||||
useEffect(() => {
|
||||
const init: Record<string, string> = {}
|
||||
for (const n of manualNodes) init[n.id] = n.macro_key ?? ''
|
||||
setLocalKeys(init)
|
||||
}, [nodes])
|
||||
|
||||
const refreshGuidance = useCallback(() => {
|
||||
api.get(`/instrument-models/${instrument}/macro-guidance`)
|
||||
.then(r => setGuidance(r.data))
|
||||
.catch(() => {})
|
||||
}, [instrument])
|
||||
|
||||
useEffect(() => { refreshGuidance() }, [refreshGuidance])
|
||||
|
||||
async function saveMacroKey(nodeId: string, mk: string) {
|
||||
setSaving(nodeId)
|
||||
try {
|
||||
await api.patch(`/instrument-models/${instrument}/nodes/${nodeId}`, { macro_key: mk })
|
||||
setSaved(prev => ({ ...prev, [nodeId]: true }))
|
||||
setTimeout(() => setSaved(prev => ({ ...prev, [nodeId]: false })), 2000)
|
||||
refreshGuidance()
|
||||
} catch {}
|
||||
setSaving(null)
|
||||
}
|
||||
|
||||
const linkedCount = Object.values(localKeys).filter(Boolean).length
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-white">Paramètres macro des nœuds</div>
|
||||
<div className="text-xs text-slate-500 mt-0.5">
|
||||
Liez chaque nœud manuel à une variable macro-économique. La machine interpolera
|
||||
entre les publications FF Calendar pour créer une guidance temporelle des fondamentaux.
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-violet-400 font-mono">
|
||||
{linkedCount}/{manualNodes.length} liés
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Guidance summary cards — nodes that already have macro keys */}
|
||||
{guidance.length > 0 && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
||||
{guidance.map(g => (
|
||||
<div key={g.node_id} className="rounded-lg border border-violet-700/30 bg-violet-900/10 p-3">
|
||||
<div className="text-xs text-violet-400 font-medium truncate">{g.node_label}</div>
|
||||
<div className="text-xs text-slate-500 mb-1.5">{g.macro_key}</div>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-base font-bold font-mono text-white">
|
||||
{g.current_value != null ? g.current_value.toFixed(2) : '—'}
|
||||
</span>
|
||||
<span className="text-xs text-slate-500">{g.unit}</span>
|
||||
</div>
|
||||
{g.next_event && (
|
||||
<div className="text-xs text-slate-500 mt-1">
|
||||
<span className="text-amber-400">→ {g.next_event.forecast != null ? g.next_event.forecast.toFixed(2) : '?'}</span>
|
||||
<span className="ml-1">dans {g.next_event.days_until}j</span>
|
||||
</div>
|
||||
)}
|
||||
{g.next_event && (
|
||||
<div className="text-xs text-slate-600 mt-0.5 truncate" title={g.next_event.name}>
|
||||
{g.next_event.name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Node mapping table */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-xs text-slate-600 uppercase tracking-wider mb-2">Mapping nœud → clé macro</div>
|
||||
{manualNodes.map(node => {
|
||||
const mk = localKeys[node.id] ?? ''
|
||||
const isSaving = saving === node.id
|
||||
const isSaved = saved[node.id]
|
||||
|
||||
return (
|
||||
<div key={node.id}
|
||||
className={clsx('flex items-center gap-3 px-3 py-2 rounded-lg border transition-colors',
|
||||
mk ? 'border-violet-700/30 bg-violet-900/10' : 'border-slate-700/20 bg-dark-800/30')}>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm text-white truncate">{node.label}</div>
|
||||
<div className="text-xs text-slate-600">{node.unit} · ×{node.coefficient_to_pips}</div>
|
||||
</div>
|
||||
|
||||
<select
|
||||
value={mk}
|
||||
onChange={e => {
|
||||
const v = e.target.value
|
||||
setLocalKeys(prev => ({ ...prev, [node.id]: v }))
|
||||
saveMacroKey(node.id, v)
|
||||
}}
|
||||
disabled={isSaving}
|
||||
className="text-xs bg-dark-900 border border-slate-700/40 rounded px-2 py-1.5 text-white w-44 shrink-0 focus:outline-none focus:border-violet-500/50">
|
||||
{MACRO_KEY_OPTIONS.map(o => (
|
||||
<option key={o.value} value={o.value}>{o.label}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<div className="w-5 shrink-0 text-center">
|
||||
{isSaving && <span className="text-xs text-blue-400 animate-pulse">…</span>}
|
||||
{isSaved && <Check className="w-3.5 h-3.5 text-emerald-400"/>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{linkedCount > 0 && (
|
||||
<div className="rounded-lg border border-slate-700/30 bg-dark-800/40 p-3 text-xs text-slate-500">
|
||||
<span className="text-white font-medium">{linkedCount} nœud{linkedCount > 1 ? 's' : ''} macro-lié{linkedCount > 1 ? 's' : ''}</span>
|
||||
{' '}— la simulation Timeline utilisera des valeurs time-varying pour ces nœuds,
|
||||
interpolant entre les publications passées et les forecasts des prochains events.
|
||||
Le niveau fondamental ne sera plus statique mais guidé par les données FF Calendar.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Main Page ──────────────────────────────────────────────────────────────────
|
||||
|
||||
type ViewMode = 'dag' | 'table' | 'timeline' | 'calibration'
|
||||
type ViewMode = 'dag' | 'table' | 'timeline' | 'calibration' | 'params'
|
||||
|
||||
export default function InstrumentModels() {
|
||||
const [instrument, setInstrument] = useState('EURUSD')
|
||||
@@ -1997,6 +2165,7 @@ export default function InstrumentModels() {
|
||||
{ key: 'table', Icon: Table2, label: 'Tableau' },
|
||||
{ key: 'timeline', Icon: LineChart, label: 'Timeline' },
|
||||
{ key: 'calibration', Icon: Activity, label: 'Calibration' },
|
||||
{ key: 'params', Icon: Settings2, label: 'Paramètres' },
|
||||
]
|
||||
|
||||
return (
|
||||
@@ -2142,6 +2311,7 @@ export default function InstrumentModels() {
|
||||
{view === 'table' && <TableView nodes={state.nodes} onEdit={setEditNode}/>}
|
||||
{view === 'timeline' && <TimelineView instrument={instrument}/>}
|
||||
{view === 'calibration' && <CalibrationView instrument={instrument} eventDetails={state.event_details || {}}/>}
|
||||
{view === 'params' && <MacroConfigView instrument={instrument} nodes={state.nodes}/>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user