feat: causal lab

This commit is contained in:
OpenSquared
2026-06-28 15:50:51 +02:00
parent 1e44557551
commit 0693d41d91
2 changed files with 84 additions and 68 deletions

View File

@@ -343,8 +343,31 @@ function NewsConfig({
}
const ECO_CURRENCIES = ['USD', 'EUR', 'GBP', 'JPY', 'AUD', 'CAD', 'NZD', 'CHF', 'CNY']
const ECO_DEFAULT_CURRENCIES = ['USD', 'EUR', 'GBP', 'JPY']
const ECO_COUNTRIES = [
{ code: 'USD', label: 'États-Unis' },
{ code: 'EUR', label: 'Zone Euro' },
{ code: 'GBP', label: 'Royaume-Uni' },
{ code: 'JPY', label: 'Japon' },
{ code: 'AUD', label: 'Australie' },
{ code: 'CAD', label: 'Canada' },
{ code: 'NZD', label: 'Nouvelle-Zélande' },
{ code: 'CHF', label: 'Suisse' },
{ code: 'CNY', label: 'Chine' },
]
const ECO_DEFAULT_COUNTRIES = ['USD', 'EUR', 'GBP', 'JPY']
const ECO_EVENT_TYPES = [
{ id: 'inflation', label: 'Inflation (CPI/PCE/PPI)' },
{ id: 'emploi', label: 'Emploi (NFP/Claims/ADP)' },
{ id: 'pmi', label: 'PMI (Mfg/Services)' },
{ id: 'pib', label: 'PIB / Croissance' },
{ id: 'taux', label: 'Décisions de taux' },
{ id: 'commerce', label: 'Balance commerciale' },
{ id: 'ventes', label: 'Ventes au détail' },
{ id: 'logement', label: 'Immobilier / Logement' },
{ id: 'confiance', label: 'Confiance / Sentiment' },
]
const ECO_DEFAULT_TYPES = ['inflation', 'emploi', 'pmi', 'pib', 'taux']
function EcoConfig({
config,
@@ -354,62 +377,70 @@ function EcoConfig({
onChange: (c: Record<string, any>) => void
}) {
const set = (k: string, v: any) => onChange({ ...config, [k]: v })
const currencies: string[] = config.currencies ?? ECO_DEFAULT_CURRENCIES
const countries: string[] = config.countries ?? ECO_DEFAULT_COUNTRIES
const eventTypes: string[] = config.event_types ?? ECO_DEFAULT_TYPES
const toggleCurrency = (ccy: string) => {
const next = currencies.includes(ccy)
? currencies.filter(c => c !== ccy)
: [...currencies, ccy]
set('currencies', next)
const toggleCountry = (code: string) => {
const next = countries.includes(code)
? countries.filter(c => c !== code)
: [...countries, code]
set('countries', next)
}
const toggleEventType = (id: string) => {
const next = eventTypes.includes(id)
? eventTypes.filter(t => t !== id)
: [...eventTypes, id]
set('event_types', next)
}
return (
<div className="space-y-4">
{/* Row 1: z-score + days */}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs text-slate-400 block mb-1">Seuil z-score (FRED)</label>
<input
type="number" min={0.5} max={5} step={0.1}
value={config.z_threshold ?? 1.5}
onChange={e => set('z_threshold', parseFloat(e.target.value))}
className="w-full bg-dark-900 border border-slate-700/40 rounded px-2 py-1.5 text-sm text-white"
/>
</div>
<div>
<label className="text-xs text-slate-400 block mb-1">Jours lookback</label>
<input
type="number" min={1} max={30}
value={config.days ?? 7}
onChange={e => set('days', parseInt(e.target.value))}
className="w-full bg-dark-900 border border-slate-700/40 rounded px-2 py-1.5 text-sm text-white"
/>
</div>
</div>
{/* Row 2: currencies */}
{/* Pays / Zones */}
<div>
<label className="text-xs text-slate-400 block mb-1.5">Devises surveillées</label>
<label className="text-xs text-slate-400 block mb-1.5">Pays / Zones économiques</label>
<div className="flex flex-wrap gap-1.5">
{ECO_CURRENCIES.map(ccy => (
{ECO_COUNTRIES.map(c => (
<button
key={ccy}
onClick={() => toggleCurrency(ccy)}
key={c.code}
onClick={() => toggleCountry(c.code)}
title={c.label}
className={clsx(
'px-2.5 py-0.5 rounded text-xs border font-mono transition-colors',
currencies.includes(ccy)
countries.includes(c.code)
? 'bg-emerald-900/40 border-emerald-600/60 text-emerald-300'
: 'bg-dark-800 border-slate-700/40 text-slate-500 hover:border-slate-600',
)}
>
{ccy}
{c.code}
</button>
))}
</div>
</div>
{/* Row 3: impact + lookback_releases */}
<div className="grid grid-cols-2 gap-3">
{/* Types d'événements */}
<div>
<label className="text-xs text-slate-400 block mb-1.5">Types d'événements calendar</label>
<div className="flex flex-wrap gap-1.5">
{ECO_EVENT_TYPES.map(t => (
<button
key={t.id}
onClick={() => toggleEventType(t.id)}
className={clsx(
'px-2.5 py-1 rounded text-xs border transition-colors',
eventTypes.includes(t.id)
? 'bg-blue-900/40 border-blue-600/60 text-blue-300'
: 'bg-dark-800 border-slate-700/40 text-slate-500 hover:border-slate-600',
)}
>
{t.label}
</button>
))}
</div>
</div>
{/* Impact + seuil surprise + releases contexte */}
<div className="grid grid-cols-3 gap-3">
<div>
<label className="text-xs text-slate-400 block mb-1">Impact minimum</label>
<select
@@ -422,6 +453,15 @@ function EcoConfig({
<option value="low">Tous niveaux</option>
</select>
</div>
<div>
<label className="text-xs text-slate-400 block mb-1">Seuil surprise z-score</label>
<input
type="number" min={0.5} max={5} step={0.1}
value={config.z_threshold ?? 1.5}
onChange={e => set('z_threshold', parseFloat(e.target.value))}
className="w-full bg-dark-900 border border-slate-700/40 rounded px-2 py-1.5 text-sm text-white"
/>
</div>
<div>
<label className="text-xs text-slate-400 block mb-1">Releases contexte</label>
<input
@@ -433,7 +473,7 @@ function EcoConfig({
</div>
</div>
{/* Row 4: create_market_event toggle */}
{/* create_market_event toggle */}
<div className="flex items-center gap-3">
<button
onClick={() => set('create_market_event', !(config.create_market_event ?? true))}
@@ -737,13 +777,13 @@ function DeskEditor({
/>
</div>
{/* Instruments / Gauges */}
{/* Instruments / Gauges — not shown for eco (link is through causal graph) */}
{d.type === 'sentiment' ? (
<MacroGaugeSelector
selected={d.instruments}
onChange={v => set('instruments', v)}
/>
) : (
) : d.type !== 'eco' && (
<InstrumentSelector
selected={d.instruments}
onChange={v => set('instruments', v)}

View File

@@ -36,7 +36,7 @@ interface ActionResult {
const SOURCE_LABELS: Record<string, string> = {
news: 'News',
eco: 'Éco. FRED',
eco: 'Éco. Calendar',
technical: 'Technique',
reports: 'Reports',
}
@@ -236,7 +236,7 @@ function CheckEventsPanel() {
{/* Fenêtre de scan */}
<div>
<div className="text-slate-400 mb-2 text-xs uppercase tracking-wide">Fenêtre de scan (news + éco)</div>
<div className="text-slate-400 mb-2 text-xs uppercase tracking-wide">Fenêtre de scan</div>
<div className="flex flex-wrap gap-1.5 mb-2">
{SCAN_PRESETS.map(p => {
const { date_from, date_to } = presetDates(p.hours)
@@ -284,30 +284,6 @@ function CheckEventsPanel() {
</div>
</div>
{/* Advanced */}
<div className="grid grid-cols-3 gap-3">
<label className="flex flex-col gap-1">
<span className="text-xs text-slate-400">News impact min</span>
<input type="number" step="0.05" min="0" max="1"
value={config.news_impact_min}
onChange={e => setConfig(c => ({ ...c, news_impact_min: +e.target.value }))}
className="bg-dark-900 border border-slate-700 rounded px-2 py-1 text-slate-200 text-sm w-full" />
</label>
<label className="flex flex-col gap-1">
<span className="text-xs text-slate-400">Éco z-score min</span>
<input type="number" step="0.25" min="0.5" max="5"
value={config.eco_z_threshold}
onChange={e => setConfig(c => ({ ...c, eco_z_threshold: +e.target.value }))}
className="bg-dark-900 border border-slate-700 rounded px-2 py-1 text-slate-200 text-sm w-full" />
</label>
<label className="flex flex-col gap-1">
<span className="text-xs text-slate-400">Report importance min</span>
<input type="number" step="1" min="1" max="5"
value={config.report_min_importance}
onChange={e => setConfig(c => ({ ...c, report_min_importance: +e.target.value }))}
className="bg-dark-900 border border-slate-700 rounded px-2 py-1 text-slate-200 text-sm w-full" />
</label>
</div>
</div>
)}