feat: translate all UI strings to English for international release

Complete French→English translation across all frontend pages and backend
services — every label, button, header, empty state, toast, and nav item
is now in English. Build verified clean (tsc + vite). No i18n library
added; direct string replacement throughout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-22 09:06:37 +02:00
parent f8a0a6d023
commit dcbc9f19fc
24 changed files with 1443 additions and 1444 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -49,7 +49,7 @@ def _max_similarity_vs_existing(candidate_kws: List[str], existing: List[Dict])
return max((_jaccard(candidate_kws, p.get("keywords") or []) for p in existing), default=0.0)
_EMBED_SIM_THRESHOLD = 0.75 # seuil cosinus pour considérer deux patterns comme doublons
_EMBED_SIM_THRESHOLD = 0.75 # cosine threshold to consider two patterns as duplicates
def _is_duplicate_pattern(
@@ -59,8 +59,8 @@ def _is_duplicate_pattern(
jaccard_threshold: float = 0.30,
) -> bool:
"""
Retourne True si le candidat est trop similaire à un pattern existant.
Essaie les embeddings cosinus (Sprint 4.3) avec fallback Jaccard.
Returns True if the candidate is too similar to an existing pattern.
Tries cosine embeddings (Sprint 4.3) with Jaccard fallback.
"""
if not existing:
return False
@@ -77,7 +77,7 @@ def _is_duplicate_pattern(
api_key=api_key,
candidate_id=candidate.get("id"),
)
if sim > 0: # embedding a fonctionné
if sim > 0: # embedding worked
return sim >= _EMBED_SIM_THRESHOLD
except Exception as _emb_err:
logger.debug(f"[Cycle] Embedding failed, fallback Jaccard: {_emb_err}")
@@ -104,13 +104,13 @@ def _run_portfolio_monitor(risk: dict, cycle_id: str) -> dict:
n_alerts = len(risk.get("alerts", []))
system_prompt = (
"Tu es un risk manager spécialisé dans les portefeuilles d'options géopolitiques. "
"Analyse l'état du portefeuille simulé ci-dessous et génère des recommandations concrètes. "
"Réponds en JSON avec ce schéma exact: "
'{"assessment": "<2 phrases max sur l\'état global>", '
"You are a risk manager specializing in geopolitical options portfolios. "
"Analyze the state of the simulated portfolio below and generate concrete recommendations. "
"Respond in JSON with this exact schema: "
'{"assessment": "<max 2 sentences on the overall state>", '
'"actions": [{"priority": "high|medium", "type": "close_trade|rebalance|monitor", '
'"trade_id": <int ou null>, "underlying": "<ticker>", "reason": "<raison courte>"}], '
'"rebalance_suggestion": "<suggestion concrète de rééquilibrage en 1 phrase>"}'
'"trade_id": <int or null>, "underlying": "<ticker>", "reason": "<short reason>"}], '
'"rebalance_suggestion": "<concrete rebalancing suggestion in 1 sentence>"}'
)
user_prompt = context

View File

@@ -99,13 +99,13 @@ def get_portfolio_concentration(open_trades: List[Dict]) -> Dict[str, int]:
def build_portfolio_context_block(open_trades: List[Dict], concentration: Dict[str, int]) -> str:
"""Build a prompt section describing current portfolio for injection into AI prompts."""
if not open_trades:
return "\n## PORTEFEUILLE ACTUEL\nAucun trade en cours — portefeuille vide.\n"
return "\n## CURRENT PORTFOLIO\nNo open trades — empty portfolio.\n"
total = len(open_trades)
conc_sorted = sorted(concentration.items(), key=lambda x: -x[1])
conc_str = " | ".join(f"{cls.upper()}: {n}" for cls, n in conc_sorted)
lines: List[str] = [f"Total: {total} trade(s) ouvert(s) | Concentration: {conc_str}", ""]
lines: List[str] = [f"Total: {total} open trade(s) | Concentration: {conc_str}", ""]
for t in open_trades:
sym = t["underlying"]
@@ -118,27 +118,27 @@ def build_portfolio_context_block(open_trades: List[Dict], concentration: Dict[s
pat = t.get("pattern_name", "")
entry = t.get("entry_price")
cur = t.get("current_price")
ep_str = f"entrée {entry:.2f}actuel {cur:.2f}" if entry and cur else ""
ep_str = f"entry {entry:.2f}current {cur:.2f}" if entry and cur else ""
line = f"{sym} | {strat} [{cls}] | {held}j tenu / {rem}j restants | J-1: {m1d_str} | J-5: {m5d_str}"
line = f"{sym} | {strat} [{cls}] | {held}d held / {rem}d remaining | D-1: {m1d_str} | D-5: {m5d_str}"
if ep_str:
line += f" | {ep_str}"
if pat:
line += f" | thèse: «{pat}»"
line += f" | thesis: «{pat}»"
lines.append(line)
# Identify overweight classes
overweight = [cls for cls, n in conc_sorted if n >= 3]
ow_str = ", ".join(overweight) if overweight else "aucune"
ow_str = ", ".join(overweight) if overweight else "none"
block = (
"\n## PORTEFEUILLE ACTUEL — POSITIONS OUVERTES\n"
"\n## CURRENT PORTFOLIO — OPEN POSITIONS\n"
+ "\n".join(lines)
+ f"\n\nClasses surpondérées (≥3 trades): {ow_str}\n"
+ "\n⚠️ CONSIGNES IMPÉRATIVES (nongociables):\n"
+ "1. NE PAS suggérer un nouveau trade sur un sous-jacent déjà en portefeuille — doublement interdit.\n"
+ "2. Signaler explicitement dans 'rationale' si une suggestion CONTREDIT une position ouverte (signal de clôture potentiel).\n"
+ "3. Éviter d'alourdir une classe surpondérée (≥3 trades) sauf catalyseur exceptionnel justifié.\n"
+ "4. Un signal opposé à une position ouverte = opportunité de SORTIE à documenter, pas d'entrée inversée.\n"
+ f"\n\nOverweight classes (≥3 trades): {ow_str}\n"
+ "\n⚠️ MANDATORY RULES (non-negotiable):\n"
+ "1. DO NOT suggest a new trade on an underlying already in the portfolio — strictly forbidden.\n"
+ "2. Explicitly flag in 'rationale' if a suggestion CONTRADICTS an open position (potential exit signal).\n"
+ "3. Avoid adding to an overweight class (≥3 trades) unless an exceptional justified catalyst exists.\n"
+ "4. A signal opposing an open position = EXIT opportunity to document, not a reverse entry.\n"
)
return block

View File

@@ -33,25 +33,25 @@ const BUCKET_ICONS: Record<string, string> = {
}
export const CATEGORIES = [
{ key: 'all', label: 'Tous' },
{ key: 'energy', label: '⛽ Énergie' },
{ key: 'metals', label: '🥇 Métaux' },
{ key: 'all', label: 'All' },
{ key: 'energy', label: '⛽ Energy' },
{ key: 'metals', label: '🥇 Metals' },
{ key: 'agriculture', label: '🌾 Agri' },
{ key: 'indices', label: '📊 Indices' },
{ key: 'equities', label: '📈 Actions' },
{ key: 'equities', label: '📈 Equities' },
{ key: 'forex', label: '💱 Forex' },
]
export const THEMATIC_CATEGORIES = [
{ key: 'all', label: 'Toutes' },
{ key: 'géopolitique', label: '⚔️ Géopo' },
{ key: 'all', label: 'All' },
{ key: 'géopolitique', label: '⚔️ Geopo' },
{ key: 'macro_monétaire', label: '🏦 Macro' },
{ key: 'technique', label: '📐 Technique' },
{ key: 'technique', label: '📐 Technical' },
{ key: 'commodités_supply',label: '🛢️ Supply' },
{ key: 'risk_off', label: '🌩️ Risk-off' },
{ key: 'flux_saisonnier', label: '📅 Flux' },
{ key: 'géo_économique', label: '🌐 Géo-éco' },
{ key: 'crédit_stress', label: '💳 Crédit' },
{ key: 'flux_saisonnier', label: '📅 Flows' },
{ key: 'géo_économique', label: '🌐 Geo-eco' },
{ key: 'crédit_stress', label: '💳 Credit' },
]
const SIGNAL_DIR: Record<string, { label: string; color: string }> = {
@@ -80,12 +80,12 @@ export interface TradeItem {
}
const BIAS_DISPLAY: Record<string, { label: string; color: string }> = {
'bullish+': { label: '★★ Compatible', color: '#10b981' },
'bullish+': { label: '★★ Aligned', color: '#10b981' },
'bullish': { label: '★ Compatible', color: '#34d399' },
'neutral': { label: '→ Neutre', color: '#64748b' },
'bearish': { label: '✗ favorable', color: '#f97316' },
'neutral': { label: '→ Neutral', color: '#64748b' },
'bearish': { label: '✗ Unfavorable', color: '#f97316' },
'bearish+': { label: '✗✗ Contra', color: '#ef4444' },
'defensive':{ label: '⚠ Défensif', color: '#f59e0b' },
'defensive':{ label: '⚠ Defensive', color: '#f59e0b' },
}
// ── IBKR helpers ──────────────────────────────────────────────────────────────
@@ -234,33 +234,33 @@ function IBKRTicket({ strategy, underlying, strikeGuidance, expiryDays, entryPri
return (
<div className="bg-blue-950/20 border border-blue-700/30 rounded p-3 space-y-2.5">
<div className="text-[10px] font-bold text-blue-300 uppercase tracking-wide flex items-center gap-1.5">
<Terminal className="w-3 h-3" /> Ticket IBKR
{!entryPrice && <span className="ml-auto text-slate-600 font-normal normal-case">Prix non disponible vérifier dans IBKR</span>}
<Terminal className="w-3 h-3" /> IBKR Ticket
{!entryPrice && <span className="ml-auto text-slate-600 font-normal normal-case">Price unavailable check in IBKR</span>}
</div>
<div className="grid grid-cols-3 gap-3 text-[10px]">
<div>
<div className="text-slate-600 mb-0.5">Sous-jacent</div>
<div className="text-slate-600 mb-0.5">Underlying</div>
<div className="text-white font-mono font-bold text-sm">{underlying}</div>
<div className="text-slate-600 text-[9px]">Security Type: OPT</div>
</div>
<div>
<div className="text-slate-600 mb-0.5">Stratégie</div>
<div className="text-slate-600 mb-0.5">Strategy</div>
<div className="text-slate-200 font-semibold">{strategy}</div>
{entryPrice && <div className="text-slate-600 text-[9px]">Sous-jacent: ${entryPrice.toFixed(2)}</div>}
{entryPrice && <div className="text-slate-600 text-[9px]">Underlying: ${entryPrice.toFixed(2)}</div>}
</div>
<div>
<div className="text-slate-600 mb-0.5">Expiration</div>
<div className="text-slate-600 mb-0.5">Expiry</div>
<div className="text-slate-200 font-mono">
{expiryDate ? format(expiryDate, 'd MMM yyyy', { locale: fr }) : expiryDays ? `~${expiryDays}j` : '—'}
{expiryDate ? format(expiryDate, 'd MMM yyyy', { locale: fr }) : expiryDays ? `~${expiryDays}d` : '—'}
</div>
<div className="text-slate-600 text-[9px]">{expiryDays ? `${expiryDays}j DTE` : ''}</div>
<div className="text-slate-600 text-[9px]">{expiryDays ? `${expiryDays}d DTE` : ''}</div>
</div>
</div>
<div className="space-y-1">
{legs.map((leg, i) => (
<div key={i} className="flex items-center gap-2 bg-slate-900/70 rounded px-2.5 py-1.5 font-mono text-[10px]">
<span className={clsx('w-8 font-bold', leg.action === 'BUY' ? 'text-emerald-400' : 'text-red-400')}>{leg.action}</span>
<span className="text-slate-400">1 contrat</span>
<span className="text-slate-400">1 contract</span>
<span className={clsx('font-bold w-10 text-center', leg.type === 'CALL' ? 'text-blue-300' : leg.type === 'PUT' ? 'text-orange-300' : 'text-slate-300')}>{leg.type}</span>
<span className="text-slate-600">Strike</span>
<span className="text-yellow-300 font-bold">{fmtStrike(leg.strike)}</span>
@@ -270,17 +270,17 @@ function IBKRTicket({ strategy, underlying, strikeGuidance, expiryDays, entryPri
</div>
<div className="grid grid-cols-3 gap-3 text-[10px]">
<div>
<div className="text-slate-600 mb-0.5">Type d'ordre</div>
<div className="text-slate-600 mb-0.5">Order type</div>
<div className="text-slate-300 font-semibold">LIMIT</div>
<div className="text-slate-600 text-[9px]">Prix = prime dans IBKR</div>
<div className="text-slate-600 text-[9px]">Price = premium in IBKR</div>
</div>
<div>
<div className="text-slate-600 mb-0.5">Budget max</div>
<div className="text-slate-300 font-mono">{maxLoss ? `${Math.abs(maxLoss).toFixed(0)}€` : '~1 000'}</div>
<div className="text-slate-600 text-[9px]">Perte max estimée</div>
<div className="text-slate-600 mb-0.5">Max budget</div>
<div className="text-slate-300 font-mono">{maxLoss ? `${Math.abs(maxLoss).toFixed(0)}` : '~1,000€'}</div>
<div className="text-slate-600 text-[9px]">Estimated max loss</div>
</div>
<div>
<div className="text-slate-600 mb-0.5">Cible</div>
<div className="text-slate-600 mb-0.5">Target</div>
<div className="text-emerald-400 font-mono">{targetGain ? `+${targetGain.toFixed(0)}` : '—'}</div>
</div>
</div>
@@ -373,7 +373,7 @@ export function TradeCard({ item, onAdd, macroInfo, addedInfo, profiles }: {
)}
</div>
) : (
<span className="ml-2 shrink-0 text-xs text-slate-500 bg-dark-600 border border-slate-700/40 rounded px-1.5 py-0.5 whitespace-nowrap">à scorer</span>
<span className="ml-2 shrink-0 text-xs text-slate-500 bg-dark-600 border border-slate-700/40 rounded px-1.5 py-0.5 whitespace-nowrap">to score</span>
)}
</div>
{convergenceCount > 0 && (
@@ -404,7 +404,7 @@ export function TradeCard({ item, onAdd, macroInfo, addedInfo, profiles }: {
<><span className="text-slate-700">·</span>
{matchedProfile
? <span className="font-semibold" style={{ color: matchedProfile.color }}> {matchedProfile.name}</span>
: <span className="text-red-400/80">✗ {gainPct === 0 ? 'Gain non défini' : 'Aucun profil'}</span>
: <span className="text-red-400/80"> {gainPct === 0 ? 'Gain undefined' : 'No profile'}</span>
}</>
)}
</div>
@@ -435,7 +435,7 @@ export function TradeCard({ item, onAdd, macroInfo, addedInfo, profiles }: {
<button onClick={() => setExpanded(!expanded)}
className="flex items-center gap-1 text-xs text-slate-500 hover:text-slate-300 mb-1.5 transition-colors">
{expanded ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
{expanded ? 'Masquer la justification' : ' Justification du score'}
{expanded ? 'Hide breakdown' : '↳ Score breakdown'}
</button>
{expanded && (
<div className="mb-2">
@@ -463,7 +463,7 @@ export function TradeCard({ item, onAdd, macroInfo, addedInfo, profiles }: {
{addedInfo && (
<div className="flex items-center gap-1.5 mb-2 text-[10px] text-emerald-400 bg-emerald-900/20 border border-emerald-700/30 rounded px-2 py-1">
<CheckCircle2 className="w-3 h-3 shrink-0" />
<span>Ajouté le {format(new Date(addedInfo.entry_date), "d MMM yyyy", { locale: fr })}</span>
<span>Added on {format(new Date(addedInfo.entry_date), "d MMM yyyy", { locale: fr })}</span>
</div>
)}
<button onClick={() => onAdd(item)}
@@ -473,7 +473,7 @@ export function TradeCard({ item, onAdd, macroInfo, addedInfo, profiles }: {
: 'text-blue-400 hover:text-white hover:bg-blue-600 border-blue-500/30 hover:border-blue-500'
)}>
<Plus className="w-3 h-3" />
{addedInfo ? 'Ajouter à nouveau' : 'Ajouter au portefeuille'}
{addedInfo ? 'Add again' : 'Add to portfolio'}
</button>
</div>
)
@@ -560,7 +560,7 @@ export function TradeRow({ item, onAdd, macroInfo, addedInfo, profiles, rank }:
)}
</div>
</div>
) : <span className="text-slate-600 text-[10px]">à scorer</span>}
) : <span className="text-slate-600 text-[10px]">to score</span>}
</td>
<td className="px-2 py-2 w-20">
{effectiveConviction !== null && (
@@ -646,7 +646,7 @@ export function TradeRow({ item, onAdd, macroInfo, addedInfo, profiles, rank }:
{addedInfo && (
<div className="flex items-center gap-1.5 text-[10px] text-emerald-400 bg-emerald-900/20 border border-emerald-700/30 rounded px-2 py-1">
<CheckCircle2 className="w-3 h-3 shrink-0" />
Ajouté le {format(new Date(addedInfo.entry_date), "d MMM yyyy", { locale: fr })}
Added on {format(new Date(addedInfo.entry_date), "d MMM yyyy", { locale: fr })}
</div>
)}
<button
@@ -657,7 +657,7 @@ export function TradeRow({ item, onAdd, macroInfo, addedInfo, profiles, rank }:
: 'text-blue-400 hover:text-white hover:bg-blue-600 border-blue-500/30 hover:border-blue-500'
)}>
<Plus className="w-3 h-3" />
{addedInfo ? 'Ajouter à nouveau' : 'Ajouter au portefeuille'}
{addedInfo ? 'Add again' : 'Add to portfolio'}
</button>
</div>
</div>
@@ -826,7 +826,7 @@ export function TradeIdeasTab() {
}, {
onSuccess: () => {
refetchPositions()
setToast({ title: 'Ajouté au portefeuille', sub: `${t.strategy ?? ''} ${t.underlying ?? ''} · ${item.patternName}`.trim() })
setToast({ title: 'Added to portfolio', sub: `${t.strategy ?? ''} ${t.underlying ?? ''} · ${item.patternName}`.trim() })
},
})
}
@@ -844,13 +844,13 @@ export function TradeIdeasTab() {
<div className="flex items-center justify-between gap-3 flex-wrap">
<div className="flex items-center gap-2 flex-wrap">
<h2 className="section-title flex items-center gap-1 mb-0">
<Target className="w-3 h-3" /> Idées de trade
<Target className="w-3 h-3" /> Trade Ideas
</h2>
{scoredAt && (
<span className="text-xs text-slate-600">— scoré le {format(new Date(scoredAt), "d MMM à HH:mm", { locale: fr })}</span>
<span className="text-xs text-slate-600"> scored on {format(new Date(scoredAt), "d MMM HH:mm", { locale: fr })}</span>
)}
{topScored.length > 0 && (
<span className="text-xs text-emerald-500">{topScored.length} scoré{topScored.length > 1 ? 's' : ''} · {allUnscored.length} à scorer</span>
<span className="text-xs text-emerald-500">{topScored.length} scored · {allUnscored.length} to score</span>
)}
</div>
<div className="flex items-center gap-2 flex-wrap">
@@ -886,7 +886,7 @@ export function TradeIdeasTab() {
'bg-slate-600 text-white': topN === n,
'text-slate-400 hover:text-slate-200': topN !== n,
})}>
{n === 0 ? 'Tous' : `Top ${n}`}
{n === 0 ? 'All' : `Top ${n}`}
</button>
))}
</div>
@@ -904,10 +904,10 @@ export function TradeIdeasTab() {
<button onClick={() => scorePatterns({})}
disabled={scoring}
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white px-3 py-1.5 rounded text-xs font-semibold transition-all">
{scoring ? <><RefreshCw className="w-3 h-3 animate-spin" /> Scoring GPT-4o...</> : <><Brain className="w-3 h-3" /> Scorer les patterns</>}
{scoring ? <><RefreshCw className="w-3 h-3 animate-spin" /> Scoring GPT-4o...</> : <><Brain className="w-3 h-3" /> Score patterns</>}
</button>
) : (
<span className="text-xs text-slate-600 border border-slate-700/30 rounded px-2 py-1">Clé OpenAI requise</span>
<span className="text-xs text-slate-600 border border-slate-700/30 rounded px-2 py-1">OpenAI key required</span>
)}
</div>
</div>
@@ -927,16 +927,16 @@ export function TradeIdeasTab() {
<tr className="border-b border-slate-700/60 text-slate-600 text-[10px] uppercase tracking-wide">
<th className="pl-3 py-2 text-left w-6">#</th>
<th className="px-2 py-2 text-left">Pattern / Ticker</th>
<th className="px-2 py-2 text-left">Stratégie</th>
<th className="px-2 py-2 text-left">Strategy</th>
<th className="px-2 py-2 text-right">Score</th>
<th className="px-2 py-2 w-20"></th>
<th className="px-2 py-2 text-right">EV</th>
<th className="px-2 py-2 text-right">Gain</th>
<th className="px-2 py-2 text-right">Max/Cible</th>
<th className="px-2 py-2">Profil</th>
<th className="px-2 py-2 text-right">Move</th>
<th className="px-2 py-2 text-right">Max/Target</th>
<th className="px-2 py-2">Profile</th>
<th className="px-2 py-2">Macro</th>
<th className="px-2 py-2 text-right">Entrée</th>
<th className="px-2 py-2 text-right">Durée</th>
<th className="px-2 py-2 text-right">Entry</th>
<th className="px-2 py-2 text-right">Duration</th>
<th className="pr-3 py-2 w-6"></th>
</tr>
</thead>
@@ -956,7 +956,7 @@ export function TradeIdeasTab() {
{topScored.length > 0 && (
<div className="flex items-center gap-2 text-xs text-slate-600 mb-3">
<span className="border-b border-slate-700/30 flex-1"></span>
{allUnscored.length} trade{allUnscored.length > 1 ? 's' : ''} à scorer
{allUnscored.length} trade{allUnscored.length > 1 ? 's' : ''} to score
<span className="border-b border-slate-700/30 flex-1"></span>
</div>
)}
@@ -982,7 +982,7 @@ export function TradeIdeasTab() {
{allPatterns.length === 0 && (
<div className="card text-center py-8 text-slate-500 text-sm">
Démarrer le backend patterns en cours de chargement
Start the backend patterns loading
</div>
)}

View File

@@ -8,23 +8,23 @@ import clsx from 'clsx'
const nav = [
{ to: '/', icon: LayoutDashboard, label: 'Cockpit' },
{ to: '/geo', icon: Globe, label: 'Radar Géopolitique' },
{ to: '/markets', icon: BarChart2, label: 'Marchés & Prix' },
{ to: '/macro', icon: Activity, label: 'Régime Macro' },
{ to: '/geo', icon: Globe, label: 'Geopolitical Radar' },
{ to: '/markets', icon: BarChart2, label: 'Markets & Prices' },
{ to: '/macro', icon: Activity, label: 'Macro Regime' },
{ to: '/options', icon: TrendingUp, label: 'Options Lab' },
{ to: '/patterns', icon: Zap, label: 'Patterns' },
{ to: '/portfolio', icon: DollarSign, label: 'Portefeuille' },
{ to: '/journal', icon: BookOpen, label: 'Journal de Bord' },
{ to: '/rapport', icon: FileBarChart, label: 'Rapport de Cycle' },
{ to: '/super-contexte', icon: Brain, label: 'Super Contexte' },
{ to: '/portfolio', icon: DollarSign, label: 'Portfolio' },
{ to: '/journal', icon: BookOpen, label: 'Trading Journal' },
{ to: '/rapport', icon: FileBarChart, label: 'Cycle Report' },
{ to: '/super-contexte', icon: Brain, label: 'Super Context' },
{ to: '/analytics', icon: FlaskConical, label: 'Analytics' },
{ to: '/analytics-advanced', icon: Microscope, label: 'Analytics Avancées' },
{ to: '/analytics-advanced', icon: Microscope, label: 'Advanced Analytics' },
{ to: '/risk', icon: ShieldAlert, label: 'Risk Dashboard' },
{ to: '/var', icon: Gauge, label: 'VaR Analyse' },
{ to: '/position-history', icon: GitCompare, label: 'Historique Positions' },
{ to: '/var', icon: Gauge, label: 'VaR Analysis' },
{ to: '/position-history', icon: GitCompare, label: 'Position History' },
{ to: '/backtest', icon: History, label: 'Backtest' },
{ to: '/calendar', icon: Calendar, label: 'Calendrier' },
{ to: '/logs', icon: ScrollText, label: 'Logs Système' },
{ to: '/calendar', icon: Calendar, label: 'Calendar' },
{ to: '/logs', icon: ScrollText, label: 'System Logs' },
{ to: '/config', icon: Settings, label: 'Configuration' },
]
@@ -56,7 +56,7 @@ export default function Sidebar() {
{/* Geo risk indicator */}
{riskScore && (
<div className={clsx('mx-3 mt-3 px-3 py-2 rounded border text-xs', riskColors[riskScore.level])}>
<div className="font-semibold uppercase tracking-wider">Risque Géo</div>
<div className="font-semibold uppercase tracking-wider">Geo Risk</div>
<div className="text-lg font-bold">{riskScore.score}/100</div>
<div className="capitalize">{riskScore.level}</div>
</div>
@@ -65,7 +65,7 @@ export default function Sidebar() {
{/* Portfolio mini summary */}
{summary && (summary.open_positions > 0 || summary.unrealized_pnl !== 0) && (
<div className="mx-3 mt-2 px-3 py-2 rounded border border-slate-700/30 bg-dark-700/50 text-xs">
<div className="text-slate-500 uppercase tracking-wider text-xs mb-1">Portefeuille</div>
<div className="text-slate-500 uppercase tracking-wider text-xs mb-1">Portfolio</div>
<div className="flex justify-between">
<span className="text-slate-400">{summary.open_positions} positions</span>
<span className={clsx('font-bold', summary.unrealized_pnl >= 0 ? 'text-emerald-400' : 'text-red-400')}>
@@ -97,7 +97,7 @@ export default function Sidebar() {
'bg-dark-700 text-slate-600': !aiStatus?.enabled,
})}>
<BrainCircuit className="w-3.5 h-3.5" />
<span>{aiStatus?.enabled ? 'IA GPT-4o active' : 'IA non configurée'}</span>
<span>{aiStatus?.enabled ? 'AI GPT-4o active' : 'AI not configured'}</span>
</div>
</div>

View File

@@ -8,8 +8,8 @@ function ReliabilityTable({ data }: { data: any[] }) {
return (
<div className="card text-center py-10 text-slate-500">
<BarChart2 className="w-8 h-8 mx-auto mb-2 opacity-20" />
<div>Aucune donnée de fiabilité</div>
<div className="text-xs mt-1">Les données apparaissent après 3+ trades matures (35% de l'horizon écoulé) par pattern</div>
<div>No reliability data</div>
<div className="text-xs mt-1">Data appears after 3+ mature trades (35% of horizon elapsed) per pattern</div>
</div>
)
}
@@ -22,10 +22,10 @@ function ReliabilityTable({ data }: { data: any[] }) {
<th className="text-left py-2 pr-3 font-medium">Pattern</th>
<th className="text-right py-2 px-2 font-medium">Trades</th>
<th className="text-right py-2 px-2 font-medium">Win Rate</th>
<th className="text-right py-2 px-2 font-medium">PnL moyen</th>
<th className="text-right py-2 px-2 font-medium">Avg PnL</th>
<th className="text-right py-2 px-2 font-medium">Max gain</th>
<th className="text-right py-2 px-2 font-medium">Max perte</th>
<th className="text-right py-2 px-2 font-medium">Score fiabilité</th>
<th className="text-right py-2 px-2 font-medium">Max loss</th>
<th className="text-right py-2 px-2 font-medium">Reliability score</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/50">
@@ -69,8 +69,8 @@ function CalibrationSection({ data }: { data: any }) {
return (
<div className="card text-center py-10 text-slate-500">
<Target className="w-8 h-8 mx-auto mb-2 opacity-20" />
<div>Pas encore de données de calibration</div>
<div className="text-xs mt-1">Nécessite des trades matures avec probabilité stockée vs résultat réalisé</div>
<div>No calibration data yet</div>
<div className="text-xs mt-1">Requires mature trades with stored probability vs realized outcome</div>
</div>
)
}
@@ -84,31 +84,31 @@ function CalibrationSection({ data }: { data: any }) {
<div className="card text-center">
<div className={clsx('text-2xl font-bold font-mono', brierColor)}>{brier_score?.toFixed(3) ?? '—'}</div>
<div className="text-xs text-slate-500 mt-1">Brier Score</div>
<div className="text-xs text-slate-600">(0 = parfait, 1 = nul)</div>
<div className="text-xs text-slate-600">(0 = perfect, 1 = null)</div>
</div>
<div className="card text-center">
<div className="text-2xl font-bold text-white">{sample_size}</div>
<div className="text-xs text-slate-500 mt-1">Trades analysés</div>
<div className="text-xs text-slate-500 mt-1">Trades analyzed</div>
</div>
<div className="card text-center">
<div className={clsx('text-sm font-bold mt-1', brierColor)}>{interpretation ?? '—'}</div>
<div className="text-xs text-slate-500 mt-1">Interprétation</div>
<div className="text-xs text-slate-500 mt-1">Interpretation</div>
</div>
</div>
{/* Calibration buckets */}
{buckets && buckets.length > 0 && (
<div>
<div className="text-xs text-slate-500 mb-2 font-medium">Calibration par décile (prédit vs réalisé)</div>
<div className="text-xs text-slate-500 mb-2 font-medium">Calibration by decile (predicted vs realized)</div>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="text-slate-500 border-b border-slate-700/30">
<th className="text-left py-1.5 pr-3 font-medium">Prob. prédite</th>
<th className="text-right py-1.5 px-2 font-medium">Taux réel</th>
<th className="text-right py-1.5 px-2 font-medium">Biais</th>
<th className="text-left py-1.5 pr-3 font-medium">Predicted prob.</th>
<th className="text-right py-1.5 px-2 font-medium">Actual rate</th>
<th className="text-right py-1.5 px-2 font-medium">Bias</th>
<th className="text-right py-1.5 px-2 font-medium">Trades</th>
<th className="py-1.5 pl-3 font-medium">Barre</th>
<th className="py-1.5 pl-3 font-medium">Bar</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/50">
@@ -142,7 +142,7 @@ function CalibrationSection({ data }: { data: any }) {
</table>
</div>
<div className="text-[10px] text-slate-600 mt-2">
Barre grise = prob. prédite · Barre colorée = taux réalisé · Vert si réel ≥ prédit, rouge sinon
Grey bar = predicted prob. · Colored bar = realized rate · Green if actual predicted, red otherwise
</div>
</div>
)}
@@ -166,7 +166,7 @@ export default function Analytics() {
<BarChart2 className="w-5 h-5 text-blue-400" /> Analytics & Calibration
</h1>
<p className="text-xs text-slate-500 mt-0.5">
Fiabilité historique des patterns · Calibration probabiliste · Brier score
Historical pattern reliability · Probabilistic calibration · Brier score
</p>
</div>
@@ -175,18 +175,18 @@ export default function Analytics() {
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
<div className="card">
<div className="text-2xl font-bold text-white font-mono">{reliability.length}</div>
<div className="text-xs text-slate-500 mt-1">Patterns avec historique</div>
<div className="text-xs text-slate-500 mt-1">Patterns with history</div>
</div>
<div className="card">
<div className="text-2xl font-bold text-white font-mono">
{reliability.reduce((s: number, r: any) => s + r.trade_count, 0)}
</div>
<div className="text-xs text-slate-500 mt-1">Trades matures analysés</div>
<div className="text-xs text-slate-500 mt-1">Mature trades analyzed</div>
</div>
{topPatterns[0] && (
<div className="card border-emerald-700/30">
<div className="text-xs text-slate-500 mb-1 flex items-center gap-1">
<TrendingUp className="w-3 h-3 text-emerald-400" /> Meilleur pattern
<TrendingUp className="w-3 h-3 text-emerald-400" /> Best pattern
</div>
<div className="text-sm font-semibold text-emerald-400 truncate">{topPatterns[0].pattern_name}</div>
<div className="text-xs font-mono text-slate-400">{topPatterns[0].win_rate_pct}% WR</div>
@@ -195,7 +195,7 @@ export default function Analytics() {
{bottomPatterns[0] && (
<div className="card border-red-700/30">
<div className="text-xs text-slate-500 mb-1 flex items-center gap-1">
<AlertTriangle className="w-3 h-3 text-red-400" /> À éviter
<AlertTriangle className="w-3 h-3 text-red-400" /> To avoid
</div>
<div className="text-sm font-semibold text-red-400 truncate">{bottomPatterns[0].pattern_name}</div>
<div className="text-xs font-mono text-slate-400">{bottomPatterns[0].win_rate_pct}% WR</div>
@@ -207,8 +207,8 @@ export default function Analytics() {
{/* Reliability table */}
<div className="card">
<div className="text-sm font-semibold text-white mb-3 flex items-center gap-2">
<BarChart2 className="w-4 h-4 text-blue-400" /> Fiabilité par pattern
<span className="text-xs text-slate-500 font-normal">(trades ≥35% de l'horizon uniquement)</span>
<BarChart2 className="w-4 h-4 text-blue-400" /> Reliability by pattern
<span className="text-xs text-slate-500 font-normal">(trades 35% of horizon only)</span>
</div>
{loadingR ? (
<div className="space-y-2">{[1,2,3].map(i => <div key={i} className="h-8 bg-dark-700 animate-pulse rounded" />)}</div>
@@ -221,17 +221,17 @@ export default function Analytics() {
<div className="card">
<div className="flex items-center justify-between mb-3">
<div className="text-sm font-semibold text-white flex items-center gap-2">
<Target className="w-4 h-4 text-blue-400" /> Calibration probabiliste
<Target className="w-4 h-4 text-blue-400" /> Probabilistic calibration
</div>
<select
value={calDays}
onChange={e => setCalDays(Number(e.target.value))}
className="bg-dark-700 border border-slate-700 rounded px-2 py-1 text-xs text-slate-300"
>
<option value={90}>90 jours</option>
<option value={180}>180 jours</option>
<option value={365}>1 an</option>
<option value={730}>2 ans</option>
<option value={90}>90 days</option>
<option value={180}>180 days</option>
<option value={365}>1 year</option>
<option value={730}>2 years</option>
</select>
</div>
{loadingC ? (

View File

@@ -11,8 +11,8 @@ function BayesianTable({ data }: { data: any[] }) {
return (
<div className="text-center py-10 text-slate-500">
<Brain className="w-8 h-8 mx-auto mb-2 opacity-20" />
<div>Pas encore de données bayésiennes</div>
<div className="text-xs mt-1">Les posteriors se calculent après des trades matures (35% de l'horizon)</div>
<div>No Bayesian data yet</div>
<div className="text-xs mt-1">Posteriors are computed after mature trades (35% of the horizon)</div>
</div>
)
}
@@ -29,11 +29,11 @@ function BayesianTable({ data }: { data: any[] }) {
<tr className="text-slate-500 border-b border-slate-700/30">
<th className="text-left py-2 pr-3 font-medium">Pattern</th>
<th className="text-right py-2 px-2 font-medium">Prior GPT</th>
<th className="text-right py-2 px-2 font-medium">WR Bayésien</th>
<th className="text-right py-2 px-2 font-medium">IC 95%</th>
<th className="text-right py-2 px-2 font-medium">Trades matures</th>
<th className="text-right py-2 px-2 font-medium">Dérive</th>
<th className="text-center py-2 px-2 font-medium">Confiance</th>
<th className="text-right py-2 px-2 font-medium">Bayesian WR</th>
<th className="text-right py-2 px-2 font-medium">CI 95%</th>
<th className="text-right py-2 px-2 font-medium">Mature trades</th>
<th className="text-right py-2 px-2 font-medium">Drift</th>
<th className="text-center py-2 px-2 font-medium">Confidence</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/50">
@@ -78,7 +78,7 @@ function BayesianTable({ data }: { data: any[] }) {
)}
{withoutData.length > 0 && (
<div className="text-xs text-slate-600 mt-1">
{withoutData.length} pattern(s) sans trades matures — prior GPT uniquement
{withoutData.length} pattern(s) without mature trades GPT prior only
</div>
)}
</div>
@@ -92,8 +92,8 @@ function TransitionMatrix({ data }: { data: any }) {
return (
<div className="text-center py-8 text-slate-500">
<GitBranch className="w-8 h-8 mx-auto mb-2 opacity-20" />
<div>Pas encore de transitions détectées</div>
<div className="text-xs mt-1">Les clusters se remplissent au fil des cycles</div>
<div>No transitions detected yet</div>
<div className="text-xs mt-1">Clusters fill up as cycles run</div>
</div>
)
}
@@ -104,13 +104,13 @@ function TransitionMatrix({ data }: { data: any }) {
return (
<div className="overflow-x-auto">
<div className="text-xs text-slate-500 mb-2">
Probabilité de transition d'un régime à l'autre (lignes = source, colonnes = destination)
· {data.n_transitions} transitions totales
Transition probability from one regime to another (rows = source, columns = destination)
· {data.n_transitions} total transitions
</div>
<table className="text-xs border-collapse">
<thead>
<tr>
<th className="text-slate-500 p-2 font-normal text-right pr-3">De ↓ / Vers →</th>
<th className="text-slate-500 p-2 font-normal text-right pr-3">From / To </th>
{ids.map(dst => (
<th key={dst} className="p-2 font-medium text-slate-300 text-center min-w-[90px]">
<div className="text-[10px] text-slate-500">C{dst}</div>
@@ -145,7 +145,7 @@ function TransitionMatrix({ data }: { data: any }) {
</tbody>
</table>
<div className="text-[10px] text-slate-600 mt-2">
Diagonal = reste dans le même cluster · Vert = transition dominante · Bleu = auto-transition
Diagonal = stays in same cluster · Green = dominant transition · Blue = self-transition
</div>
</div>
)
@@ -165,7 +165,7 @@ function ClusterTimeline({ data }: { data: any[] }) {
if (!data || data.length === 0) {
return (
<div className="text-center py-6 text-slate-500 text-xs">
Aucun cluster détecté encore — lancez un cycle pour démarrer
No cluster detected yet run a cycle to get started
</div>
)
}
@@ -187,7 +187,7 @@ function ClusterTimeline({ data }: { data: any[] }) {
{recent.map((r: any, i: number) => (
<div
key={i}
title={`${r.timestamp?.slice(0, 16)} · ${r.cluster_label} · ${r.dominant_regime}${r.anomaly_flag ? ' anomalie' : ''}`}
title={`${r.timestamp?.slice(0, 16)} · ${r.cluster_label} · ${r.dominant_regime}${r.anomaly_flag ? ' ⚠️ anomaly' : ''}`}
className={clsx(
'w-5 h-5 rounded cursor-default',
CLUSTER_COLORS[r.cluster_id] ?? 'bg-slate-600',
@@ -197,20 +197,20 @@ function ClusterTimeline({ data }: { data: any[] }) {
))}
</div>
<div className="text-[10px] text-slate-600">
Chaque carré = 1 snapshot · Blanc cerclé = anomalie détectée · {data.length} entrées au total
Each square = 1 snapshot · White ring = anomaly detected · {data.length} total entries
</div>
{/* Latest cluster detail */}
{data[0] && (
<div className="mt-3 p-3 rounded bg-dark-700/60 border border-slate-700/30 text-xs">
<div className="text-slate-400 mb-1">Cluster actuel</div>
<div className="text-slate-400 mb-1">Current cluster</div>
<div className="flex items-center gap-2">
<span className={clsx('w-3 h-3 rounded-full', CLUSTER_COLORS[data[0].cluster_id])} />
<span className="text-white font-semibold">{data[0].cluster_label}</span>
<span className="text-slate-500">({data[0].dominant_regime})</span>
{data[0].anomaly_flag ? (
<span className="flex items-center gap-1 text-amber-400 font-bold">
<AlertTriangle className="w-3 h-3" /> Anomalie
<AlertTriangle className="w-3 h-3" /> Anomaly
</span>
) : null}
</div>
@@ -228,21 +228,21 @@ function EmbeddingsSummary({ data }: { data: any[] }) {
return (
<div className="text-center py-6 text-slate-500 text-xs">
<Cpu className="w-6 h-6 mx-auto mb-1 opacity-20" />
Aucun embedding disponible — ils se génèrent automatiquement lors du filtrage des patterns
No embeddings available they are generated automatically during pattern filtering
</div>
)
}
return (
<div className="space-y-2">
<div className="text-xs text-slate-500">{data.length} patterns vectorisés</div>
<div className="text-xs text-slate-500">{data.length} vectorized patterns</div>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="text-slate-500 border-b border-slate-700/30">
<th className="text-left py-1.5 pr-3 font-medium">Pattern</th>
<th className="text-left py-1.5 px-2 font-medium">Classe</th>
<th className="text-right py-1.5 px-2 font-medium">Modèle</th>
<th className="text-right py-1.5 px-2 font-medium">Mis à jour</th>
<th className="text-left py-1.5 px-2 font-medium">Class</th>
<th className="text-right py-1.5 px-2 font-medium">Model</th>
<th className="text-right py-1.5 px-2 font-medium">Updated</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/30">
@@ -301,10 +301,10 @@ export default function AnalyticsAdvanced() {
<div className="flex items-start justify-between gap-4">
<div>
<h1 className="text-xl font-bold text-white flex items-center gap-2">
<Brain className="w-5 h-5 text-purple-400" /> Analytics Avancées — Phase 4
<Brain className="w-5 h-5 text-purple-400" /> Advanced Analytics Phase 4
</h1>
<p className="text-xs text-slate-500 mt-0.5">
Bayesian updating · Clustering de régimes · Embeddings sémantiques
Bayesian updating · Regime clustering · Semantic embeddings
</p>
</div>
<div className="flex gap-2">
@@ -322,7 +322,7 @@ export default function AnalyticsAdvanced() {
className="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-blue-600/20 hover:bg-blue-600/30 border border-blue-500/30 text-blue-300 rounded transition-colors disabled:opacity-50"
>
<RefreshCw className={clsx('w-3 h-3', regimeDetect.isPending && 'animate-spin')} />
tecter régime
Detect regime
</button>
</div>
</div>
@@ -333,25 +333,25 @@ export default function AnalyticsAdvanced() {
<div className="text-2xl font-bold text-purple-400 font-mono">
{patternsWithData.length}
</div>
<div className="text-xs text-slate-500 mt-1">Patterns bayésiens actifs</div>
<div className="text-xs text-slate-500 mt-1">Active Bayesian patterns</div>
</div>
<div className="card">
<div className="text-2xl font-bold text-white font-mono">
{avgBayesWR !== null ? `${avgBayesWR}%` : '—'}
</div>
<div className="text-xs text-slate-500 mt-1">Win rate bayésien moyen</div>
<div className="text-xs text-slate-500 mt-1">Average Bayesian win rate</div>
</div>
<div className="card">
<div className={clsx('text-2xl font-bold font-mono', latestCluster ? CLUSTER_COLORS[latestCluster.cluster_id]?.replace('bg-', 'text-') ?? 'text-slate-400' : 'text-slate-500')}>
{latestCluster ? `C${latestCluster.cluster_id}` : '—'}
</div>
<div className="text-xs text-slate-500 mt-1">
{latestCluster?.cluster_label ?? 'Aucun cluster'}
{latestCluster?.cluster_label ?? 'No cluster'}
</div>
</div>
<div className="card">
<div className="text-2xl font-bold text-blue-400 font-mono">{embeddings.length}</div>
<div className="text-xs text-slate-500 mt-1">Patterns vectorisés</div>
<div className="text-xs text-slate-500 mt-1">Vectorized patterns</div>
</div>
</div>
@@ -360,8 +360,8 @@ export default function AnalyticsAdvanced() {
<div className="flex items-center justify-between mb-3">
<div className="text-sm font-semibold text-white flex items-center gap-2">
<Brain className="w-4 h-4 text-purple-400" />
Posteriors bayésiens par pattern
<span className="text-xs text-slate-500 font-normal">Beta(α,β) · IC 95%</span>
Bayesian posteriors by pattern
<span className="text-xs text-slate-500 font-normal">Beta(α,β) · CI 95%</span>
</div>
{bayesUpdate.data && (
<span className="text-xs text-emerald-400">{bayesUpdate.data.message}</span>
@@ -374,9 +374,9 @@ export default function AnalyticsAdvanced() {
)}
<div className="mt-3 p-3 bg-dark-700/40 rounded text-xs text-slate-500 border border-slate-700/20">
<strong className="text-slate-400">Lecture :</strong> Prior GPT = probabilité annoncée par GPT-4o ·
WR Bayésien = win rate observé avec lissage Laplace (β a priori faible) ·
Dérive = écart posterior prior · IC 95% basé sur la distribution Beta
<strong className="text-slate-400">Reading:</strong> GPT Prior = probability stated by GPT-4o ·
Bayesian WR = observed win rate with Laplace smoothing (weak β prior) ·
Drift = posterior prior gap · CI 95% based on Beta distribution
</div>
</div>
@@ -387,16 +387,16 @@ export default function AnalyticsAdvanced() {
<div className="flex items-center justify-between mb-3">
<div className="text-sm font-semibold text-white flex items-center gap-2">
<Layers className="w-4 h-4 text-blue-400" />
Historique des clusters
Cluster history
</div>
<select
value={regimeDays}
onChange={e => setRegimeDays(Number(e.target.value))}
className="bg-dark-700 border border-slate-700 rounded px-2 py-1 text-xs text-slate-300"
>
<option value={30}>30 jours</option>
<option value={90}>90 jours</option>
<option value={180}>180 jours</option>
<option value={30}>30 days</option>
<option value={90}>90 days</option>
<option value={180}>180 days</option>
</select>
</div>
{loadingClusters ? (
@@ -410,8 +410,8 @@ export default function AnalyticsAdvanced() {
<div className="card">
<div className="text-sm font-semibold text-white flex items-center gap-2 mb-3">
<GitBranch className="w-4 h-4 text-blue-400" />
Matrice de transition
<span className="text-xs text-slate-500 font-normal">P(régime j | régime i)</span>
Transition matrix
<span className="text-xs text-slate-500 font-normal">P(regime j | regime i)</span>
</div>
{loadingTrans ? (
<div className="h-32 bg-dark-700 animate-pulse rounded" />
@@ -425,9 +425,9 @@ export default function AnalyticsAdvanced() {
<div className="card">
<div className="text-sm font-semibold text-white flex items-center gap-2 mb-3">
<Cpu className="w-4 h-4 text-emerald-400" />
Embeddings sémantiques
Semantic embeddings
<span className="text-xs text-slate-500 font-normal">
text-embedding-3-small · Similarité cosinus remplace Jaccard
text-embedding-3-small · Cosine similarity replaces Jaccard
</span>
</div>
{loadingEmb ? (
@@ -436,9 +436,9 @@ export default function AnalyticsAdvanced() {
<EmbeddingsSummary data={embeddings} />
)}
<div className="mt-3 p-3 bg-dark-700/40 rounded text-xs text-slate-500 border border-slate-700/20">
<strong className="text-slate-400">Comment ça marche :</strong> Chaque nouveau pattern suggéré est vectori et comparé
aux patterns existants via similarité cosinus (seuil : 0.75). Si la similarité est &gt; 0.75,
le pattern est reje comme doublon sémantique plus précis que Jaccard qui ne voit que les mots communs.
<strong className="text-slate-400">How it works:</strong> Each newly suggested pattern is vectorized and compared
to existing patterns via cosine similarity (threshold: 0.75). If similarity is &gt; 0.75,
the pattern is rejected as a semantic duplicate more precise than Jaccard which only sees common words.
</div>
</div>
</div>

View File

@@ -63,7 +63,7 @@ export default function Backtest() {
<History className="w-5 h-5 text-blue-400" /> Backtest & Simulation
</h1>
<p className="text-xs text-slate-500 mt-0.5">
Testez vos stratégies options sur des données historiques réelles
Test your options strategies on real historical data
</p>
</div>
@@ -74,7 +74,7 @@ export default function Backtest() {
<div className="section-title">Configuration</div>
<div className="space-y-3">
<div>
<label className="text-xs text-slate-500 mb-1 block">Sous-jacent</label>
<label className="text-xs text-slate-500 mb-1 block">Underlying</label>
<div className="flex flex-wrap gap-1 mb-1">
{SYMBOLS.slice(0, 7).map(s => (
<button
@@ -98,7 +98,7 @@ export default function Backtest() {
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Stratégie</label>
<label className="text-xs text-slate-500 mb-1 block">Strategy</label>
{STRATEGIES.map(s => (
<button
key={s.key}
@@ -114,7 +114,7 @@ export default function Backtest() {
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Période</label>
<label className="text-xs text-slate-500 mb-1 block">Period</label>
<input
type="date"
value={form.start_date}
@@ -142,7 +142,7 @@ export default function Backtest() {
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Expiration: {form.expiry_days}j</label>
<label className="text-xs text-slate-500 mb-1 block">Expiration: {form.expiry_days}d</label>
<div className="flex gap-1 mb-1">
{[30, 60, 90, 180].map(d => (
<button
@@ -153,14 +153,14 @@ export default function Backtest() {
'border-slate-700 text-slate-500': form.expiry_days !== d,
})}
>
{d}j
{d}d
</button>
))}
</div>
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Capital initial ()</label>
<label className="text-xs text-slate-500 mb-1 block">Initial capital ()</label>
<input
type="number"
value={form.capital}
@@ -175,7 +175,7 @@ export default function Backtest() {
className="w-full bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white rounded py-2 text-sm font-semibold flex items-center justify-center gap-2 transition-colors"
>
<Play className="w-4 h-4" />
{isPending ? 'Calcul...' : 'Lancer le backtest'}
{isPending ? 'Computing...' : 'Run backtest'}
</button>
</div>
</div>
@@ -196,12 +196,12 @@ export default function Backtest() {
{/* KPIs */}
<div className="grid grid-cols-4 gap-3">
<StatCard
label="Retour total"
label="Total return"
value={`${typed.total_return_pct >= 0 ? '+' : ''}${typed.total_return_pct.toFixed(2)}%`}
positive={typed.total_return_pct >= 0}
/>
<StatCard
label="Taux de succès"
label="Win rate"
value={`${typed.win_rate.toFixed(1)}%`}
sub={`${typed.wins}W / ${typed.losses}L`}
positive={typed.win_rate >= 50}
@@ -221,9 +221,9 @@ export default function Backtest() {
{/* Equity curve */}
<div className="card">
<div className="section-title">Courbe d'équité — {form.symbol} {STRATEGIES.find(s => s.key === form.strategy)?.label}</div>
<div className="section-title">Equity curve {form.symbol} {STRATEGIES.find(s => s.key === form.strategy)?.label}</div>
<div className="text-xs text-slate-500 mb-3">
Capital final: <span className="text-white font-bold">{typed.final_capital.toFixed(2)}€</span>
Final capital: <span className="text-white font-bold">{typed.final_capital.toFixed(2)}</span>
{' '}(initial: {form.capital} · P&L: {typed.total_pnl >= 0 ? '+' : ''}{typed.total_pnl.toFixed(2)})
</div>
<ResponsiveContainer width="100%" height={220}>
@@ -254,17 +254,17 @@ export default function Backtest() {
{/* Last trades */}
<div className="card">
<div className="section-title">Derniers trades exécutés</div>
<div className="section-title">Last executed trades</div>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="text-slate-600 border-b border-slate-700/40">
<th className="text-left pb-2">Entrée</th>
<th className="text-left pb-2">Sortie</th>
<th className="text-right pb-2">Prix entrée</th>
<th className="text-left pb-2">Entry</th>
<th className="text-left pb-2">Exit</th>
<th className="text-right pb-2">Entry price</th>
<th className="text-right pb-2">Strike</th>
<th className="text-right pb-2">Prime</th>
<th className="text-right pb-2">Prix sortie</th>
<th className="text-right pb-2">Premium</th>
<th className="text-right pb-2">Exit price</th>
<th className="text-right pb-2">P&L</th>
<th className="text-right pb-2">Capital</th>
</tr>
@@ -298,8 +298,8 @@ export default function Backtest() {
<div className="card h-80 flex items-center justify-center text-slate-600">
<div className="text-center">
<History className="w-10 h-10 mx-auto mb-3 opacity-20" />
<div className="text-sm">Configurer et lancer un backtest</div>
<div className="text-xs mt-1">Données yfinance historique complet disponible</div>
<div className="text-sm">Configure and run a backtest</div>
<div className="text-xs mt-1">yfinance data full history available</div>
</div>
</div>
)}

View File

@@ -3,7 +3,6 @@ import clsx from 'clsx'
import { Calendar, Clock, Globe, AlertTriangle } from 'lucide-react'
import type { EconomicEvent, AssetClass } from '../types'
import { format, parseISO, isAfter, isBefore, addDays } from 'date-fns'
import { fr } from 'date-fns/locale'
const ASSET_EMOJIS: Record<string, string> = {
energy: '⛽', metals: '🥇', agriculture: '🌾', equities: '📈',
@@ -16,9 +15,9 @@ const COUNTRY_FLAGS: Record<string, string> = {
}
const IMPORTANCE_CONFIG: Record<string, { color: string; label: string; dots: number }> = {
high: { color: 'text-red-400 border-red-700/40', label: 'Majeur', dots: 3 },
medium: { color: 'text-yellow-400 border-yellow-700/40', label: 'Modéré', dots: 2 },
low: { color: 'text-slate-400 border-slate-700/40', label: 'Mineur', dots: 1 },
high: { color: 'text-red-400 border-red-700/40', label: 'Major', dots: 3 },
medium: { color: 'text-yellow-400 border-yellow-700/40', label: 'Moderate', dots: 2 },
low: { color: 'text-slate-400 border-slate-700/40', label: 'Minor', dots: 1 },
}
function EventCard({ ev }: { ev: EconomicEvent }) {
@@ -42,19 +41,19 @@ function EventCard({ ev }: { ev: EconomicEvent }) {
<span className={clsx('text-xs font-bold uppercase tracking-wider', cfg.color.split(' ')[0])}>
{'●'.repeat(cfg.dots)}
</span>
{isToday && <span className="badge badge-yellow text-xs">AUJOURD'HUI</span>}
{isSoon && !isToday && <span className="badge badge-blue text-xs">BIENTÔT</span>}
{isToday && <span className="badge badge-yellow text-xs">TODAY</span>}
{isSoon && !isToday && <span className="badge badge-blue text-xs">SOON</span>}
</div>
<div className="text-sm text-white font-semibold">{ev.title}</div>
<div className="text-xs text-slate-500 mt-0.5">
{format(evDate, "EEEE d MMM yyyy", { locale: fr })} · {ev.country}
{format(evDate, "EEEE, MMM d yyyy")} · {ev.country}
</div>
</div>
<div className="text-right shrink-0">
<div className={clsx('text-xs font-semibold', cfg.color.split(' ')[0])}>{cfg.label}</div>
{ev.previous && <div className="text-xs text-slate-600 mt-0.5">Préc: {ev.previous}</div>}
{ev.forecast && <div className="text-xs text-slate-500">Prév: {ev.forecast}</div>}
{ev.actual && <div className="text-xs text-emerald-400 font-bold">Réel: {ev.actual}</div>}
{ev.previous && <div className="text-xs text-slate-600 mt-0.5">Prev: {ev.previous}</div>}
{ev.forecast && <div className="text-xs text-slate-500">Fcst: {ev.forecast}</div>}
{ev.actual && <div className="text-xs text-emerald-400 font-bold">Actual: {ev.actual}</div>}
</div>
</div>
{ev.asset_impact && ev.asset_impact.length > 0 && (
@@ -84,25 +83,25 @@ export default function CalendarPage() {
<div className="p-6 space-y-5">
<div>
<h1 className="text-xl font-bold text-white flex items-center gap-2">
<Calendar className="w-5 h-5 text-blue-400" /> Calendrier Économique & Géopolitique
<Calendar className="w-5 h-5 text-blue-400" /> Economic & Geopolitical Calendar
</h1>
<p className="text-xs text-slate-500 mt-0.5">
Événements macro, catalyseurs géopolitiques, dates clés
Macro events, geopolitical catalysts, key dates
</p>
</div>
{/* Legend */}
<div className="flex items-center gap-4 text-xs text-slate-500">
<div className="flex items-center gap-1"><span className="text-red-400">●●●</span> Majeur (forte volatilité attendue)</div>
<div className="flex items-center gap-1"><span className="text-yellow-400">●●</span> Modéré</div>
<div className="flex items-center gap-1"><span className="text-slate-400">●</span> Mineur</div>
<div className="flex items-center gap-1"><span className="text-red-400"></span> Major (high volatility expected)</div>
<div className="flex items-center gap-1"><span className="text-yellow-400"></span> Moderate</div>
<div className="flex items-center gap-1"><span className="text-slate-400"></span> Minor</div>
</div>
<div className="grid grid-cols-3 gap-5">
{/* Economic calendar */}
<div className="col-span-2 space-y-3">
<div className="section-title flex items-center gap-1">
<Clock className="w-3 h-3" /> Événements à venir ({upcoming.length})
<Clock className="w-3 h-3" /> Upcoming events ({upcoming.length})
</div>
{isLoading ? (
[1,2,3].map(i => <div key={i} className="card animate-pulse h-20 bg-dark-700"></div>)
@@ -110,14 +109,14 @@ export default function CalendarPage() {
upcoming.map((ev, i) => <EventCard key={i} ev={ev} />)
) : (
<div className="card text-center py-8 text-slate-500 text-sm">
Démarrer le backend pour charger le calendrier
Start the backend to load the calendar
</div>
)}
{past.length > 0 && (
<>
<div className="section-title mt-6 flex items-center gap-1 opacity-60">
Événements passés ({past.length})
Past events ({past.length})
</div>
{past.map((ev, i) => <EventCard key={i} ev={ev} />)}
</>
@@ -128,7 +127,7 @@ export default function CalendarPage() {
<div className="col-span-1 space-y-4">
<div className="card">
<div className="section-title flex items-center gap-1">
<AlertTriangle className="w-3 h-3 text-orange-400" /> Alertes géopolitiques
<AlertTriangle className="w-3 h-3 text-orange-400" /> Geopolitical alerts
</div>
{highImpactNews.length > 0 ? (
<div className="space-y-2">
@@ -145,20 +144,20 @@ export default function CalendarPage() {
))}
</div>
) : (
<div className="text-xs text-slate-600 text-center py-4">Charger les actualités géopolitiques</div>
<div className="text-xs text-slate-600 text-center py-4">Load geopolitical news</div>
)}
</div>
{/* Trade opportunity windows */}
<div className="card">
<div className="section-title">Fenêtres d'opportunité</div>
<div className="section-title">Opportunity windows</div>
<div className="space-y-2 text-xs">
{[
{ window: 'Pré-FOMC (-3j)', strategy: 'Straddle sur SPY', rationale: 'IV monte avant décision' },
{ window: 'Pré-NFP (-2j)', strategy: 'Straddle sur indices', rationale: 'Directional uncertainty' },
{ window: 'Post-OPEC', strategy: 'Bull Call Spread USO', rationale: 'Cut → oil spike probable' },
{ window: 'Pré-USDA Crop', strategy: 'Long Call WEAT', rationale: 'Supply news catalyst' },
{ window: 'Élections US approche', strategy: 'Long VIX Call', rationale: 'Vol expansion garantie' },
{ window: 'Pre-FOMC (-3d)', strategy: 'Straddle on SPY', rationale: 'IV rises ahead of decision' },
{ window: 'Pre-NFP (-2d)', strategy: 'Straddle on indices', rationale: 'Directional uncertainty' },
{ window: 'Post-OPEC', strategy: 'Bull Call Spread USO', rationale: 'Cut → oil spike likely' },
{ window: 'Pre-USDA Crop', strategy: 'Long Call WEAT', rationale: 'Supply news catalyst' },
{ window: 'US Election approaching', strategy: 'Long VIX Call', rationale: 'Vol expansion guaranteed' },
].map((op, i) => (
<div key={i} className="card-sm">
<div className="font-semibold text-blue-400">{op.window}</div>
@@ -172,16 +171,16 @@ export default function CalendarPage() {
{/* Geo-event impact guide */}
<div className="card">
<div className="section-title flex items-center gap-1">
<Globe className="w-3 h-3" /> Guide d'impact
<Globe className="w-3 h-3" /> Impact guide
</div>
<div className="space-y-1.5 text-xs">
{[
{ event: 'Conflit Moyen-Orient', impact: 'Oil +10-20%', cls: 'energy' },
{ event: 'Sanctions Russie', impact: 'Gaz +15-40%', cls: 'energy' },
{ event: 'Tarifs US-Chine', impact: 'Soja -8%', cls: 'agriculture' },
{ event: 'Crise sanitaire', impact: 'Or +7-12%', cls: 'metals' },
{ event: 'Hausses Fed', impact: 'USD +2-4%', cls: 'forex' },
{ event: 'Guerre Ukraine', impact: 'Blé +15-50%', cls: 'agriculture' },
{ event: 'Middle East conflict', impact: 'Oil +10-20%', cls: 'energy' },
{ event: 'Russia sanctions', impact: 'Gas +15-40%', cls: 'energy' },
{ event: 'US-China tariffs', impact: 'Soy -8%', cls: 'agriculture' },
{ event: 'Health crisis', impact: 'Gold +7-12%', cls: 'metals' },
{ event: 'Fed hikes', impact: 'USD +2-4%', cls: 'forex' },
{ event: 'Ukraine war', impact: 'Wheat +15-50%', cls: 'agriculture' },
].map((g, i) => (
<div key={i} className="flex justify-between items-center py-1 border-b border-slate-700/20 last:border-0">
<span className="text-slate-400">{g.event}</span>

View File

@@ -7,39 +7,39 @@ import clsx from 'clsx'
const API = ''
const SOURCE_CATEGORIES = {
'Flux RSS actifs': ['reuters_world', 'reuters_business', 'reuters_energy', 'ap_top', 'aljazeera', 'ft', 'bloomberg'],
'Données économiques': ['newsapi', 'gdelt', 'eia', 'fred', 'usda'],
'Santé & Catastrophes': ['who', 'emdat'],
'Réseaux sociaux': ['twitter_trump'],
'Active RSS Feeds': ['reuters_world', 'reuters_business', 'reuters_energy', 'ap_top', 'aljazeera', 'ft', 'bloomberg'],
'Economic Data': ['newsapi', 'gdelt', 'eia', 'fred', 'usda'],
'Health & Disasters': ['who', 'emdat'],
'Social Media': ['twitter_trump'],
}
const SOURCE_DOCS: Record<string, { description: string; link?: string; cost: string }> = {
reuters_world: { description: 'Actualités mondiales Reuters', cost: 'Gratuit' },
reuters_business: { description: 'Business et marchés Reuters', cost: 'Gratuit' },
reuters_energy: { description: 'Énergie et commodités Reuters', cost: 'Gratuit' },
ap_top: { description: 'Associated Press — Top Stories', cost: 'Gratuit' },
aljazeera: { description: 'Al Jazeera — couverture Moyen-Orient/Monde', cost: 'Gratuit' },
ft: { description: 'Financial Times — finance internationale', cost: 'Gratuit' },
bloomberg: { description: 'Bloomberg Markets RSS', cost: 'Gratuit' },
newsapi: { description: '100 req/jour gratuit — actualités mondiales multi-sources', link: 'https://newsapi.org', cost: '100 req/j gratuit' },
gdelt: { description: 'Base de données géopolitique mondiale — 300K events/jour', link: 'https://gdeltproject.org', cost: 'Gratuit total' },
eia: { description: 'US Energy Information Administration — données pétrole/gaz hebdo', link: 'https://www.eia.gov/opendata', cost: 'Gratuit avec clé' },
fred: { description: 'Federal Reserve St. Louis — macro US (PIB, inflation, emploi)', link: 'https://fred.stlouisfed.org/docs/api/fred/', cost: 'Gratuit avec clé' },
usda: { description: 'USDA — rapports agricoles officiels US', cost: 'Gratuit' },
who: { description: 'OMS — alertes sanitaires mondiales RSS', cost: 'Gratuit' },
emdat: { description: 'EM-DAT — base de données catastrophes naturelles', link: 'https://www.emdat.be', cost: 'Inscription gratuite' },
twitter_trump: { description: 'Flux X/Twitter Trump — discours, annonces tarifs', cost: 'API payante ($100/mois+)' },
reuters_world: { description: 'Reuters World News', cost: 'Free' },
reuters_business: { description: 'Reuters Business & Markets', cost: 'Free' },
reuters_energy: { description: 'Reuters Energy & Commodities', cost: 'Free' },
ap_top: { description: 'Associated Press — Top Stories', cost: 'Free' },
aljazeera: { description: 'Al Jazeera — Middle East/World coverage', cost: 'Free' },
ft: { description: 'Financial Times — international finance', cost: 'Free' },
bloomberg: { description: 'Bloomberg Markets RSS', cost: 'Free' },
newsapi: { description: '100 req/day free — multi-source world news', link: 'https://newsapi.org', cost: '100 req/d free' },
gdelt: { description: 'Global geopolitical database — 300K events/day', link: 'https://gdeltproject.org', cost: 'Totally free' },
eia: { description: 'US Energy Information Administration — weekly oil/gas data', link: 'https://www.eia.gov/opendata', cost: 'Free with key' },
fred: { description: 'Federal Reserve St. Louis — US macro (GDP, inflation, employment)', link: 'https://fred.stlouisfed.org/docs/api/fred/', cost: 'Free with key' },
usda: { description: 'USDA — official US agricultural reports', cost: 'Free' },
who: { description: 'WHO — global health alerts RSS', cost: 'Free' },
emdat: { description: 'EM-DAT — natural disasters database', link: 'https://www.emdat.be', cost: 'Free registration' },
twitter_trump: { description: 'X/Twitter Trump feed — speeches, tariff announcements', cost: 'Paid API ($100/mo+)' },
}
// ── Risk Profiles Component ───────────────────────────────────────────────────
const PROFILE_COLORS = [
{ value: '#22c55e', label: 'Vert' },
{ value: '#3b82f6', label: 'Bleu' },
{ value: '#22c55e', label: 'Green' },
{ value: '#3b82f6', label: 'Blue' },
{ value: '#f97316', label: 'Orange' },
{ value: '#ef4444', label: 'Rouge' },
{ value: '#8b5cf6', label: 'Violet' },
{ value: '#eab308', label: 'Jaune' },
{ value: '#ef4444', label: 'Red' },
{ value: '#8b5cf6', label: 'Purple' },
{ value: '#eab308', label: 'Yellow' },
]
function NextRunCountdown({ nextRunAt }: { nextRunAt: string }) {
@@ -64,16 +64,16 @@ function NextRunCountdown({ nextRunAt }: { nextRunAt: string }) {
return (
<div className="flex items-center gap-2 text-emerald-400/80">
<span className="inline-block w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
<span>Prochain cycle dans <span className="font-mono font-semibold text-emerald-300">{remaining}</span></span>
<span className="text-slate-600">({absTime} heure locale)</span>
<span>Next cycle in <span className="font-mono font-semibold text-emerald-300">{remaining}</span></span>
<span className="text-slate-600">({absTime} local time)</span>
</div>
)
}
function evNetLabel(evNet: number): { text: string; cls: string } {
if (evNet > 0.05) return { text: `EV nette +${(evNet * 100).toFixed(0)}%`, cls: 'text-emerald-400' }
if (evNet >= -0.01) return { text: 'EV nette ≈ 0', cls: 'text-yellow-400' }
return { text: `EV nette ${(evNet * 100).toFixed(0)}%`, cls: 'text-slate-600' }
if (evNet > 0.05) return { text: `Net EV +${(evNet * 100).toFixed(0)}%`, cls: 'text-emerald-400' }
if (evNet >= -0.01) return { text: 'Net EV ≈ 0', cls: 'text-yellow-400' }
return { text: `Net EV ${(evNet * 100).toFixed(0)}%`, cls: 'text-slate-600' }
}
function ProfileRow({
@@ -113,7 +113,7 @@ function ProfileRow({
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className={clsx('text-sm font-semibold', enabled ? 'text-slate-200' : 'text-slate-600')}>{profile.name}</span>
{!enabled && <span className="text-[10px] text-slate-700 italic">désactivé</span>}
{!enabled && <span className="text-[10px] text-slate-700 italic">disabled</span>}
<span className="text-xs text-slate-500">Score <span className="text-slate-300 font-mono">{profile.min_score}</span></span>
<span className="text-xs text-slate-500">Gain <span className="text-slate-300 font-mono">{profile.min_gain_pct}%</span></span>
<span className={clsx('text-[11px] font-mono', fevc)}>{fev}</span>
@@ -138,7 +138,7 @@ function ProfileRow({
<div className="bg-dark-700/60 rounded-lg p-3 border border-slate-600/40 space-y-3">
<div className="grid grid-cols-3 gap-3">
<div>
<label className="text-[10px] text-slate-500 mb-1 block">Nom du profil</label>
<label className="text-[10px] text-slate-500 mb-1 block">Profile name</label>
<input value={name} onChange={e => setName(e.target.value)}
className="w-full bg-dark-800 border border-slate-700 rounded px-2 py-1 text-sm text-white" />
</div>
@@ -158,7 +158,7 @@ function ProfileRow({
{/* Live EV preview */}
<div className="bg-dark-800 rounded px-3 py-2 text-xs flex items-center gap-4 flex-wrap">
<span className="text-slate-500">À la frontière (score={score}, gain={gain}%)</span>
<span className="text-slate-500">At the frontier (score={score}, gain={gain}%)</span>
<span className={clsx('font-mono font-bold', evCls)}>{evText}</span>
<span className="text-slate-600">Trade Score = <span className="text-slate-400">{trade_score.toFixed(1)}</span>/100</span>
<span className="text-slate-600">
@@ -179,14 +179,14 @@ function ProfileRow({
<button onClick={() => setEnabled(!enabled)}
className={clsx('text-xs px-2 py-0.5 rounded border transition-all',
enabled ? 'border-emerald-600/40 text-emerald-400 bg-emerald-900/20' : 'border-slate-700 text-slate-600')}>
{enabled ? '✓ Activé' : '○ Désactivé'}
{enabled ? '✓ Enabled' : '○ Disabled'}
</button>
<div className="ml-auto flex gap-2">
<button onClick={() => setEditing(false)}
className="text-xs text-slate-500 hover:text-slate-300 px-2 py-1">Annuler</button>
className="text-xs text-slate-500 hover:text-slate-300 px-2 py-1">Cancel</button>
<button onClick={save}
className="text-xs bg-blue-600 hover:bg-blue-500 text-white px-3 py-1 rounded font-semibold">
Sauvegarder
Save
</button>
</div>
</div>
@@ -227,22 +227,22 @@ function RiskProfilesCard() {
<div className="card bg-dark-700/20 mb-4">
<div className="flex items-center justify-between mb-3">
<div>
<div className="text-sm font-semibold text-slate-300">Profils de risque</div>
<div className="text-sm font-semibold text-slate-300">Risk Profiles</div>
<div className="text-[10px] text-slate-600 mt-0.5">
Un trade est loggé dans le journal s'il passe <span className="text-slate-500">au moins un</span> profil activé
· Formule : EV nette = (score/100 × gain/100) (1 score/100)
A trade is logged in the journal if it passes <span className="text-slate-500">at least one</span> enabled profile
· Formula: Net EV = (score/100 × gain/100) (1 score/100)
</div>
</div>
<button onClick={() => setShowNew(true)}
className="flex items-center gap-1 text-xs border border-blue-500/40 text-blue-400 hover:bg-blue-900/20 px-2.5 py-1 rounded">
<Plus className="w-3 h-3" /> Nouveau profil
<Plus className="w-3 h-3" /> New profile
</button>
</div>
{isLoading ? (
<div className="h-16 animate-pulse bg-dark-700 rounded" />
) : profiles.length === 0 ? (
<div className="text-center py-6 text-slate-600 text-sm">Aucun profil — tous les trades seront ignorés</div>
<div className="text-center py-6 text-slate-600 text-sm">No profiles all trades will be ignored</div>
) : (
<div className="space-y-1">
{profiles.map((p: any) => (
@@ -257,13 +257,13 @@ function RiskProfilesCard() {
{showNew && (
<div className="bg-dark-700/60 rounded-lg p-3 border border-blue-700/30 space-y-3 mt-3">
<div className="flex items-center justify-between">
<span className="text-xs font-semibold text-blue-400">Nouveau profil</span>
<span className="text-xs font-semibold text-blue-400">New profile</span>
<button onClick={() => setShowNew(false)} className="text-slate-600 hover:text-slate-400"><X className="w-3.5 h-3.5" /></button>
</div>
<div className="grid grid-cols-3 gap-3">
<div>
<label className="text-[10px] text-slate-500 mb-1 block">Nom</label>
<input value={newName} onChange={e => setNewName(e.target.value)} placeholder="ex: Ultra-risqué"
<label className="text-[10px] text-slate-500 mb-1 block">Name</label>
<input value={newName} onChange={e => setNewName(e.target.value)} placeholder="e.g. Ultra-aggressive"
className="w-full bg-dark-800 border border-slate-700 rounded px-2 py-1 text-sm text-white placeholder:text-slate-700" />
</div>
<div>
@@ -282,7 +282,7 @@ function RiskProfilesCard() {
<div className="bg-dark-800 rounded px-3 py-2 text-xs flex items-center gap-4 flex-wrap">
<span className={clsx('font-mono font-bold', evNetLabel(nEvNet).cls)}>{evNetLabel(nEvNet).text}</span>
<span className="text-slate-600">Trade Score = <span className="text-slate-400">{nTradeScore.toFixed(1)}</span>/100</span>
<span className="text-slate-700">Score min pour EV=0 avec gain {newGain}% : <span className="text-slate-500 font-mono">{nMinScore}</span>/100</span>
<span className="text-slate-700">Min score for EV=0 with gain {newGain}% : <span className="text-slate-500 font-mono">{nMinScore}</span>/100</span>
</div>
<div className="flex items-center gap-3">
@@ -295,10 +295,10 @@ function RiskProfilesCard() {
))}
</div>
<div className="ml-auto flex gap-2">
<button onClick={() => setShowNew(false)} className="text-xs text-slate-500 hover:text-slate-300 px-2 py-1">Annuler</button>
<button onClick={() => setShowNew(false)} className="text-xs text-slate-500 hover:text-slate-300 px-2 py-1">Cancel</button>
<button onClick={saveNew} disabled={!newName.trim()}
className="text-xs bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-3 py-1 rounded font-semibold">
Créer
Create
</button>
</div>
</div>
@@ -308,7 +308,7 @@ function RiskProfilesCard() {
{/* Frontier visualization */}
{profiles.length > 0 && (
<div className="mt-3 pt-3 border-t border-slate-700/30">
<div className="text-[10px] text-slate-600 mb-2">Frontière d'acceptation chaque point représente le minimum requis par profil</div>
<div className="text-[10px] text-slate-600 mb-2">Acceptance frontier each point represents the minimum required per profile</div>
<div className="flex flex-wrap gap-2">
{profiles.filter((p: any) => p.enabled).map((p: any) => {
const { text: ev, cls } = evNetLabel(p.ev_net_at_frontier ?? 0)
@@ -412,15 +412,15 @@ export default function Config() {
mutationFn: (cfg: any) => fetch(`${API}/api/var/scheduler/config`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(cfg),
}).then(r => r.json()),
onSuccess: () => { refetchSched(); setSavedMsg('Schedulers sauvegardés'); setTimeout(() => setSavedMsg(''), 2000) },
onSuccess: () => { refetchSched(); setSavedMsg('Schedulers saved'); setTimeout(() => setSavedMsg(''), 2000) },
})
const { mutate: triggerVarNow, isPending: triggeringVar } = useMutation({
mutationFn: () => fetch(`${API}/api/var/run-now`, { method: 'POST' }).then(r => r.json()),
onSuccess: () => { setSavedMsg('Snapshot VaR sauvegardé'); setTimeout(() => setSavedMsg(''), 2000) },
onSuccess: () => { setSavedMsg('VaR snapshot saved'); setTimeout(() => setSavedMsg(''), 2000) },
})
const { mutate: triggerPnlNow, isPending: triggeringPnl } = useMutation({
mutationFn: () => fetch(`${API}/api/var/pnl/run-now`, { method: 'POST' }).then(r => r.json()),
onSuccess: () => { setSavedMsg('Snapshot PnL sauvegardé'); setTimeout(() => setSavedMsg(''), 2000) },
onSuccess: () => { setSavedMsg('PnL snapshot saved'); setTimeout(() => setSavedMsg(''), 2000) },
})
// Auto-cycle local state
@@ -486,7 +486,7 @@ export default function Config() {
const saveSources = () => {
updateSources(displaySources, {
onSuccess: () => { setSavedMsg('Sources sauvegardées'); setTimeout(() => setSavedMsg(''), 2000) }
onSuccess: () => { setSavedMsg('Sources saved'); setTimeout(() => setSavedMsg(''), 2000) }
})
}
@@ -498,7 +498,7 @@ export default function Config() {
if (fredKey) keys.fred_api_key = fredKey
updateApiKeys(keys, {
onSuccess: () => {
setSavedMsg('Clés API sauvegardées')
setSavedMsg('API keys saved')
setTimeout(() => setSavedMsg(''), 2000)
setOpenaiKey(''); setNewsapiKey(''); setEiaKey(''); setFredKey('')
}
@@ -506,11 +506,11 @@ export default function Config() {
}
const CONFIG_TABS = [
{ key: 'ia', label: 'IA & Analyse', icon: Brain },
{ key: 'ia', label: 'AI & Analysis', icon: Brain },
{ key: 'cycle', label: 'Auto-Cycle & Logging', icon: Zap },
{ key: 'sources', label: 'Sources', icon: Globe },
{ key: 'options', label: 'Options — Paramètres', icon: TrendingUp },
{ key: 'profiles', label: 'Profils de risque', icon: SlidersHorizontal },
{ key: 'options', label: 'Options — Settings', icon: TrendingUp },
{ key: 'profiles', label: 'Risk Profiles', icon: SlidersHorizontal },
] as const
return (
@@ -520,7 +520,7 @@ export default function Config() {
<h1 className="text-xl font-bold text-white flex items-center gap-2">
<Settings className="w-5 h-5 text-blue-400" /> Configuration
</h1>
<p className="text-xs text-slate-500 mt-0.5">Clés API, sources, paramètres IA &amp; cycle</p>
<p className="text-xs text-slate-500 mt-0.5">API keys, sources, AI &amp; cycle settings</p>
</div>
{savedMsg && (
<div className="flex items-center gap-2 text-emerald-400 text-sm bg-emerald-900/10 border border-emerald-700/30 rounded px-3 py-1.5">
@@ -549,30 +549,30 @@ export default function Config() {
<div className="space-y-4">
{/* AI Status */}
<div className={clsx('card', aiStatus?.enabled ? 'border-emerald-700/40' : 'border-slate-700/40')}>
<div className="section-title flex items-center gap-1"><Key className="w-3 h-3" /> Statut IA</div>
<div className="section-title flex items-center gap-1"><Key className="w-3 h-3" /> AI Status</div>
<div className="flex items-center gap-3 mb-3">
{aiStatus?.enabled ? (
<><CheckCircle className="w-5 h-5 text-emerald-400" />
<div><div className="text-sm text-emerald-400 font-semibold">OpenAI Connecté</div>
<div><div className="text-sm text-emerald-400 font-semibold">OpenAI Connected</div>
<div className="text-xs text-slate-500">GPT-4o + GPT-4o-mini</div></div></>
) : (
<><XCircle className="w-5 h-5 text-red-400" />
<div><div className="text-sm text-red-400 font-semibold">OpenAI Non configuré</div>
<div className="text-xs text-slate-500">Entrer la clé ci-dessous</div></div></>
<div><div className="text-sm text-red-400 font-semibold">OpenAI Not configured</div>
<div className="text-xs text-slate-500">Enter key below</div></div></>
)}
</div>
<div className="text-xs text-slate-600 space-y-1">
<div> Analyse de discours (Trump, Powell...)</div>
<div> Classification IA des actualités</div>
<div> Évaluation de patterns</div>
<div> Top 10 idées contextualisées</div>
<div> Speech analysis (Trump, Powell...)</div>
<div> AI news classification</div>
<div> Pattern evaluation</div>
<div> Top 10 contextualized ideas</div>
</div>
</div>
{/* OpenAI API Key */}
<div className="card">
<div className="flex items-center justify-between mb-3">
<div className="section-title mb-0 flex items-center gap-1"><Key className="w-3 h-3" /> Clé OpenAI</div>
<div className="section-title mb-0 flex items-center gap-1"><Key className="w-3 h-3" /> OpenAI Key</div>
<button onClick={() => setShowKeys(!showKeys)} className="text-slate-500 hover:text-slate-300">
{showKeys ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
</button>
@@ -582,7 +582,7 @@ export default function Config() {
<div className="flex items-center justify-between mb-1">
<label className="text-xs text-slate-500">OpenAI API Key</label>
<span className={clsx('text-xs', aiStatus?.enabled ? 'text-emerald-400' : 'text-slate-600')}>
{aiStatus?.enabled ? '✓ Actif' : '○ Inactif'}
{aiStatus?.enabled ? '✓ Active' : '○ Inactive'}
</span>
</div>
<input
@@ -599,7 +599,7 @@ export default function Config() {
className="w-full bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white rounded py-1.5 text-xs font-semibold flex items-center justify-center gap-1.5"
>
<Save className="w-3.5 h-3.5" />
{savingKeys ? 'Sauvegarde...' : 'Sauvegarder la clé'}
{savingKeys ? 'Saving...' : 'Save key'}
</button>
</div>
</div>
@@ -608,11 +608,11 @@ export default function Config() {
{/* Right: Analysis params */}
<div className="card">
<h2 className="text-base font-bold text-white flex items-center gap-2 mb-4">
<SlidersHorizontal className="w-4 h-4 text-blue-400" /> Paramètres d'analyse IA
<SlidersHorizontal className="w-4 h-4 text-blue-400" /> AI Analysis Settings
</h2>
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<label className="text-xs text-slate-500 mb-1 block">Top N résultats par défaut</label>
<label className="text-xs text-slate-500 mb-1 block">Default top N results</label>
<div className="flex gap-1">
{[5, 10, 15, 20].map(n => (
<button key={n} onClick={() => setAnalysisTopN(n)}
@@ -626,46 +626,46 @@ export default function Config() {
</div>
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Catégorie par défaut</label>
<label className="text-xs text-slate-500 mb-1 block">Default category</label>
<select value={analysisCategoryDefault} onChange={e => setAnalysisCategoryDefault(e.target.value)}
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-blue-500">
<option value="all">Toutes les catégories</option>
<option value="energy">Énergie</option>
<option value="metals">Métaux</option>
<option value="all">All categories</option>
<option value="energy">Energy</option>
<option value="metals">Metals</option>
<option value="agriculture">Agriculture</option>
<option value="indices">Indices</option>
<option value="equities">Actions</option>
<option value="equities">Equities</option>
<option value="forex">Forex</option>
</select>
</div>
</div>
<div className="mb-4">
<div className="flex items-center justify-between mb-1">
<label className="text-xs text-slate-500">Template d'analyse (prompt GPT-4o)</label>
<label className="text-xs text-slate-500">Analysis template (GPT-4o prompt)</label>
<button onClick={() => setAnalysisTemplate('')} className="text-xs text-slate-600 hover:text-slate-400">
Par faut
Default
</button>
</div>
<textarea
value={analysisTemplate}
onChange={e => setAnalysisTemplate(e.target.value)}
rows={8}
placeholder="Laisser vide pour utiliser le template par défaut..."
placeholder="Leave empty to use default template..."
className="w-full bg-dark-700 border border-slate-700 rounded px-3 py-2 text-sm text-white font-mono focus:outline-none focus:border-blue-500 resize-y"
/>
<div className="text-xs text-slate-600 mt-1">
Contexte marché (prix, IV, variation 1j) et news filtrées sont injectés automatiquement.
Market context (prices, IV, 1d change) and filtered news are automatically injected.
</div>
</div>
<button
onClick={() => saveAnalysis(
{ top_n: analysisTopN, category_filter: analysisCategoryDefault, template: analysisTemplate || undefined },
{ onSuccess: () => { setSavedMsg('Paramètres d\'analyse sauvegardés'); setTimeout(() => setSavedMsg(''), 2000) } }
{ onSuccess: () => { setSavedMsg('Analysis settings saved'); setTimeout(() => setSavedMsg(''), 2000) } }
)}
disabled={savingAnalysis}
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
<Save className="w-4 h-4" />
{savingAnalysis ? 'Sauvegarde...' : 'Sauvegarder'}
{savingAnalysis ? 'Saving...' : 'Save'}
</button>
</div>
</div>
@@ -677,11 +677,11 @@ export default function Config() {
{/* Score min threshold — prominent */}
<div className="card border-blue-700/40 bg-blue-900/5">
<h2 className="text-base font-bold text-white flex items-center gap-2 mb-1">
<SlidersHorizontal className="w-4 h-4 text-blue-400" /> Score minimum pour le cycle
<SlidersHorizontal className="w-4 h-4 text-blue-400" /> Minimum score for the cycle
</h2>
<p className="text-xs text-slate-500 mb-4">
Pré-filtre du cycle les patterns avec un score inférieur ne sont pas soumis à l'analyse.
Les profils de risque déterminent ensuite quels trades sont loggés.
Cycle pre-filter patterns with a score below this are not submitted for analysis.
Risk profiles then determine which trades get logged.
</p>
<div className="grid grid-cols-2 gap-6">
<div>
@@ -694,12 +694,12 @@ export default function Config() {
onChange={e => setMinScore(parseInt(e.target.value))}
className="w-full accent-blue-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>0 (tous)</span><span>80 (très sélectif)</span>
<span>0 (all)</span><span>80 (very selective)</span>
</div>
<div className="text-xs text-slate-500 mt-2">
{minScore === 0
? 'Tous les patterns analysés par le cycle'
: `Seuls les patterns avec score ≥ ${minScore} sont soumis au cycle`}
? 'All patterns processed by the cycle'
: `Only patterns with score ≥ ${minScore} are submitted to the cycle`}
</div>
</div>
<div>
@@ -716,10 +716,10 @@ export default function Config() {
</div>
<div className="text-xs text-slate-500 mt-2">
{minEv === 0
? 'Toute EV nette acceptée'
? 'Any net EV accepted'
: minEv > 0
? `EV nette ≥ +${(minEv * 100).toFixed(0)}% requise`
: `Filtre désactivé (EV < ${(minEv * 100).toFixed(0)}%)`}
? `Net EV ≥ +${(minEv * 100).toFixed(0)}% required`
: `Filter disabled (EV < ${(minEv * 100).toFixed(0)}%)`}
</div>
</div>
</div>
@@ -735,38 +735,38 @@ export default function Config() {
<div className="flex items-center gap-2 text-xs">
{cs.running ? (
<span className="flex items-center gap-1 text-blue-400 bg-blue-900/30 border border-blue-700/30 px-2 py-0.5 rounded animate-pulse">
<RefreshCw className="w-3 h-3 animate-spin" /> En cours...
<RefreshCw className="w-3 h-3 animate-spin" /> Running...
</span>
) : cs.scheduler_alive ? (
<span className="text-emerald-400 bg-emerald-900/30 border border-emerald-700/30 px-2 py-0.5 rounded">
Scheduler actif
Scheduler active
</span>
) : (
<span className="text-slate-600 bg-dark-700 border border-slate-700/30 px-2 py-0.5 rounded">
Scheduler inactif
Scheduler inactive
</span>
)}
</div>
)}
</div>
<p className="text-xs text-slate-500 mb-4">
Toutes les N heures : suggère de nouveaux patterns, filtre les doublons, score tout, log les prix et génère un commentaire IA.
Every N hours: suggests new patterns, deduplicates, scores everything, logs prices, and generates an AI comment.
</p>
<div className="grid grid-cols-3 gap-4 mb-4">
<div>
<label className="text-xs text-slate-500 mb-2 block">Activer l'auto-cycle</label>
<label className="text-xs text-slate-500 mb-2 block">Enable auto-cycle</label>
<button
onClick={() => setCycleEnabled(!cycleEnabled)}
className={clsx('w-full py-2 rounded border text-sm font-semibold transition-all', {
'bg-blue-600 border-blue-500 text-white': cycleEnabled,
'bg-dark-700 border-slate-700 text-slate-400 hover:border-slate-500': !cycleEnabled,
})}>
{cycleEnabled ? '✓ Activé' : '○ Désactivé'}
{cycleEnabled ? '✓ Enabled' : '○ Disabled'}
</button>
</div>
<div>
<label className="text-xs text-slate-500 mb-2 block">Intervalle (heures)</label>
<label className="text-xs text-slate-500 mb-2 block">Interval (hours)</label>
<div className="flex gap-1">
{[1, 3, 6, 12].map(h => (
<button key={h} onClick={() => setCycleHours(h)}
@@ -781,7 +781,7 @@ export default function Config() {
</div>
<div>
<label className="text-xs text-slate-500 mb-2 block">
Similarité max ({Math.round(cycleSimilarity * 100)}%)
Max similarity ({Math.round(cycleSimilarity * 100)}%)
</label>
<input type="range" min="0.1" max="0.8" step="0.05"
value={cycleSimilarity}
@@ -796,7 +796,7 @@ export default function Config() {
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<label className="text-xs text-slate-500 mb-2 block">
Rétention Journal ({retentionDays}j)
Journal retention ({retentionDays}d)
</label>
<div className="flex gap-1">
{[30, 60, 90, 180].map(d => (
@@ -812,7 +812,7 @@ export default function Config() {
</div>
<div>
<label className="text-xs text-slate-500 mb-2 block">
Seuil maturité ({maturityThreshold}%)
Maturity threshold ({maturityThreshold}%)
</label>
<div className="flex gap-1">
{[20, 30, 35, 50].map(p => (
@@ -832,8 +832,8 @@ export default function Config() {
<div className="border border-slate-700/50 rounded-lg p-3 mb-4 bg-dark-700/30">
<div className="flex items-center justify-between mb-3">
<div>
<span className="text-xs font-semibold text-slate-300">Cycles weekend</span>
<p className="text-[10px] text-slate-500 mt-0.5">Sam/Dim Globex ouvre dim ~18h UTC. Heures en UTC.</p>
<span className="text-xs font-semibold text-slate-300">Weekend cycles</span>
<p className="text-[10px] text-slate-500 mt-0.5">Sat/Sun Globex opens Sun ~18h UTC. Times in UTC.</p>
</div>
<button
onClick={() => setWeekendEnabled(!weekendEnabled)}
@@ -841,12 +841,12 @@ export default function Config() {
'bg-amber-600/20 border-amber-500/50 text-amber-300': weekendEnabled,
'bg-dark-700 border-slate-700 text-slate-500 hover:border-slate-500': !weekendEnabled,
})}>
{weekendEnabled ? '✓ Activé' : '○ Désactivé'}
{weekendEnabled ? '✓ Enabled' : '○ Disabled'}
</button>
</div>
{weekendEnabled && (
<div>
<label className="text-[10px] text-slate-500 mb-1.5 block">Horaires UTC (cliquer pour activer/désactiver)</label>
<label className="text-[10px] text-slate-500 mb-1.5 block">UTC times (click to enable/disable)</label>
<div className="flex gap-1.5 flex-wrap">
{['06:00', '08:00', '12:00', '18:00', '22:00', '00:00'].map(t => {
const active = weekendTimes.includes(t)
@@ -865,7 +865,7 @@ export default function Config() {
})}
</div>
{weekendTimes.length === 0 && (
<p className="text-[10px] text-red-400 mt-1">Sélectionner au moins un horaire</p>
<p className="text-[10px] text-red-400 mt-1">Select at least one time</p>
)}
</div>
)}
@@ -874,14 +874,14 @@ export default function Config() {
{cs?.last_cycle && (
<div className="card bg-dark-700/50 mb-2 text-xs space-y-2">
<div className="flex items-center gap-4 flex-wrap">
<span className="text-slate-500">Dernier cycle :</span>
<span className="text-slate-500">Last cycle:</span>
<span className="text-slate-300">{cs.last_cycle.started_at?.slice(0, 16)} UTC</span>
<span className={clsx('badge', cs.last_cycle.status === 'completed' ? 'badge-green' : 'badge-red')}>
{cs.last_cycle.status}
</span>
<span className="text-slate-500">+{cs.last_cycle.patterns_added} patterns</span>
<span className="text-slate-500">{cs.last_cycle.patterns_scored} scorés</span>
<span className="text-slate-500">Géo: {cs.last_cycle.geo_score}</span>
<span className="text-slate-500">{cs.last_cycle.patterns_scored} scored</span>
<span className="text-slate-500">Geo: {cs.last_cycle.geo_score}</span>
<span className="text-slate-500 capitalize">{cs.last_cycle.dominant_regime}</span>
</div>
</div>
@@ -906,19 +906,19 @@ export default function Config() {
weekend_cycle_enabled: weekendEnabled,
weekend_cycle_times: weekendTimes.length > 0 ? weekendTimes.join(',') : '08:00,22:00',
},
{ onSuccess: () => { refetchCycle(); setSavedMsg('Auto-cycle configuré'); setTimeout(() => setSavedMsg(''), 2000) } }
{ onSuccess: () => { refetchCycle(); setSavedMsg('Auto-cycle configured'); setTimeout(() => setSavedMsg(''), 2000) } }
)}
disabled={savingCycle}
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
<Save className="w-4 h-4" />
{savingCycle ? 'Sauvegarde...' : 'Appliquer'}
{savingCycle ? 'Saving...' : 'Apply'}
</button>
<button
onClick={() => triggerCycle(undefined, { onSuccess: () => { refetchCycle(); setSavedMsg('Cycle lancé !'); setTimeout(() => setSavedMsg(''), 3000) } })}
onClick={() => triggerCycle(undefined, { onSuccess: () => { refetchCycle(); setSavedMsg('Cycle started!'); setTimeout(() => setSavedMsg(''), 3000) } })}
disabled={triggeringCycle || !aiStatus?.enabled}
className="flex items-center gap-1.5 border border-blue-500/50 text-blue-400 hover:bg-blue-900/20 disabled:opacity-40 px-4 py-2 rounded text-sm font-semibold">
<RefreshCw className={clsx('w-4 h-4', triggeringCycle && 'animate-spin')} />
Lancer maintenant
Run now
</button>
</div>
</div>
@@ -929,8 +929,8 @@ export default function Config() {
<Gauge className="w-4 h-4 text-red-400" /> Schedulers VaR &amp; PnL
</h2>
<p className="text-xs text-slate-500 mb-4">
Calcule et sauvegarde automatiquement les snapshots VaR (Black-Scholes delta) et PnL à intervalles définis.
Les données sont accessibles depuis la page VaR Analyse avec un historique complet.
Automatically computes and saves VaR (Black-Scholes delta) and PnL snapshots at defined intervals.
Data is accessible from the VaR Analysis page with full history.
</p>
<div className="grid grid-cols-2 gap-6 mb-4">
{/* VaR scheduler */}
@@ -939,21 +939,21 @@ export default function Config() {
<Gauge className="w-3.5 h-3.5 text-red-400" />
<span className="text-sm font-semibold text-slate-300">Scheduler VaR</span>
{schedStatus?.var?.alive
? <span className="text-[10px] bg-emerald-900/30 text-emerald-400 border border-emerald-700/30 px-1.5 py-0.5 rounded ml-auto"> Actif</span>
: <span className="text-[10px] bg-dark-700 text-slate-600 border border-slate-700/30 px-1.5 py-0.5 rounded ml-auto"> Inactif</span>}
? <span className="text-[10px] bg-emerald-900/30 text-emerald-400 border border-emerald-700/30 px-1.5 py-0.5 rounded ml-auto"> Active</span>
: <span className="text-[10px] bg-dark-700 text-slate-600 border border-slate-700/30 px-1.5 py-0.5 rounded ml-auto"> Inactive</span>}
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Activer</label>
<label className="text-xs text-slate-500 mb-1 block">Enable</label>
<button onClick={() => setVarSchedEnabled(!varSchedEnabled)}
className={clsx('w-full py-2 rounded border text-sm font-semibold transition-all', {
'bg-red-700 border-red-600 text-white': varSchedEnabled,
'bg-dark-700 border-slate-700 text-slate-400': !varSchedEnabled,
})}>
{varSchedEnabled ? '✓ Activé' : '○ Désactivé'}
{varSchedEnabled ? '✓ Enabled' : '○ Disabled'}
</button>
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Intervalle</label>
<label className="text-xs text-slate-500 mb-1 block">Interval</label>
<div className="flex gap-1">
{[1, 3, 6, 12, 24].map(h => (
<button key={h} onClick={() => setVarSchedHours(h)}
@@ -974,21 +974,21 @@ export default function Config() {
<DollarSign className="w-3.5 h-3.5 text-emerald-400" />
<span className="text-sm font-semibold text-slate-300">Scheduler PnL</span>
{schedStatus?.pnl?.alive
? <span className="text-[10px] bg-emerald-900/30 text-emerald-400 border border-emerald-700/30 px-1.5 py-0.5 rounded ml-auto"> Actif</span>
: <span className="text-[10px] bg-dark-700 text-slate-600 border border-slate-700/30 px-1.5 py-0.5 rounded ml-auto"> Inactif</span>}
? <span className="text-[10px] bg-emerald-900/30 text-emerald-400 border border-emerald-700/30 px-1.5 py-0.5 rounded ml-auto"> Active</span>
: <span className="text-[10px] bg-dark-700 text-slate-600 border border-slate-700/30 px-1.5 py-0.5 rounded ml-auto"> Inactive</span>}
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Activer</label>
<label className="text-xs text-slate-500 mb-1 block">Enable</label>
<button onClick={() => setPnlSchedEnabled(!pnlSchedEnabled)}
className={clsx('w-full py-2 rounded border text-sm font-semibold transition-all', {
'bg-emerald-700 border-emerald-600 text-white': pnlSchedEnabled,
'bg-dark-700 border-slate-700 text-slate-400': !pnlSchedEnabled,
})}>
{pnlSchedEnabled ? '✓ Activé' : '○ Désactivé'}
{pnlSchedEnabled ? '✓ Enabled' : '○ Disabled'}
</button>
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Intervalle</label>
<label className="text-xs text-slate-500 mb-1 block">Interval</label>
<div className="flex gap-1">
{[1, 2, 4, 8, 24].map(h => (
<button key={h} onClick={() => setPnlSchedHours(h)}
@@ -1010,21 +1010,21 @@ export default function Config() {
disabled={savingSched}
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
<Save className="w-4 h-4" />
{savingSched ? 'Sauvegarde…' : 'Appliquer'}
{savingSched ? 'Saving…' : 'Apply'}
</button>
<button
onClick={() => triggerVarNow()}
disabled={triggeringVar}
className="flex items-center gap-1.5 border border-red-500/50 text-red-400 hover:bg-red-900/20 disabled:opacity-40 px-4 py-2 rounded text-sm font-semibold">
<Gauge className={clsx('w-4 h-4', triggeringVar && 'animate-spin')} />
Snapshot VaR maintenant
VaR snapshot now
</button>
<button
onClick={() => triggerPnlNow()}
disabled={triggeringPnl}
className="flex items-center gap-1.5 border border-emerald-500/50 text-emerald-400 hover:bg-emerald-900/20 disabled:opacity-40 px-4 py-2 rounded text-sm font-semibold">
<DollarSign className={clsx('w-4 h-4', triggeringPnl && 'animate-spin')} />
Snapshot PnL maintenant
PnL snapshot now
</button>
</div>
</div>
@@ -1037,16 +1037,16 @@ export default function Config() {
{/* Data API Keys */}
<div className="card">
<div className="flex items-center justify-between mb-3">
<div className="section-title mb-0 flex items-center gap-1"><Key className="w-3 h-3" /> Clés API données</div>
<div className="section-title mb-0 flex items-center gap-1"><Key className="w-3 h-3" /> Data API Keys</div>
<button onClick={() => setShowKeys(!showKeys)} className="text-slate-500 hover:text-slate-300">
{showKeys ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
</button>
</div>
<div className="grid grid-cols-3 gap-4">
{[
{ label: 'NewsAPI Key', value: newsapiKey, setter: setNewsapiKey, placeholder: 'Obtenir sur newsapi.org' },
{ label: 'EIA API Key', value: eiaKey, setter: setEiaKey, placeholder: 'Obtenir sur eia.gov' },
{ label: 'FRED API Key', value: fredKey, setter: setFredKey, placeholder: 'Obtenir sur fred.stlouisfed.org' },
{ label: 'NewsAPI Key', value: newsapiKey, setter: setNewsapiKey, placeholder: 'Get at newsapi.org' },
{ label: 'EIA API Key', value: eiaKey, setter: setEiaKey, placeholder: 'Get at eia.gov' },
{ label: 'FRED API Key', value: fredKey, setter: setFredKey, placeholder: 'Get at fred.stlouisfed.org' },
].map(({ label, value, setter, placeholder }) => (
<div key={label}>
<label className="text-xs text-slate-500 mb-1 block">{label}</label>
@@ -1065,7 +1065,7 @@ export default function Config() {
disabled={savingKeys || (!newsapiKey && !eiaKey && !fredKey)}
className="mt-3 flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
<Save className="w-4 h-4" />
{savingKeys ? 'Sauvegarde...' : 'Sauvegarder les clés'}
{savingKeys ? 'Saving...' : 'Save keys'}
</button>
</div>
@@ -1093,14 +1093,14 @@ export default function Config() {
<div className="flex items-center gap-2">
<span className="text-xs text-white font-semibold">{src.name || key}</span>
<span className={clsx('badge text-xs', {
'badge-green': doc?.cost === 'Gratuit' || doc?.cost === 'Gratuit total',
'badge-blue': doc?.cost?.includes('gratuit') || doc?.cost?.includes('Inscription'),
'badge-yellow': doc?.cost?.includes('clé'),
'badge-red': doc?.cost?.includes('payante'),
'badge-green': doc?.cost === 'Free' || doc?.cost === 'Totally free',
'badge-blue': doc?.cost?.includes('free') || doc?.cost?.includes('registration'),
'badge-yellow': doc?.cost?.includes('key') || doc?.cost?.includes('key'),
'badge-red': doc?.cost?.includes('Paid'),
})}>
{doc?.cost}
</span>
{requiresKey && !keySet && <span className="badge badge-orange text-xs">Clé requise</span>}
{requiresKey && !keySet && <span className="badge badge-orange text-xs">Key required</span>}
</div>
<div className="text-xs text-slate-500 mt-0.5">{doc?.description}</div>
</div>
@@ -1121,7 +1121,7 @@ export default function Config() {
<button onClick={saveSources} disabled={savingSources || !localSources}
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
<Save className="w-4 h-4" />
{savingSources ? 'Sauvegarde...' : 'Appliquer les changements'}
{savingSources ? 'Saving...' : 'Apply changes'}
</button>
</div>
</>
@@ -1137,7 +1137,7 @@ export default function Config() {
<div className="card border-blue-700/30 bg-blue-900/5">
<div className="flex items-center justify-between mb-1">
<h2 className="text-base font-bold text-white flex items-center gap-2">
<ShieldAlert className="w-4 h-4 text-blue-400" /> IV Gate Filtres d'entrée
<ShieldAlert className="w-4 h-4 text-blue-400" /> IV Gate Entry Filters
</h2>
<button
onClick={() => setIvGateEnabled(!ivGateEnabled)}
@@ -1145,13 +1145,13 @@ export default function Config() {
'bg-blue-600 border-blue-500 text-white': ivGateEnabled,
'bg-dark-700 border-slate-700 text-slate-400': !ivGateEnabled,
})}>
{ivGateEnabled ? ' Gate actif' : ' Gate désactivé'}
{ivGateEnabled ? '✓ Gate active' : '○ Gate disabled'}
</button>
</div>
<p className="text-xs text-slate-500 mb-5">
Bloque automatiquement les trades avec verdict <span className="text-red-400 font-semibold">ALERT</span> avant
qu'ils soient loggés dans le journal. Un trade ALERT est une stratégie incompatible avec le régime de volatilité actuel
(ex: Long Call à IVR 100%). Les trades bloqués apparaissent dans les trades skippés avec la raison <span className="text-slate-400 font-mono">[IV_GATE]</span>.
Automatically blocks trades with verdict <span className="text-red-400 font-semibold">ALERT</span> before
they are logged in the journal. An ALERT trade is a strategy incompatible with the current volatility regime
(e.g. Long Call at IVR 100%). Blocked trades appear in skipped trades with reason <span className="text-slate-400 font-mono">[IV_GATE]</span>.
</p>
<div className={clsx('space-y-5 transition-opacity', !ivGateEnabled && 'opacity-40 pointer-events-none')}>
@@ -1160,7 +1160,7 @@ export default function Config() {
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-xs text-slate-400 font-medium">
Seuil vol chère (IVR High)
High vol threshold (IVR High)
</label>
<span className={clsx('font-mono font-bold text-base', ivGateHigh >= 70 ? 'text-orange-400' : 'text-yellow-400')}>
{ivGateHigh}%
@@ -1186,7 +1186,7 @@ export default function Config() {
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-xs text-slate-400 font-medium">
Seuil vol extrême (IVR Extreme)
Extreme vol threshold (IVR Extreme)
</label>
<span className="font-mono font-bold text-base text-red-400">{ivGateExtreme}%</span>
</div>
@@ -1202,8 +1202,8 @@ export default function Config() {
<span>50%</span><span>100%</span>
</div>
<div className="text-[10px] text-slate-500 mt-1.5 space-y-0.5">
<div>IVR &gt; {ivGateExtreme}% <span className="text-red-400 font-semibold">BLOQUÉ</span> naked long vol ALERT</div>
<div>Straddle / Strangle ALERT dès IVR &gt; {ivGateExtreme}% (pénalité ×1.3)</div>
<div>IVR &gt; {ivGateExtreme}% <span className="text-red-400 font-semibold">BLOCKED</span> naked long vol ALERT</div>
<div>Straddle / Strangle ALERT when IVR &gt; {ivGateExtreme}% (penalty ×1.3)</div>
</div>
</div>
</div>
@@ -1213,7 +1213,7 @@ export default function Config() {
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-xs text-slate-400 font-medium">
Seuil skew put (protection chère)
Put skew threshold (expensive protection)
</label>
<span className="font-mono font-bold text-base text-orange-400">{ivGateSkew} pts</span>
</div>
@@ -1222,31 +1222,31 @@ export default function Config() {
onChange={e => setIvGateSkew(parseInt(e.target.value))}
className="w-full accent-orange-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>2pts (très sensible)</span><span>20pts (très tolérant)</span>
<span>2pts (very sensitive)</span><span>20pts (very tolerant)</span>
</div>
<div className="text-[10px] text-slate-500 mt-1.5">
Skew put &gt; {ivGateSkew}pts puts très chers, signal WARN sur Long Put
Put skew &gt; {ivGateSkew}pts puts very expensive, WARN signal on Long Put
</div>
</div>
{/* Visual summary */}
<div className="bg-dark-700/60 rounded-lg px-4 py-3 text-xs space-y-2">
<div className="text-slate-500 font-semibold mb-2 text-[10px] uppercase tracking-wide">Résumé du gate actuel</div>
<div className="text-slate-500 font-semibold mb-2 text-[10px] uppercase tracking-wide">Current gate summary</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-emerald-500" />
<span className="text-slate-400">IVR &lt; <span className="text-emerald-400 font-mono">{ivGateHigh}%</span> Toute stratégie OK</span>
<span className="text-slate-400">IVR &lt; <span className="text-emerald-400 font-mono">{ivGateHigh}%</span> Any strategy OK</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-yellow-500" />
<span className="text-slate-400">IVR <span className="text-yellow-400 font-mono">{ivGateHigh}{ivGateExtreme}%</span> Spreads préférés</span>
<span className="text-slate-400">IVR <span className="text-yellow-400 font-mono">{ivGateHigh}{ivGateExtreme}%</span> Spreads preferred</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-red-500 animate-pulse" />
<span className="text-slate-400">IVR &gt; <span className="text-red-400 font-mono">{ivGateExtreme}%</span> Long naked <strong className="text-red-400">BLOQUÉ</strong></span>
<span className="text-slate-400">IVR &gt; <span className="text-red-400 font-mono">{ivGateExtreme}%</span> Long naked <strong className="text-red-400">BLOCKED</strong></span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-orange-500" />
<span className="text-slate-400">Skew &gt; <span className="text-orange-400 font-mono">{ivGateSkew}pts</span> Puts chers (WARN)</span>
<span className="text-slate-400">Skew &gt; <span className="text-orange-400 font-mono">{ivGateSkew}pts</span> Expensive puts (WARN)</span>
</div>
</div>
</div>
@@ -1257,12 +1257,12 @@ export default function Config() {
onClick={() => saveOptionsGate(
{ iv_gate_enabled: ivGateEnabled, iv_gate_ivr_high: ivGateHigh,
iv_gate_ivr_extreme: ivGateExtreme, iv_gate_skew_threshold: ivGateSkew },
{ onSuccess: () => { setSavedMsg('IV Gate sauvegardé'); setTimeout(() => setSavedMsg(''), 2000) } }
{ onSuccess: () => { setSavedMsg('IV Gate saved'); setTimeout(() => setSavedMsg(''), 2000) } }
)}
disabled={savingOptionsGate}
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
<Save className="w-4 h-4" />
{savingOptionsGate ? 'Sauvegarde...' : 'Sauvegarder le gate'}
{savingOptionsGate ? 'Saving...' : 'Save gate'}
</button>
</div>
</div>
@@ -1270,16 +1270,16 @@ export default function Config() {
{/* ── Paramètres de sortie ── */}
<div className="card">
<h2 className="text-base font-bold text-white flex items-center gap-2 mb-1">
<Lock className="w-4 h-4 text-amber-400" /> Paramètres de sortie des trades
<Lock className="w-4 h-4 text-amber-400" /> Trade Exit Settings
</h2>
<p className="text-xs text-slate-500 mb-5">
Valeurs par défaut pour les objectifs et stop-loss. Chaque trade peut avoir ses propres seuils (Journal Ouverts).
Default values for targets and stop-losses. Each trade can have its own thresholds (Journal Open).
</p>
<div className="grid grid-cols-2 gap-6 mb-4">
<div>
<label className="text-xs text-slate-500 mb-2 block">
Objectif de gain : <span className="text-emerald-400 font-mono font-bold">+{exitTarget}%</span>
<span className="text-slate-600 ml-1">(P&L sous-jacent)</span>
Profit target: <span className="text-emerald-400 font-mono font-bold">+{exitTarget}%</span>
<span className="text-slate-600 ml-1">(underlying P&L)</span>
</label>
<input type="range" min="5" max="150" step="5"
value={exitTarget}
@@ -1291,8 +1291,8 @@ export default function Config() {
</div>
<div>
<label className="text-xs text-slate-500 mb-2 block">
Stop-loss : <span className="text-red-400 font-mono font-bold">{exitStop}%</span>
<span className="text-slate-600 ml-1">(P&L sous-jacent)</span>
Stop-loss: <span className="text-red-400 font-mono font-bold">{exitStop}%</span>
<span className="text-slate-600 ml-1">(underlying P&L)</span>
</label>
<input type="range" min="-90" max="-5" step="5"
value={exitStop}
@@ -1305,11 +1305,11 @@ export default function Config() {
</div>
<div className="grid grid-cols-2 gap-6 mb-5">
<div>
<label className="text-xs text-slate-500 mb-2 block">Mode signal IA de retournement</label>
<label className="text-xs text-slate-500 mb-2 block">AI reversal signal mode</label>
<div className="flex gap-2">
{[
{ value: 'badge_only', label: '🔔 Badge uniquement' },
{ value: 'auto', label: '⚡ Auto (futur)' },
{ value: 'badge_only', label: '🔔 Badge only' },
{ value: 'auto', label: '⚡ Auto (future)' },
].map(opt => (
<button key={opt.value} onClick={() => setExitSignalMode(opt.value)}
className={clsx('flex-1 py-2 rounded text-xs border transition-all', {
@@ -1321,19 +1321,19 @@ export default function Config() {
))}
</div>
<div className="text-[10px] text-slate-600 mt-1">
Badge only : alerte visible, vous confirmez manuellement. Auto : fermeture proposée.
Badge only: visible alert, you confirm manually. Auto: closure suggested.
</div>
</div>
<div>
<label className="text-xs text-slate-500 mb-2 block">
Seuil retournement signal : <span className="text-blue-400 font-mono font-bold">{exitSignalThreshold}pts</span>
Signal reversal threshold: <span className="text-blue-400 font-mono font-bold">{exitSignalThreshold}pts</span>
</label>
<input type="range" min="10" max="60" step="5"
value={exitSignalThreshold}
onChange={e => setExitSignalThreshold(parseFloat(e.target.value))}
className="w-full accent-blue-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>10pts (sensible)</span><span>60pts (tolérant)</span>
<span>10pts (sensitive)</span><span>60pts (tolerant)</span>
</div>
</div>
</div>
@@ -1341,12 +1341,12 @@ export default function Config() {
onClick={() => saveExitDefaults(
{ target_pct: exitTarget, stop_loss_pct: exitStop,
signal_reversal_mode: exitSignalMode, signal_reversal_threshold: exitSignalThreshold },
{ onSuccess: () => { setSavedMsg('Paramètres de sortie sauvegardés'); setTimeout(() => setSavedMsg(''), 2000) } }
{ onSuccess: () => { setSavedMsg('Exit settings saved'); setTimeout(() => setSavedMsg(''), 2000) } }
)}
disabled={savingExitDefaults}
className="flex items-center gap-1.5 bg-amber-600 hover:bg-amber-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
<Save className="w-4 h-4" />
{savingExitDefaults ? 'Sauvegarde...' : 'Sauvegarder les seuils'}
{savingExitDefaults ? 'Saving...' : 'Save thresholds'}
</button>
</div>
@@ -1354,7 +1354,7 @@ export default function Config() {
<div className="card border-purple-700/30 bg-purple-900/5">
<div className="flex items-center justify-between mb-1">
<h2 className="text-base font-bold text-white flex items-center gap-2">
<TrendingUp className="w-4 h-4 text-purple-400" /> Indicateurs techniques du sous-jacent
<TrendingUp className="w-4 h-4 text-purple-400" /> Underlying Technical Indicators
</h2>
<button
onClick={() => setTechEnabled(!techEnabled)}
@@ -1362,23 +1362,23 @@ export default function Config() {
'bg-purple-600 border-purple-500 text-white': techEnabled,
'bg-dark-700 border-slate-700 text-slate-400': !techEnabled,
})}>
{techEnabled ? '✓ Activé' : '○ Désactivé'}
{techEnabled ? '✓ Enabled' : '○ Disabled'}
</button>
</div>
<p className="text-xs text-slate-500 mb-4">
Injecte RSI, MA, Bollinger et ATR dans le contexte IA à chaque cycle. Les périodes sont calibrées automatiquement selon l'horizon de l'option analysée.
Injects RSI, MA, Bollinger and ATR into the AI context at each cycle. Periods are automatically calibrated based on the option's horizon.
</p>
<div className={clsx('space-y-4 transition-opacity', !techEnabled && 'opacity-40 pointer-events-none')}>
{/* Indicateurs à activer */}
<div>
<label className="text-xs text-slate-400 font-medium block mb-2">Indicateurs actifs</label>
<label className="text-xs text-slate-400 font-medium block mb-2">Active indicators</label>
<div className="flex flex-wrap gap-2">
{[
{ key: 'rsi', label: 'RSI', desc: 'Momentum / surachat-survente' },
{ key: 'ma', label: 'MA fast/slow', desc: 'Tendance & golden/death cross' },
{ key: 'bollinger', label: 'Bollinger', desc: 'Position dans les bandes de vol' },
{ key: 'atr', label: 'ATR', desc: 'Volatilité réalisée récente' },
{ key: 'rsi', label: 'RSI', desc: 'Momentum / overbought-oversold' },
{ key: 'ma', label: 'MA fast/slow', desc: 'Trend & golden/death cross' },
{ key: 'bollinger', label: 'Bollinger', desc: 'Position in vol bands' },
{ key: 'atr', label: 'ATR', desc: 'Recent realized volatility' },
].map(({ key, label, desc }) => {
const active = techList.includes(key)
return (
@@ -1397,13 +1397,13 @@ export default function Config() {
)
})}
</div>
<p className="text-[10px] text-slate-600 mt-1.5">Survolez pour voir la description de chaque indicateur.</p>
<p className="text-[10px] text-slate-600 mt-1.5">Hover to see each indicator's description.</p>
</div>
{/* Calibration auto */}
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-300 font-medium">Calibration automatique selon horizon</p>
<p className="text-xs text-slate-300 font-medium">Auto-calibration by horizon</p>
<p className="text-[10px] text-slate-500">30j RSI14, MA20/50 | 90j RSI21, MA50/100 | &gt;90j RSI28, MA100/200</p>
</div>
<button
@@ -1412,7 +1412,7 @@ export default function Config() {
'bg-purple-600 border-purple-500 text-white': techAutoCalibrate,
'bg-dark-700 border-slate-700 text-slate-400': !techAutoCalibrate,
})}>
{techAutoCalibrate ? '✓ Auto' : '○ Fixe'}
{techAutoCalibrate ? '✓ Auto' : '○ Fixed'}
</button>
</div>
</div>
@@ -1420,12 +1420,12 @@ export default function Config() {
<button
onClick={() => saveTechIndicators(
{ tech_indicators_enabled: techEnabled, tech_indicators_list: techList.join(','), tech_indicators_auto_calibrate: techAutoCalibrate },
{ onSuccess: () => { setSavedMsg('Indicateurs techniques sauvegardés'); setTimeout(() => setSavedMsg(''), 2000) } }
{ onSuccess: () => { setSavedMsg('Technical indicators saved'); setTimeout(() => setSavedMsg(''), 2000) } }
)}
disabled={savingTechIndicators}
className="mt-4 flex items-center gap-1.5 bg-purple-700 hover:bg-purple-600 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
<Save className="w-4 h-4" />
{savingTechIndicators ? 'Sauvegarde...' : 'Sauvegarder les indicateurs'}
{savingTechIndicators ? 'Saving...' : 'Save indicators'}
</button>
</div>
</div>

View File

@@ -19,10 +19,10 @@ import {
import { scoreColor } from '../components/TradeIdeas'
const riskGauge = (score: number) => {
if (score < 25) return { color: 'text-emerald-400', bg: 'bg-emerald-500', label: 'FAIBLE' }
if (score < 50) return { color: 'text-yellow-400', bg: 'bg-yellow-500', label: 'MODÉRÉ' }
if (score < 75) return { color: 'text-orange-400', bg: 'bg-orange-500', label: 'ÉLEVÉ' }
return { color: 'text-red-400', bg: 'bg-red-500', label: 'EXTRÊME' }
if (score < 25) return { color: 'text-emerald-400', bg: 'bg-emerald-500', label: 'LOW' }
if (score < 50) return { color: 'text-yellow-400', bg: 'bg-yellow-500', label: 'MODERATE' }
if (score < 75) return { color: 'text-orange-400', bg: 'bg-orange-500', label: 'HIGH' }
return { color: 'text-red-400', bg: 'bg-red-500', label: 'EXTREME' }
}
const assetEmoji: Record<string, string> = {
@@ -37,9 +37,9 @@ const ASSET_COLORS: Record<string, string> = {
}
const REGIME_LABELS: Record<string, string> = {
goldilocks: 'Goldilocks', desinflation: 'Désinflation', stagflation: 'Stagflation',
recession: 'Récession', crise_liquidite: 'Crise liq.', reflation: 'Reflation',
soft_landing: 'Soft Landing', inflation_shock: 'Infl. Choc', incertain: 'Incertain',
goldilocks: 'Goldilocks', desinflation: 'Disinflation', stagflation: 'Stagflation',
recession: 'Recession', crise_liquidite: 'Liq. Crisis', reflation: 'Reflation',
soft_landing: 'Soft Landing', inflation_shock: 'Infl. Shock', incertain: 'Uncertain',
}
function ViewToggle({ value, onChange }: { value: 'simulated' | 'portfolio'; onChange: (v: 'simulated' | 'portfolio') => void }) {
@@ -51,7 +51,7 @@ function ViewToggle({ value, onChange }: { value: 'simulated' | 'portfolio'; onC
<button onClick={click('simulated')}
className={clsx('px-1.5 py-0.5 rounded transition-colors',
value === 'simulated' ? 'bg-blue-600 text-white' : 'text-slate-600 hover:text-slate-400')}>
Simulé
Simulated
</button>
<button onClick={click('portfolio')}
className={clsx('px-1.5 py-0.5 rounded transition-colors',
@@ -195,7 +195,7 @@ export default function Dashboard() {
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold text-white">Cockpit GeoOptions</h1>
<h1 className="text-xl font-bold text-white">GeoOptions Cockpit</h1>
<p className="text-xs text-slate-500 mt-0.5">
{format(new Date(), "EEEE d MMMM yyyy · HH:mm", { locale: fr })}
</p>
@@ -235,10 +235,10 @@ export default function Dashboard() {
<div className="card col-span-1">
<div className="flex items-center justify-between mb-2">
<div className="section-title flex items-center gap-1 mb-0">
<Globe className="w-3 h-3" /> Risque Géopolitique
<Globe className="w-3 h-3" /> Geopolitical Risk
</div>
<Link to="/geo" className="flex items-center gap-0.5 text-[10px] text-slate-600 hover:text-slate-300 transition-colors">
News géo <ArrowUpRight className="w-2.5 h-2.5" />
Geo news <ArrowUpRight className="w-2.5 h-2.5" />
</Link>
</div>
{riskLoading ? (
@@ -259,12 +259,12 @@ export default function Dashboard() {
))}
</div>
</>
) : <div className="text-slate-500 text-xs">Backend requis</div>}
) : <div className="text-slate-500 text-xs">Backend required</div>}
</div>
{/* Radar */}
<div className="card col-span-1">
<div className="section-title">Cartographie risques</div>
<div className="section-title">Risk Map</div>
{radarData.length > 0 ? (
<ResponsiveContainer width="100%" height={160}>
<RadarChart data={radarData}>
@@ -273,18 +273,18 @@ export default function Dashboard() {
<Radar dataKey="score" stroke="#3b82f6" fill="#3b82f6" fillOpacity={0.25} />
</RadarChart>
</ResponsiveContainer>
) : <div className="h-40 flex items-center justify-center text-slate-600 text-xs">Chargement...</div>}
) : <div className="h-40 flex items-center justify-center text-slate-600 text-xs">Loading...</div>}
</div>
{/* Patterns — AI scores */}
<div className="card col-span-1 overflow-y-auto max-h-64">
<div className="section-title flex items-center gap-1">
<Brain className="w-3 h-3" /> Scores patterns
<Brain className="w-3 h-3" /> Pattern Scores
{allPatterns.length > 0 && <span className="text-slate-600 text-xs ml-auto font-normal">{allPatterns.length} patterns</span>}
</div>
<div className="space-y-1.5 mt-1">
{allPatterns.length === 0 ? (
<div className="text-slate-600 text-xs">Backend requis</div>
<div className="text-slate-600 text-xs">Backend required</div>
) : (
[...allPatterns]
.sort((a, b) => {
@@ -321,7 +321,7 @@ export default function Dashboard() {
{/* Calendrier */}
<div className="card col-span-1 space-y-3">
<div className="section-title flex items-center gap-1"><Clock className="w-3 h-3" /> Prochains catalyseurs</div>
<div className="section-title flex items-center gap-1"><Clock className="w-3 h-3" /> Upcoming Catalysts</div>
<div className="space-y-1.5">
{calendar?.slice(0, 4).map((ev, i) => (
<div key={i} className="flex items-center gap-2 text-xs">
@@ -397,7 +397,7 @@ export default function Dashboard() {
<div className="grid grid-cols-2 gap-2 flex-shrink-0">
{/* Left: Unrealized */}
<div className="bg-dark-700/40 rounded px-2.5 py-2 border border-slate-700/30">
<div className="text-[9px] text-slate-600 uppercase tracking-wide mb-1">Ouvertes</div>
<div className="text-[9px] text-slate-600 uppercase tracking-wide mb-1">Open</div>
{pnlView === 'simulated' ? (
<>
<div className={clsx('text-xl font-bold font-mono leading-none',
@@ -437,7 +437,7 @@ export default function Dashboard() {
{/* Right: Realized */}
<div className="bg-dark-700/40 rounded px-2.5 py-2 border border-slate-700/30">
<div className="text-[9px] text-slate-600 uppercase tracking-wide mb-1">Réali</div>
<div className="text-[9px] text-slate-600 uppercase tracking-wide mb-1">Realized</div>
{pnlView === 'simulated' ? (
<>
<div className={clsx('text-xl font-bold font-mono leading-none',
@@ -451,10 +451,10 @@ export default function Dashboard() {
closedProfitEur >= 0 ? 'text-emerald-500' : 'text-red-400')}>
{closedCapital > 0
? `${closedProfitEur >= 0 ? '+' : ''}${closedProfitEur.toFixed(0)}`
: closedTrades.length > 0 ? 'sans capital' : ''}
: closedTrades.length > 0 ? 'no capital' : ''}
</div>
<div className="text-[10px] text-slate-500 mt-1">
{closedTrades.length} fermés · <span className="text-emerald-500">{closedWinners}</span>{' '}
{closedTrades.length} closed · <span className="text-emerald-500">{closedWinners}</span>{' '}
<span className="text-red-400">{closedLosers}</span>
</div>
</>
@@ -473,7 +473,7 @@ export default function Dashboard() {
</div>
)}
<div className="text-[10px] text-slate-500 mt-1">
{pf?.closed_positions ?? 0} fermées
{pf?.closed_positions ?? 0} closed
</div>
</>
)}
@@ -483,7 +483,7 @@ export default function Dashboard() {
{/* Total line */}
<div className="mt-1.5 flex items-center justify-between text-[10px] flex-shrink-0">
<span className="text-slate-600">
Investi{isEstimated ? ' ~' : ''} {pnlView === 'simulated'
Invested{isEstimated ? ' ~' : ''} {pnlView === 'simulated'
? (totalCapital > 0 ? `${totalCapital.toFixed(0)}` : '—')
: (pfInvest != null ? `${pfInvest.toFixed(0)}` : '—')}
</span>
@@ -509,7 +509,7 @@ export default function Dashboard() {
const gradId = 'pnlGrad'
return (
<div className="mt-1.5 pt-1.5 border-t border-slate-700/30">
<div className="text-[9px] text-slate-600 mb-0.5">Courbe PnL historique ({snaps.length} pts)</div>
<div className="text-[9px] text-slate-600 mb-0.5">Historical PnL curve ({snaps.length} pts)</div>
<ResponsiveContainer width="100%" height={58}>
<AreaChart data={chartData} margin={{ top: 2, right: 0, left: -38, bottom: 0 }}>
<defs>
@@ -552,14 +552,14 @@ export default function Dashboard() {
return (
<Link to="/risk" className="card flex flex-col h-[288px] overflow-hidden hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-1">
<span className="section-title mb-0 text-[10px]">🛡 Risque</span>
<span className="section-title mb-0 text-[10px]">🛡 Risk</span>
<div className="flex items-center gap-1.5">
<ViewToggle value={riskView} onChange={setRiskViewPersist} />
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
</div>
<div className={clsx('text-lg font-bold mt-0.5', alertCount > 0 ? 'text-red-400' : 'text-emerald-400')}>
{alertCount > 0 ? `${alertCount} alerte${alertCount > 1 ? 's' : ''}` : 'OK'}
{alertCount > 0 ? `${alertCount} alert${alertCount > 1 ? 's' : ''}` : 'OK'}
</div>
{sortedClasses.length > 0 && (
<div className="mt-2 space-y-1">
@@ -568,11 +568,11 @@ export default function Dashboard() {
<span className="text-[9px] text-slate-500 w-12 truncate capitalize">{cls}</span>
<div className="flex-1 h-2 bg-dark-700 rounded-full overflow-hidden flex">
{data.bullish > 0 && (
<div className="h-full rounded-l-full" title={`${data.bullish} haussier`}
<div className="h-full rounded-l-full" title={`${data.bullish} bullish`}
style={{ width: `${(data.bullish / openCount) * 100}%`, background: ASSET_COLORS[cls] ?? '#3b82f6', opacity: 0.9 }} />
)}
{data.bearish > 0 && (
<div className="h-full rounded-r-full" title={`${data.bearish} baissier`}
<div className="h-full rounded-r-full" title={`${data.bearish} bearish`}
style={{ width: `${(data.bearish / openCount) * 100}%`, background: '#ef4444', opacity: 0.7 }} />
)}
</div>
@@ -580,15 +580,15 @@ export default function Dashboard() {
</div>
))}
<div className="flex items-center gap-1 text-[9px] text-slate-700 pt-0.5">
<span className="inline-block w-2 h-1.5 rounded-sm bg-blue-500/60"></span>haussier
<span className="inline-block w-2 h-1.5 rounded-sm bg-red-500/60 ml-1"></span>baissier
{conflictCount > 0 && <span className="ml-auto text-red-400">{conflictCount} conflit{conflictCount > 1 ? 's' : ''}</span>}
<span className="inline-block w-2 h-1.5 rounded-sm bg-blue-500/60"></span>bullish
<span className="inline-block w-2 h-1.5 rounded-sm bg-red-500/60 ml-1"></span>bearish
{conflictCount > 0 && <span className="ml-auto text-red-400">{conflictCount} conflict{conflictCount > 1 ? 's' : ''}</span>}
</div>
</div>
)}
{sortedClasses.length === 0 && (
<div className="text-[10px] text-slate-600 mt-1">
{openCount > 0 ? `${openCount} positions` : 'Aucune position'}
{openCount > 0 ? `${openCount} positions` : 'No positions'}
</div>
)}
{/* Latest VaR snapshot */}
@@ -629,7 +629,7 @@ export default function Dashboard() {
return (
<Link to="/journal" className="card flex flex-col h-[288px] overflow-hidden hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-2">
<span className="section-title mb-0 text-[10px]">🔄 Derniers Cycles</span>
<span className="section-title mb-0 text-[10px]">🔄 Recent Cycles</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
{/* Column headers */}
@@ -638,7 +638,7 @@ export default function Dashboard() {
<span className="text-[8px] text-slate-700 w-8 shrink-0 text-right">Pat.</span>
<span className="text-[8px] text-slate-700 w-8 shrink-0 text-right">Log.</span>
<span className="text-[8px] text-slate-700 w-8 shrink-0 text-right">Clos</span>
<span className="text-[8px] text-slate-700 flex-1 truncate">Régime</span>
<span className="text-[8px] text-slate-700 flex-1 truncate">Regime</span>
</div>
{runs.length > 0 ? (
<div className="space-y-2.5 flex-1 overflow-hidden">
@@ -668,7 +668,7 @@ export default function Dashboard() {
})}
</div>
) : (
<div className="text-[10px] text-slate-600 mt-1">Aucun cycle enregistré</div>
<div className="text-[10px] text-slate-600 mt-1">No cycles recorded</div>
)}
</Link>
)
@@ -694,38 +694,38 @@ export default function Dashboard() {
key: 'vix', label: 'VIX',
color: (v: number) => v < 18 ? 'text-emerald-400' : v < 25 ? 'text-yellow-400' : 'text-red-400',
fmt: (v: number) => v.toFixed(1),
hint: (v: number) => v < 18 ? '↓ risque bas' : v < 25 ? '↑ alerte' : '↑ crise',
hint: (v: number) => v < 18 ? '↓ low risk' : v < 25 ? '↑ alert' : '↑ crisis',
},
{
key: 'spx_vs_200d', label: 'S&P/200j',
key: 'spx_vs_200d', label: 'S&P/200d',
color: (v: number) => v >= 0 ? 'text-emerald-400' : 'text-red-400',
fmt: (v: number) => `${v >= 0 ? '+' : ''}${v.toFixed(1)}%`,
hint: (v: number) => v >= 5 ? 'bull fort' : v >= 0 ? 'bull mod.' : 'bear',
hint: (v: number) => v >= 5 ? 'strong bull' : v >= 0 ? 'mod. bull' : 'bear',
},
{
key: 'slope_10y3m', label: 'Pente 10Y-3M',
key: 'slope_10y3m', label: 'Slope 10Y-3M',
color: (v: number) => v >= 0.5 ? 'text-emerald-400' : v >= 0 ? 'text-yellow-400' : 'text-red-400',
fmt: (v: number) => `${v >= 0 ? '+' : ''}${v.toFixed(2)}`,
hint: (v: number) => v >= 0.5 ? 'normale' : v >= 0 ? 'plate' : 'inversée',
hint: (v: number) => v >= 0.5 ? 'normal' : v >= 0 ? 'flat' : 'inverted',
},
{
key: 'copper', label: 'Cuivre',
key: 'copper', label: 'Copper',
color: (v: number, chg: number) => chg >= 0 ? 'text-emerald-400' : 'text-red-400',
fmt: (v: number, chg: number) => `${chg >= 0 ? '+' : ''}${chg.toFixed(2)}%`,
hint: (v: number, chg: number) => chg >= 0 ? '↑ croissance' : '↓ ralentis.',
hint: (v: number, chg: number) => chg >= 0 ? '↑ growth' : '↓ slowing',
},
{
key: 'gold', label: 'Or',
key: 'gold', label: 'Gold',
color: (v: number, chg: number) => chg >= 1 ? 'text-amber-400' : chg <= -1 ? 'text-emerald-400' : 'text-slate-400',
fmt: (v: number, chg: number) => `${chg >= 0 ? '+' : ''}${chg.toFixed(2)}%`,
hint: (v: number, chg: number) => chg >= 1 ? '↑ refuge' : chg <= -1 ? '↓ risk-on' : '≈ neutre',
hint: (v: number, chg: number) => chg >= 1 ? '↑ safe haven' : chg <= -1 ? '↓ risk-on' : '≈ neutral',
},
]
return (
<Link to="/macro" className="card flex flex-col h-[288px] overflow-hidden hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-1">
<span className="section-title mb-0 text-[10px]">🌐 Régime Macro</span>
<span className="section-title mb-0 text-[10px]">🌐 Macro Regime</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
<div className={clsx('text-base font-bold mt-1', colorClass)}>
@@ -834,7 +834,7 @@ export default function Dashboard() {
})}
</div>
) : (
<div className="text-[10px] text-slate-600 mt-1">Aucun trade loggé</div>
<div className="text-[10px] text-slate-600 mt-1">No trades logged</div>
)}
</Link>
)
@@ -849,7 +849,7 @@ export default function Dashboard() {
return (
<Link to="/journal" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-1">
<span className="section-title mb-0 text-[10px]">📥 Derniers trades loggés</span>
<span className="section-title mb-0 text-[10px]">📥 Latest Logged Trades</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
{recent.length > 0 ? (
@@ -873,7 +873,7 @@ export default function Dashboard() {
{pnl >= 0 ? '+' : ''}{pnl.toFixed(1)}%
</span>
) : (
<span className="text-slate-700 shrink-0 text-[9px]">en cours</span>
<span className="text-slate-700 shrink-0 text-[9px]">in progress</span>
)}
</div>
{entryDate && (
@@ -884,7 +884,7 @@ export default function Dashboard() {
})}
</div>
) : (
<div className="text-[10px] text-slate-600 mt-1">Aucun trade loggé</div>
<div className="text-[10px] text-slate-600 mt-1">No trades logged</div>
)}
</Link>
)
@@ -899,7 +899,7 @@ export default function Dashboard() {
return (
<Link to="/patterns" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-1">
<span className="section-title mb-0 text-[10px]"> Derniers Patterns ajoutés</span>
<span className="section-title mb-0 text-[10px]"> Latest Patterns Added</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
{recent.length > 0 ? (
@@ -932,7 +932,7 @@ export default function Dashboard() {
})}
</div>
) : (
<div className="text-[10px] text-slate-600 mt-1">Aucun pattern enregistré</div>
<div className="text-[10px] text-slate-600 mt-1">No patterns recorded</div>
)}
</Link>
)
@@ -969,7 +969,7 @@ export default function Dashboard() {
})}
</div>
) : (
<div className="text-[10px] text-slate-600 mt-1">Chargement des news</div>
<div className="text-[10px] text-slate-600 mt-1">Loading news</div>
)}
</Link>
)
@@ -982,7 +982,7 @@ export default function Dashboard() {
<div key={cls} className="card">
<div className="section-title">{assetEmoji[cls]} {cls}</div>
{allQuotes?.[cls]?.map(q => <QuoteRow key={q.symbol} q={q} />) ?? (
<div className="text-xs text-slate-600">Chargement...</div>
<div className="text-xs text-slate-600">Loading...</div>
)}
</div>
))}

View File

@@ -13,16 +13,16 @@ const CATEGORY_COLORS: Record<string, string> = {
}
const CATEGORY_LABELS: Record<string, string> = {
military: '⚔️ Militaire', sanctions: '🚫 Sanctions', elections: '🗳️ Élections',
natural_disaster: '🌪️ Catastrophe', health_crisis: '🏥 Santé',
resource_scarcity: '⚠️ Ressources', trade_war: '🤝 Commerce',
energy: '⚡ Énergie', political_speech: '🎙️ Discours',
financial_crisis: '💸 Finance', general: '📰 Général',
military: '⚔️ Military', sanctions: '🚫 Sanctions', elections: '🗳️ Elections',
natural_disaster: '🌪️ Disaster', health_crisis: '🏥 Health',
resource_scarcity: '⚠️ Resources', trade_war: '🤝 Trade',
energy: '⚡ Energy', political_speech: '🎙️ Speech',
financial_crisis: '💸 Finance', general: '📰 General',
}
const ASSET_LABELS: Record<string, string> = {
energy: '⛽ Énergie', metals: '🥇 Métaux', agriculture: '🌾 Agri',
equities: '📈 Actions', indices: '📊 Indices', forex: '💱 Forex',
energy: '⛽ Energy', metals: '🥇 Metals', agriculture: '🌾 Agri',
equities: '📈 Equities', indices: '📊 Indices', forex: '💱 Forex',
}
function ImpactBar({ value, label }: { value: number; label: string }) {
@@ -90,14 +90,14 @@ function NewsCard({ news }: { news: GeoNews }) {
)
})}
{(news as any).ai_resolution && (
<span className="text-[10px] px-1.5 py-0.5 rounded bg-blue-900/30 text-blue-400">🕊 résolution</span>
<span className="text-[10px] px-1.5 py-0.5 rounded bg-blue-900/30 text-blue-400">🕊 resolution</span>
)}
</div>
)}
<p className="text-xs text-slate-400 leading-relaxed mb-3">{news.summary}</p>
{Object.keys(news.asset_impacts).length > 0 && (
<div className="mb-3">
<div className="text-xs text-slate-600 mb-1">Impact par classe d'actif</div>
<div className="text-xs text-slate-600 mb-1">Impact by asset class</div>
{Object.entries(news.asset_impacts).map(([cls, val]) => (
<ImpactBar key={cls} label={ASSET_LABELS[cls] ?? cls} value={val} />
))}
@@ -114,14 +114,14 @@ function NewsCard({ news }: { news: GeoNews }) {
<a href={news.url} target="_blank" rel="noopener noreferrer"
className="text-xs text-blue-400 hover:text-blue-300 flex items-center gap-1"
onClick={e => e.stopPropagation()}>
<ExternalLink className="w-3 h-3" /> Lire l'article
<ExternalLink className="w-3 h-3" /> Read article
</a>
)}
</>
)}
<div className="flex items-center justify-between mt-2 text-xs text-slate-600">
<span>{news.date?.slice(0, 16)}</span>
<span>{expanded ? '▲ Réduire' : '▼ Détail'}</span>
<span>{expanded ? '▲ Collapse' : '▼ Details'}</span>
</div>
</div>
)
@@ -156,7 +156,7 @@ function PatternRelevanceCard({ p }: { p: any }) {
<div className="text-sm font-semibold text-white line-clamp-1">{p.name}</div>
<div className="flex items-center gap-1 mt-0.5 flex-wrap">
<span className="badge badge-blue text-xs">{p.asset_class}</span>
<span className="text-xs text-slate-500">{p.keyword_hits}/{p.keyword_total} mots-clés</span>
<span className="text-xs text-slate-500">{p.keyword_hits}/{p.keyword_total} keywords</span>
{hasNews && (
<span className="text-xs text-emerald-400 flex items-center gap-0.5">
<Search className="w-2.5 h-2.5" />{p.matching_news.length} news
@@ -170,7 +170,7 @@ function PatternRelevanceCard({ p }: { p: any }) {
: 'bg-emerald-900/30 text-emerald-400 border border-emerald-700/30'
)}>
<Brain className="w-2.5 h-2.5" />
{p.ai_contra_signal ? `contre-signal (${p.ai_alignment > 0 ? '+' : ''}${p.ai_alignment}pts)` : `IA aligné (+${p.ai_alignment}pts)`}
{p.ai_contra_signal ? `counter-signal (${p.ai_alignment > 0 ? '+' : ''}${p.ai_alignment}pts)` : `AI aligned (+${p.ai_alignment}pts)`}
</span>
)}
</div>
@@ -211,7 +211,7 @@ function PatternRelevanceCard({ p }: { p: any }) {
<>
<button onClick={() => setExpanded(!expanded)}
className="text-xs text-slate-600 hover:text-slate-400 mb-2">
{expanded ? '▲ Masquer les news' : `Voir les ${p.matching_news.length} news correspondantes`}
{expanded ? '▲ Hide news' : `See ${p.matching_news.length} matching news`}
</button>
{expanded && (
<div className="space-y-1.5">
@@ -235,7 +235,7 @@ function PatternRelevanceCard({ p }: { p: any }) {
<a href={n.url} target="_blank" rel="noopener noreferrer"
className="text-xs text-blue-400 hover:text-blue-300 flex items-center gap-1 mt-1"
onClick={e => e.stopPropagation()}>
<ExternalLink className="w-2.5 h-2.5" /> Lire
<ExternalLink className="w-2.5 h-2.5" /> Read
</a>
)}
</div>
@@ -246,7 +246,7 @@ function PatternRelevanceCard({ p }: { p: any }) {
)}
{!hasNews && p.relevance === 0 && (
<div className="text-xs text-slate-600">Aucune news correspondante sur la période</div>
<div className="text-xs text-slate-600">No matching news for this period</div>
)}
</div>
)
@@ -256,10 +256,10 @@ type SortMode = 'impact_desc' | 'impact_asc' | 'date_desc' | 'date_asc'
type DateRange = 'all' | 'today' | '7d' | '30d'
const DATE_RANGE_LABELS: Record<DateRange, string> = {
all: 'Toutes', today: "Aujourd'hui", '7d': '7 jours', '30d': '30 jours',
all: 'All', today: 'Today', '7d': '7 days', '30d': '30 days',
}
const SORT_LABELS: Record<SortMode, string> = {
impact_desc: 'Impact ↓', impact_asc: 'Impact ↑', date_desc: 'Récentes ↓', date_asc: 'Anciennes ↑',
impact_desc: 'Impact ↓', impact_asc: 'Impact ↑', date_desc: 'Recent ↓', date_asc: 'Oldest ↑',
}
function isWithinRange(dateStr: string, range: DateRange): boolean {
@@ -304,10 +304,10 @@ export default function GeoRadar() {
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold text-white flex items-center gap-2">
<Globe className="w-5 h-5 text-blue-400" /> Radar Géopolitique
<Globe className="w-5 h-5 text-blue-400" /> Geopolitical Radar
</h1>
<p className="text-xs text-slate-500 mt-0.5">
Flux d'actualités · Correspondance avec les patterns de trading
News feed · Pattern matching for trading signals
</p>
</div>
{riskScore && (
@@ -337,7 +337,7 @@ export default function GeoRadar() {
'bg-blue-600 text-white': tab === t,
'text-slate-400 hover:text-slate-200': tab !== t,
})}>
{t === 'news' ? `📰 Actualités (${news?.length ?? 0})` : `🧩 Patterns (${patternsWithNews}/${totalPatterns} avec news)`}
{t === 'news' ? `📰 News (${news?.length ?? 0})` : `🧩 Patterns (${patternsWithNews}/${totalPatterns} with news)`}
</button>
))}
</div>
@@ -345,7 +345,7 @@ export default function GeoRadar() {
{/* Day selector — only shown on patterns tab */}
{tab === 'patterns' && (
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500">Fenêtre :</span>
<span className="text-xs text-slate-500">Window:</span>
<div className="flex gap-0.5 bg-dark-700 p-0.5 rounded">
{[1, 2, 7, 30].map(d => (
<button key={d} onClick={() => setDays(d)}
@@ -353,11 +353,11 @@ export default function GeoRadar() {
'bg-slate-600 text-white': days === d,
'text-slate-400 hover:text-slate-200': days !== d,
})}>
{d}j
{d}d
</button>
))}
</div>
<span className="text-xs text-slate-600">derniers jours</span>
<span className="text-xs text-slate-600">last days</span>
</div>
)}
</div>
@@ -375,7 +375,7 @@ export default function GeoRadar() {
'bg-blue-600 border-blue-500 text-white': filterCat === cat,
'border-slate-700 text-slate-400 hover:border-slate-500': filterCat !== cat,
})}>
{cat === 'all' ? 'Toutes catégories' : CATEGORY_LABELS[cat] ?? cat}
{cat === 'all' ? 'All categories' : CATEGORY_LABELS[cat] ?? cat}
</button>
))}
</div>
@@ -384,7 +384,7 @@ export default function GeoRadar() {
{/* Sort + date range row */}
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500">Période :</span>
<span className="text-xs text-slate-500">Period:</span>
<div className="flex gap-0.5 bg-dark-700 p-0.5 rounded">
{(['today', '7d', '30d', 'all'] as DateRange[]).map(r => (
<button key={r} onClick={() => setDateRange(r)}
@@ -398,7 +398,7 @@ export default function GeoRadar() {
</div>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500">Tri :</span>
<span className="text-xs text-slate-500">Sort:</span>
<div className="flex gap-0.5 bg-dark-700 p-0.5 rounded">
{(['impact_desc', 'impact_asc', 'date_desc', 'date_asc'] as SortMode[]).map(s => (
<button key={s} onClick={() => setSortMode(s)}
@@ -423,7 +423,7 @@ export default function GeoRadar() {
{filtered.map((n, i) => <NewsCard key={n.id || i} news={n} />)}
{filtered.length === 0 && (
<div className="card col-span-2 text-center py-8 text-slate-500">
Démarrer le backend pour charger les actualités
Start the backend to load news
</div>
)}
</div>
@@ -437,11 +437,11 @@ export default function GeoRadar() {
<div className="flex items-center gap-3 text-xs text-slate-500 bg-dark-700/40 rounded px-3 py-2">
<Search className="w-3.5 h-3.5 shrink-0 text-blue-400" />
<span>
Correspondance entre les mots-clés de chaque pattern et les news des {days} derniers jours.
Ce signal sert à enrichir le contexte donné à l'IA lors du scoring.
Matching each pattern's keywords against news from the last {days} days.
This signal enriches the context sent to the AI during scoring.
{patternsWithNews > 0 && (
<span className="text-emerald-400 ml-1">
{patternsWithNews} patterns ont des news correspondantes.
{patternsWithNews} patterns have matching news.
</span>
)}
</span>
@@ -458,7 +458,7 @@ export default function GeoRadar() {
))}
{(!patternRelevance || patternRelevance.length === 0) && (
<div className="card text-center py-8 text-slate-500">
Démarrer le backend pour calculer la correspondance news-patterns
Start the backend to compute news-pattern matching
</div>
)}
</div>

View File

@@ -9,14 +9,14 @@ import { fr } from 'date-fns/locale'
const SCENARIO_META: Record<string, { label: string; color: string; emoji: string }> = {
goldilocks: { label: 'Goldilocks', color: '#22c55e', emoji: '🌟' },
desinflation: { label: 'Désinflation', color: '#06b6d4', emoji: '❄️' },
desinflation: { label: 'Disinflation', color: '#06b6d4', emoji: '❄️' },
soft_landing: { label: 'Soft Landing', color: '#3b82f6', emoji: '🛬' },
reflation: { label: 'Reflation', color: '#f97316', emoji: '🔥' },
stagflation: { label: 'Stagflation', color: '#eab308', emoji: '⚡' },
inflation_shock: { label: 'Inflation Shock', color: '#ef4444', emoji: '💥' },
recession: { label: 'Récession', color: '#8b5cf6', emoji: '📉' },
crise_liquidite: { label: 'Crise Liquidité', color: '#ec4899', emoji: '🚨' },
incertain: { label: 'Incertain', color: '#64748b', emoji: '❓' },
recession: { label: 'Recession', color: '#8b5cf6', emoji: '📉' },
crise_liquidite: { label: 'Liquidity Crisis', color: '#ec4899', emoji: '🚨' },
incertain: { label: 'Uncertain', color: '#64748b', emoji: '❓' },
}
function ScenarioBadge({ dominant, size = 'sm' }: { dominant: string; size?: 'sm' | 'lg' }) {
@@ -97,7 +97,7 @@ function KellyCell({ patternId, capital = 10000 }: { patternId: string; capital?
<div className="flex flex-col items-end gap-0.5">
<span className={clsx('text-[10px] font-bold font-mono', color)}>{pct?.toFixed(1)}%</span>
<span className="text-[8px] text-slate-600">{eur != null ? `${eur}` : ''}</span>
{adjusted && <span className="text-[8px] text-orange-400" title={adjusted}> ajusté</span>}
{adjusted && <span className="text-[8px] text-orange-400" title={adjusted}> adjusted</span>}
</div>
)
}
@@ -105,10 +105,10 @@ function KellyCell({ patternId, capital = 10000 }: { patternId: string; capital?
// ── Close Trade Modal ─────────────────────────────────────────────────────────
const CLOSE_REASONS = [
{ value: 'target', label: '🎯 Objectif atteint' },
{ value: 'stop_loss', label: '🛑 Stop-loss déclenché' },
{ value: 'signal_reversal', label: '🔄 Signal IA retourné' },
{ value: 'manual', label: '✍️ Fermeture manuelle' },
{ value: 'target', label: '🎯 Target reached' },
{ value: 'stop_loss', label: '🛑 Stop-loss triggered' },
{ value: 'signal_reversal', label: '🔄 AI signal reversed' },
{ value: 'manual', label: '✍️ Manual close' },
]
function CloseTradeModal({ trade, onClose }: { trade: any; onClose: () => void }) {
@@ -142,10 +142,10 @@ function CloseTradeModal({ trade, onClose }: { trade: any; onClose: () => void }
<div className="flex items-center justify-between">
<div>
<div className="text-sm font-bold text-white flex items-center gap-2">
<Lock className="w-4 h-4 text-amber-400" /> Fermer le trade
<Lock className="w-4 h-4 text-amber-400" /> Close trade
</div>
<div className="text-xs text-slate-500 mt-0.5">
{trade.underlying} · {trade.strategy} · entrée {trade.entry_price?.toFixed(2) ?? '—'}
{trade.underlying} · {trade.strategy} · entry {trade.entry_price?.toFixed(2) ?? '—'}
</div>
</div>
<button onClick={onClose} className="text-slate-600 hover:text-slate-400"><X className="w-4 h-4" /></button>
@@ -156,7 +156,7 @@ function CloseTradeModal({ trade, onClose }: { trade: any; onClose: () => void }
trade.alert_type === 'target_reached'
? 'bg-emerald-900/30 border-emerald-700/40 text-emerald-300'
: 'bg-red-900/30 border-red-700/40 text-red-300')}>
{trade.alert_type === 'target_reached' ? '🎯 Objectif atteint !' : '🛑 Stop-loss atteint'}
{trade.alert_type === 'target_reached' ? '🎯 Target reached!' : '🛑 Stop-loss hit'}
{' — P&L actuel '}
<span className="font-mono">{trade.pnl_pct >= 0 ? '+' : ''}{trade.pnl_pct?.toFixed(2)}%</span>
</div>
@@ -164,24 +164,24 @@ function CloseTradeModal({ trade, onClose }: { trade: any; onClose: () => void }
<div className="space-y-3">
<div>
<label className="block text-xs text-slate-500 mb-1">Prix de clôture</label>
<label className="block text-xs text-slate-500 mb-1">Close price</label>
<input
type="number" step="0.01"
value={closePrice}
onChange={e => setClosePrice(e.target.value)}
className="w-full bg-dark-700 border border-slate-600/50 rounded px-3 py-1.5 text-sm text-white font-mono"
placeholder="Prix actuel du sous-jacent"
placeholder="Current underlying price"
/>
{pnlPreview !== null && (
<div className={clsx('text-xs mt-1 font-mono font-bold',
pnlPreview >= 0 ? 'text-emerald-400' : 'text-red-400')}>
P&L réalisé : {pnlPreview >= 0 ? '+' : ''}{pnlPreview}%
Realized P&L: {pnlPreview >= 0 ? '+' : ''}{pnlPreview}%
</div>
)}
</div>
<div>
<label className="block text-xs text-slate-500 mb-1">Motif de fermeture</label>
<label className="block text-xs text-slate-500 mb-1">Close reason</label>
<select
value={reason}
onChange={e => setReason(e.target.value)}
@@ -194,24 +194,24 @@ function CloseTradeModal({ trade, onClose }: { trade: any; onClose: () => void }
</div>
<div>
<label className="block text-xs text-slate-500 mb-1">Note (optionnel)</label>
<label className="block text-xs text-slate-500 mb-1">Note (optional)</label>
<input
type="text"
value={note}
onChange={e => setNote(e.target.value)}
className="w-full bg-dark-700 border border-slate-600/50 rounded px-3 py-1.5 text-sm text-white"
placeholder="Contexte, raison…"
placeholder="Context, reason…"
maxLength={200}
/>
</div>
</div>
{isError && <div className="text-xs text-red-400">Erreur lors de la fermeture du trade.</div>}
{isError && <div className="text-xs text-red-400">Error closing trade.</div>}
<div className="flex gap-3 pt-1">
<button onClick={onClose}
className="flex-1 px-4 py-2 rounded border border-slate-700/50 text-slate-400 hover:text-slate-200 text-sm">
Annuler
Cancel
</button>
<button
onClick={handleSubmit}
@@ -219,7 +219,7 @@ function CloseTradeModal({ trade, onClose }: { trade: any; onClose: () => void }
className="flex-1 px-4 py-2 rounded bg-amber-600 hover:bg-amber-500 disabled:opacity-50 text-white text-sm font-semibold flex items-center justify-center gap-2"
>
<Lock className="w-3.5 h-3.5" />
{isPending ? 'Fermeture…' : 'Confirmer la fermeture'}
{isPending ? 'Closing…' : 'Confirm close'}
</button>
</div>
</div>
@@ -230,9 +230,9 @@ function CloseTradeModal({ trade, onClose }: { trade: any; onClose: () => void }
// ── Shared filter bar ────────────────────────────────────────────────────────
const DIRECTIONS = [
{ key: 'all', label: 'Tous' },
{ key: 'bullish', label: '🐂 Haussier' },
{ key: 'bearish', label: '🐻 Baissier' },
{ key: 'all', label: 'All' },
{ key: 'bullish', label: '🐂 Bullish' },
{ key: 'bearish', label: '🐻 Bearish' },
]
function _isBearishStr(strategy: string) {
@@ -314,7 +314,7 @@ function FilterBar({ search, onSearch, assetClass, onAssetClass, direction, onDi
type="text"
value={search}
onChange={e => onSearch(e.target.value)}
placeholder="Ticker, stratégie…"
placeholder="Ticker, strategy…"
className="pl-6 pr-2 py-1 bg-dark-700 border border-slate-700/50 rounded text-xs text-slate-300 placeholder-slate-600 w-36 focus:outline-none focus:border-slate-500"
/>
{search && (
@@ -353,9 +353,9 @@ function FilterBar({ search, onSearch, assetClass, onAssetClass, direction, onDi
{onPnlFilter && (
<div className="flex items-center gap-0.5 bg-dark-700 rounded p-0.5">
{[
{ key: 'all', label: 'Tous' },
{ key: 'winners', label: '✓ Gagnants' },
{ key: 'losers', label: '✗ Perdants' },
{ key: 'all', label: 'All' },
{ key: 'winners', label: '✓ Winners' },
{ key: 'losers', label: '✗ Losers' },
].map(p => (
<button key={p.key} onClick={() => onPnlFilter(p.key)}
className={clsx('px-2 py-1 rounded text-xs transition-colors', {
@@ -398,10 +398,10 @@ function ClosedTradesSection({ days }: { days: number }) {
})
const REASON_META: Record<string, { label: string; color: string }> = {
target: { label: '🎯 Objectif', color: 'text-emerald-400' },
target: { label: '🎯 Target', color: 'text-emerald-400' },
stop_loss: { label: '🛑 Stop-loss', color: 'text-red-400' },
signal_reversal: { label: '🔄 Signal IA', color: 'text-blue-400' },
manual: { label: '✍️ Manuel', color: 'text-slate-400' },
signal_reversal: { label: '🔄 AI Signal', color: 'text-blue-400' },
manual: { label: '✍️ Manual', color: 'text-slate-400' },
}
return (
@@ -410,10 +410,10 @@ function ClosedTradesSection({ days }: { days: number }) {
{allTrades.length > 0 && (
<div className="grid grid-cols-4 gap-3">
{[
{ label: 'Trades fermés', value: stats.total ?? 0, sub: '', color: 'text-white' },
{ label: 'Taux de succès', value: stats.win_rate != null ? `${stats.win_rate}%` : '—', sub: '', color: stats.win_rate >= 50 ? 'text-emerald-400' : 'text-red-400' },
{ label: 'P&L moyen', value: stats.avg_pnl != null ? `${stats.avg_pnl >= 0 ? '+' : ''}${stats.avg_pnl?.toFixed(1)}%` : '—', sub: '', color: (stats.avg_pnl ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400' },
{ label: 'P&L total', value: stats.total_pnl != null ? `${stats.total_pnl >= 0 ? '+' : ''}${stats.total_pnl?.toFixed(1)}%` : '—', sub: `meilleur ${stats.best?.toFixed(1) ?? '—'}%`, color: (stats.total_pnl ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400' },
{ label: 'Closed trades', value: stats.total ?? 0, sub: '', color: 'text-white' },
{ label: 'Win rate', value: stats.win_rate != null ? `${stats.win_rate}%` : '—', sub: '', color: stats.win_rate >= 50 ? 'text-emerald-400' : 'text-red-400' },
{ label: 'Avg P&L', value: stats.avg_pnl != null ? `${stats.avg_pnl >= 0 ? '+' : ''}${stats.avg_pnl?.toFixed(1)}%` : '—', sub: '', color: (stats.avg_pnl ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400' },
{ label: 'Total P&L', value: stats.total_pnl != null ? `${stats.total_pnl >= 0 ? '+' : ''}${stats.total_pnl?.toFixed(1)}%` : '—', sub: `best ${stats.best?.toFixed(1) ?? '—'}%`, color: (stats.total_pnl ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400' },
].map(s => (
<div key={s.label} className="card text-center py-3">
<div className={clsx('text-lg font-bold font-mono', s.color)}>{s.value}</div>
@@ -426,7 +426,7 @@ function ClosedTradesSection({ days }: { days: number }) {
<div className="flex items-center justify-between flex-wrap gap-2">
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide">
{trades.length}/{allTrades.length} trades clôturés · {days} derniers jours
{trades.length}/{allTrades.length} closed trades · last {days} days
</div>
<button onClick={() => refetch()} disabled={isFetching}
className="text-slate-600 hover:text-slate-400 disabled:opacity-40">
@@ -446,11 +446,11 @@ function ClosedTradesSection({ days }: { days: number }) {
) : allTrades.length === 0 ? (
<div className="card text-center py-12 text-slate-600 text-sm">
<Lock className="w-8 h-8 mx-auto mb-2 opacity-20" />
Aucun trade clôturé dans les {days} derniers jours
No closed trades in the last {days} days
</div>
) : trades.length === 0 ? (
<div className="card text-center py-8 text-slate-600 text-sm">
Aucun résultat pour ces filtres
No results for these filters
</div>
) : (
<div className="overflow-x-auto rounded-lg border border-slate-700/40">
@@ -458,15 +458,15 @@ function ClosedTradesSection({ days }: { days: number }) {
<thead>
<tr className="text-slate-600 border-b border-slate-700/40">
<th className="text-left px-3 py-2 font-medium">Pattern</th>
<th className="text-left px-3 py-2 font-medium">Stratégie</th>
<th className="text-left px-3 py-2 font-medium">Strategy</th>
<th className="text-left px-3 py-2 font-medium">Ticker</th>
<th className="text-right px-3 py-2 font-medium">Entrée</th>
<th className="text-right px-3 py-2 font-medium">Sortie</th>
<th className="text-right px-3 py-2 font-medium">Date entrée</th>
<th className="text-right px-3 py-2 font-medium">Date sortie</th>
<th className="text-right px-3 py-2 font-medium">Durée</th>
<th className="text-right px-3 py-2 font-medium">P&L réalisé</th>
<th className="text-left px-3 py-2 font-medium">Motif</th>
<th className="text-right px-3 py-2 font-medium">Entry</th>
<th className="text-right px-3 py-2 font-medium">Exit</th>
<th className="text-right px-3 py-2 font-medium">Entry date</th>
<th className="text-right px-3 py-2 font-medium">Close date</th>
<th className="text-right px-3 py-2 font-medium">Duration</th>
<th className="text-right px-3 py-2 font-medium">Realized P&L</th>
<th className="text-left px-3 py-2 font-medium">Reason</th>
<th className="text-left px-3 py-2 font-medium">Note</th>
<th className="px-2 py-2 w-8"></th>
</tr>
@@ -523,7 +523,7 @@ function ClosedTradesSection({ days }: { days: number }) {
disabled={deleting}
className="text-[10px] font-semibold text-red-400 hover:text-red-300 bg-red-900/30 border border-red-700/40 rounded px-1.5 py-0.5 disabled:opacity-40"
>
{deleting ? '…' : 'Supprimer'}
{deleting ? '…' : 'Delete'}
</button>
<button onClick={() => setConfirmDeleteId(null)} className="text-slate-600 hover:text-slate-400">
<X className="w-3 h-3" />
@@ -533,7 +533,7 @@ function ClosedTradesSection({ days }: { days: number }) {
<button
onClick={() => setConfirmDeleteId(t.id)}
className="p-1 rounded text-slate-700 hover:text-red-400 hover:bg-red-900/20 transition-colors"
title="Supprimer ce trade"
title="Delete this trade"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
@@ -618,27 +618,27 @@ function IBKRTicket({ strategy, underlying, strikeGuidance, expiryDays, entryPri
return (
<div className="bg-blue-950/20 border border-blue-700/30 rounded p-3 space-y-2.5">
<div className="text-[10px] font-bold text-blue-300 uppercase tracking-wide flex items-center gap-1.5">
<Terminal className="w-3 h-3" /> Ticket IBKR
{!entryPrice && <span className="ml-auto text-slate-600 font-normal normal-case">Prix non disponible vérifier dans IBKR</span>}
<Terminal className="w-3 h-3" /> IBKR Ticket
{!entryPrice && <span className="ml-auto text-slate-600 font-normal normal-case">Price unavailable check in IBKR</span>}
</div>
<div className="grid grid-cols-3 gap-3 text-[10px]">
<div>
<div className="text-slate-600 mb-0.5">Sous-jacent</div>
<div className="text-slate-600 mb-0.5">Underlying</div>
<div className="text-white font-mono font-bold text-sm">{underlying}</div>
<div className="text-slate-600 text-[9px]">Security Type: OPT</div>
</div>
<div>
<div className="text-slate-600 mb-0.5">Stratégie</div>
<div className="text-slate-600 mb-0.5">Strategy</div>
<div className="text-slate-200 font-semibold">{strategy}</div>
{entryPrice && <div className="text-slate-600 text-[9px]">Sous-jacent: ${entryPrice.toFixed(2)}</div>}
{entryPrice && <div className="text-slate-600 text-[9px]">Underlying: ${entryPrice.toFixed(2)}</div>}
</div>
<div>
<div className="text-slate-600 mb-0.5">Expiration</div>
<div className="text-slate-600 mb-0.5">Expiry</div>
<div className="text-slate-200 font-mono">
{expiryDate ? format(expiryDate, 'd MMM yyyy', { locale: fr }) : expiryDays ? `~${expiryDays}j` : '—'}
{expiryDate ? format(expiryDate, 'd MMM yyyy', { locale: fr }) : expiryDays ? `~${expiryDays}d` : '—'}
</div>
<div className="text-slate-600 text-[9px]">{expiryDays ? `${expiryDays}j DTE` : ''}</div>
<div className="text-slate-600 text-[9px]">{expiryDays ? `${expiryDays}d DTE` : ''}</div>
</div>
</div>
@@ -646,7 +646,7 @@ function IBKRTicket({ strategy, underlying, strikeGuidance, expiryDays, entryPri
{legs.map((leg, i) => (
<div key={i} className="flex items-center gap-2 bg-slate-900/70 rounded px-2.5 py-1.5 font-mono text-[10px]">
<span className={clsx('w-8 font-bold', leg.action === 'BUY' ? 'text-emerald-400' : 'text-red-400')}>{leg.action}</span>
<span className="text-slate-400">1 contrat</span>
<span className="text-slate-400">1 contract</span>
<span className={clsx('font-bold w-10 text-center', leg.type === 'CALL' ? 'text-blue-300' : leg.type === 'PUT' ? 'text-orange-300' : 'text-slate-300')}>{leg.type}</span>
<span className="text-slate-600">Strike</span>
<span className="text-yellow-300 font-bold">{fmtStrike(leg.strike)}</span>
@@ -659,17 +659,17 @@ function IBKRTicket({ strategy, underlying, strikeGuidance, expiryDays, entryPri
<div className="grid grid-cols-3 gap-3 text-[10px]">
<div>
<div className="text-slate-600 mb-0.5">Type d'ordre</div>
<div className="text-slate-600 mb-0.5">Order type</div>
<div className="text-slate-300 font-semibold">LIMIT</div>
<div className="text-slate-600 text-[9px]">Prix = prime dans IBKR</div>
<div className="text-slate-600 text-[9px]">Price = premium in IBKR</div>
</div>
<div>
<div className="text-slate-600 mb-0.5">Budget max</div>
<div className="text-slate-600 mb-0.5">Max budget</div>
<div className="text-slate-300 font-mono">{maxLoss ? `${Math.abs(maxLoss).toFixed(0)}` : '~1 000€'}</div>
<div className="text-slate-600 text-[9px]">Perte max estimée</div>
<div className="text-slate-600 text-[9px]">Estimated max loss</div>
</div>
<div>
<div className="text-slate-600 mb-0.5">Cible</div>
<div className="text-slate-600 mb-0.5">Target</div>
<div className="text-emerald-400 font-mono">{targetGain ? `+${targetGain.toFixed(0)}` : '—'}</div>
</div>
</div>
@@ -685,7 +685,7 @@ function MacroHistorySection({ days }: { days: number }) {
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide">
{history.length} snapshots · {days} derniers jours
{history.length} snapshots · last {days} days
</div>
<button onClick={() => refetch()} disabled={isFetching}
className="text-slate-600 hover:text-slate-400 disabled:opacity-40">
@@ -698,7 +698,7 @@ function MacroHistorySection({ days }: { days: number }) {
) : history.length === 0 ? (
<div className="card text-center py-10 text-slate-600 text-sm">
<Activity className="w-8 h-8 mx-auto mb-2 opacity-20" />
Aucun snapshot — cliquer "Ré-analyser" dans Régime Macro pour commencer à logger
No snapshots click "Re-analyze" in Macro Regime to start logging
</div>
) : (
<div className="space-y-2">
@@ -714,7 +714,7 @@ function MacroHistorySection({ days }: { days: number }) {
<div className="flex items-center gap-3 mb-2">
{isTransition && (
<span className="text-[10px] bg-amber-900/30 text-amber-400 border border-amber-700/30 rounded px-1.5 py-0.5 shrink-0">
Transition
Transition (regime change)
</span>
)}
<ScenarioBadge dominant={entry.dominant} size="lg" />
@@ -800,7 +800,7 @@ function PostmortemPanel({ tradeId, onClose }: { tradeId: number; onClose: () =>
{/* Score history sparkline */}
{score_history?.length > 0 && (
<div className="flex items-center gap-3">
<span className="text-slate-600 shrink-0">Historique :</span>
<span className="text-slate-600 shrink-0">History:</span>
<div className="flex items-center gap-1.5 flex-wrap">
{[...score_history].reverse().map((h: any, i: number) => (
<div key={i} className="flex flex-col items-center gap-0.5">
@@ -817,7 +817,7 @@ function PostmortemPanel({ tradeId, onClose }: { tradeId: number; onClose: () =>
</div>
{score_history.length > 0 && (
<span className="text-slate-600 ml-auto shrink-0">
{score_history[0]?.macro_dominant ?? ''} — géo {score_history[0]?.geo_score ?? '?'}
{score_history[0]?.macro_dominant ?? ''} geo {score_history[0]?.geo_score ?? '?'}
</span>
)}
</div>
@@ -830,7 +830,7 @@ function PostmortemPanel({ tradeId, onClose }: { tradeId: number; onClose: () =>
onClick={() => setShowScoring(v => !v)}
>
{showScoring ? <ChevronUp className="w-3.5 h-3.5" /> : <ChevronDown className="w-3.5 h-3.5" />}
Pourquoi noté {scOut.score ?? '?'}/100
Why scored {scOut.score ?? '?'}/100
{scOut.key_catalyst && <span className="text-slate-600 font-normal ml-1">· {scOut.key_catalyst}</span>}
</button>
{showScoring && (
@@ -838,13 +838,13 @@ function PostmortemPanel({ tradeId, onClose }: { tradeId: number; onClose: () =>
{/* Regime / bias */}
<div className="flex flex-wrap gap-3 text-[11px] text-slate-500">
{sc?.macro_dominant && (
<span>Régime : <ScenarioBadge dominant={sc.macro_dominant} /></span>
<span>Regime: <ScenarioBadge dominant={sc.macro_dominant} /></span>
)}
{scCtx.asset_bias && (
<span>Biais asset : <span className="text-slate-300">{scCtx.asset_bias}</span></span>
<span>Asset bias: <span className="text-slate-300">{scCtx.asset_bias}</span></span>
)}
{sc?.geo_score != null && (
<span>Géo : <span className="font-mono text-slate-300">{sc.geo_score}/100</span></span>
<span>Geo: <span className="font-mono text-slate-300">{sc.geo_score}/100</span></span>
)}
</div>
{/* Buckets */}
@@ -885,7 +885,7 @@ function PostmortemPanel({ tradeId, onClose }: { tradeId: number; onClose: () =>
onClick={() => setShowSuggestion(v => !v)}
>
{showSuggestion ? <ChevronUp className="w-3.5 h-3.5" /> : <ChevronDown className="w-3.5 h-3.5" />}
Pourquoi ce pattern a été créé
Why this pattern was created
</button>
{showSuggestion && (
<div className="pl-5 space-y-1.5 text-[11px] text-slate-400">
@@ -893,8 +893,8 @@ function PostmortemPanel({ tradeId, onClose }: { tradeId: number; onClose: () =>
{sgOut.description && <p className="text-slate-500">{sgOut.description}</p>}
{sg.macro_dominant && (
<div className="flex gap-3 mt-1">
<span>Créé sous : <ScenarioBadge dominant={sg.macro_dominant} /></span>
{sg.geo_score != null && <span>Géo : <span className="font-mono text-slate-300">{sg.geo_score}/100</span></span>}
<span>Created under: <ScenarioBadge dominant={sg.macro_dominant} /></span>
{sg.geo_score != null && <span>Geo: <span className="font-mono text-slate-300">{sg.geo_score}/100</span></span>}
</div>
)}
</div>
@@ -911,22 +911,22 @@ function PostmortemPanel({ tradeId, onClose }: { tradeId: number; onClose: () =>
className="flex items-center gap-2 px-3 py-1.5 rounded bg-blue-900/30 border border-blue-700/40 text-blue-300 hover:bg-blue-900/50 disabled:opacity-50 text-xs font-medium"
>
<Brain className={clsx('w-3.5 h-3.5', analyzing && 'animate-pulse')} />
{analyzing ? 'Analyse GPT-4o en cours' : 'Analyser avec GPT-4o'}
{analyzing ? 'GPT-4o analysis running…' : 'Analyze with GPT-4o'}
</button>
) : (
<div className="space-y-3">
<div className="flex items-center gap-2 text-blue-400 font-semibold">
<Brain className="w-3.5 h-3.5" />
Analyse GPT-4o
GPT-4o Analysis
</div>
{[
{ key: 'diagnostic', label: 'Diagnostic', color: 'text-slate-200' },
{ key: 'what_worked', label: 'Ce qui a marché', color: 'text-emerald-400' },
{ key: 'what_missed', label: 'Ce qui a manqué', color: 'text-red-400' },
{ key: 'regime_alignment', label: 'Alignement régime', color: 'text-yellow-400' },
{ key: 'what_worked', label: 'What worked', color: 'text-emerald-400' },
{ key: 'what_missed', label: 'What was missed', color: 'text-red-400' },
{ key: 'regime_alignment', label: 'Regime alignment', color: 'text-yellow-400' },
{ key: 'contra_assessment', label: 'Contra-signals', color: 'text-orange-400' },
{ key: 'lesson', label: 'Leçon', color: 'text-blue-300' },
{ key: 'next_cycle', label: 'Prochain cycle', color: 'text-purple-400' },
{ key: 'lesson', label: 'Lesson', color: 'text-blue-300' },
{ key: 'next_cycle', label: 'Next cycle', color: 'text-purple-400' },
].map(({ key, label, color }) =>
analysis[key] ? (
<div key={key}>
@@ -974,10 +974,10 @@ function TradeMtmSection({ days }: { days: number }) {
<div className="space-y-3">
<div className="flex items-center justify-between gap-4 flex-wrap">
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide">
{trades.length}/{allTrades.length} trades · {withPnl.length} pricés
{trades.length}/{allTrades.length} trades · {withPnl.length} priced
{avgPnl != null && (
<span className={clsx('ml-2 font-bold', avgPnl >= 0 ? 'text-emerald-400' : 'text-red-400')}>
· moy {avgPnl >= 0 ? '+' : ''}{avgPnl.toFixed(1)}%
· avg {avgPnl >= 0 ? '+' : ''}{avgPnl.toFixed(1)}%
</span>
)}
</div>
@@ -1015,8 +1015,8 @@ function TradeMtmSection({ days }: { days: number }) {
<div className="card text-center py-10 text-slate-600 text-sm">
<TrendingUp className="w-8 h-8 mx-auto mb-2 opacity-20" />
{allTrades.length === 0
? 'Aucun trade ouvert scorer des patterns pour commencer le suivi'
: 'Aucun résultat pour ces filtres'}
? 'No open trades — score patterns to start tracking'
: 'No results for these filters'}
</div>
) : (
<div className="overflow-x-auto rounded-lg border border-slate-700/40">
@@ -1024,20 +1024,20 @@ function TradeMtmSection({ days }: { days: number }) {
<thead>
<tr className="text-slate-600 border-b border-slate-700/40">
<th className="text-left px-3 py-2 font-medium">Pattern</th>
<th className="text-left px-3 py-2 font-medium">Profil</th>
<th className="text-left px-3 py-2 font-medium">Stratégie</th>
<th className="text-left px-3 py-2 font-medium">Profile</th>
<th className="text-left px-3 py-2 font-medium">Strategy</th>
<th className="text-right px-3 py-2 font-medium">Strike</th>
<th className="text-right px-3 py-2 font-medium">DTE</th>
<th className="text-left px-3 py-2 font-medium">Ticker</th>
<th className="text-right px-3 py-2 font-medium">Score</th>
<th className="text-right px-3 py-2 font-medium">Trade Score</th>
<th className="text-right px-3 py-2 font-medium">EV nette</th>
<th className="text-right px-3 py-2 font-medium">Gain prévu</th>
<th className="text-right px-3 py-2 font-medium">Net EV</th>
<th className="text-right px-3 py-2 font-medium">Expected move</th>
<th className="text-right px-3 py-2 font-medium">Date</th>
<th className="text-right px-3 py-2 font-medium">Prix entrée</th>
<th className="text-right px-3 py-2 font-medium">Prix actuel</th>
<th className="text-right px-3 py-2 font-medium">Cible/Stop</th>
<th className="text-right px-3 py-2 font-medium">Maturité</th>
<th className="text-right px-3 py-2 font-medium">Entry price</th>
<th className="text-right px-3 py-2 font-medium">Current price</th>
<th className="text-right px-3 py-2 font-medium">Target/Stop</th>
<th className="text-right px-3 py-2 font-medium">Maturity</th>
<th className="text-right px-3 py-2 font-medium">IV Rank</th>
<th className="text-right px-3 py-2 font-medium">Kelly</th>
<th className="text-right px-3 py-2 font-medium">P&L</th>
@@ -1074,7 +1074,7 @@ function TradeMtmSection({ days }: { days: number }) {
{t.strike_guidance ?? '—'}
</td>
<td className="px-3 py-2 text-right font-mono text-[11px] text-slate-400">
{t.expiry_days_at_entry != null ? `${t.expiry_days_at_entry}j` : t.horizon_days != null ? `${t.horizon_days}j` : ''}
{t.expiry_days_at_entry != null ? `${t.expiry_days_at_entry}d` : t.horizon_days != null ? `${t.horizon_days}d` : '—'}
</td>
<td className="px-3 py-2 font-mono">
<div className="flex items-center gap-1">
@@ -1082,7 +1082,7 @@ function TradeMtmSection({ days }: { days: number }) {
{t.price_warning && (
<span
className="text-[8px] font-bold bg-amber-900/40 border border-amber-600/40 text-amber-400 rounded px-1 py-0.5 leading-none shrink-0"
title={t.price_warning === 'no_price_data' ? 'Aucune donnée prix monitoring impossible' : t.price_warning === 'no_entry_price' ? 'Prix d\'entrée manquant' : 'Prix live indisponible'}
title={t.price_warning === 'no_price_data' ? 'No price data — monitoring unavailable' : t.price_warning === 'no_entry_price' ? 'Missing entry price' : 'Live price unavailable'}
>
{t.price_warning === 'no_price_data' ? 'NO DATA' : t.price_warning === 'no_entry_price' ? 'NO ENTRY' : 'NO LIVE'}
</span>
@@ -1167,7 +1167,7 @@ function TradeMtmSection({ days }: { days: number }) {
{t.maturity.emoji} {t.maturity.label}
</span>
<span className="text-[9px] text-slate-600 font-mono">
{t.days_held ?? 0}j / {t.horizon_days ?? 90}j ({t.maturity.ratio_pct}%)
{t.days_held ?? 0}d / {t.horizon_days ?? 90}d ({t.maturity.ratio_pct}%)
</span>
<div className="w-16 h-1 bg-slate-700 rounded-full overflow-hidden">
<div
@@ -1183,7 +1183,7 @@ function TradeMtmSection({ days }: { days: number }) {
</div>
) : (
<span className="text-slate-700 text-[11px]">
{t.days_held != null ? `${t.days_held}j` : '—'}
{t.days_held != null ? `${t.days_held}d` : '—'}
</span>
)}
</td>
@@ -1219,7 +1219,7 @@ function TradeMtmSection({ days }: { days: number }) {
? 'text-amber-400 bg-amber-900/30 hover:bg-amber-900/50'
: 'text-slate-700 hover:text-amber-400 hover:bg-amber-900/20'
)}
title="Fermer le trade"
title="Close trade"
>
<Lock className="w-3.5 h-3.5" />
</button>
@@ -1265,7 +1265,7 @@ function GeoHistorySection({ days }: { days: number }) {
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide">
{history.length} alertes · {days} derniers jours
{history.length} alerts · last {days} days
</div>
<button onClick={() => refetch()} disabled={isFetching}
className="text-slate-600 hover:text-slate-400 disabled:opacity-40">
@@ -1278,7 +1278,7 @@ function GeoHistorySection({ days }: { days: number }) {
) : history.length === 0 ? (
<div className="card text-center py-10 text-slate-600 text-sm">
<AlertTriangle className="w-8 h-8 mx-auto mb-2 opacity-20" />
Aucune alerte scorer des patterns pour commencer le suivi
No alerts score patterns to start tracking
</div>
) : (
<div className="space-y-2">
@@ -1348,7 +1348,7 @@ function CyclesSection() {
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide">
{runs.length} cycles · {cs?.enabled ? `auto ${cs.interval_hours}h` : 'manuel uniquement'}
{runs.length} cycles · {cs?.enabled ? `auto ${cs.interval_hours}h` : 'manual only'}
</div>
<div className="flex gap-2">
<button
@@ -1356,7 +1356,7 @@ function CyclesSection() {
disabled={triggering}
className="flex items-center gap-1 text-xs border border-blue-500/40 text-blue-400 hover:bg-blue-900/20 px-2.5 py-1 rounded disabled:opacity-40">
<Zap className={clsx('w-3 h-3', triggering && 'animate-pulse')} />
{triggering ? 'Lancement...' : 'Lancer un cycle'}
{triggering ? 'Launching...' : 'Run a cycle'}
</button>
<button onClick={() => refetch()} disabled={isFetching}
className="text-slate-600 hover:text-slate-400 disabled:opacity-40">
@@ -1370,7 +1370,7 @@ function CyclesSection() {
) : runs.length === 0 ? (
<div className="card text-center py-10 text-slate-600 text-sm">
<Zap className="w-8 h-8 mx-auto mb-2 opacity-20" />
Aucun cycle activer l'auto-cycle dans Configuration ou lancer manuellement
No cycles enable auto-cycle in Configuration or run manually
</div>
) : (
<div className="space-y-3">
@@ -1392,16 +1392,16 @@ function CyclesSection() {
<div className="flex items-center gap-2 flex-wrap">
<span className="text-xs text-slate-300 font-semibold">{ts} UTC</span>
<span className={clsx('badge text-[10px]', run.trigger === 'manual' ? 'badge-blue' : 'badge-purple')}>
{run.trigger === 'manual' ? 'Manuel' : `Auto ${cs?.interval_hours ?? ''}h`}
{run.trigger === 'manual' ? 'Manual' : `Auto ${cs?.interval_hours ?? ''}h`}
</span>
{run.dominant_regime && <ScenarioBadge dominant={run.dominant_regime} />}
{duration != null && <span className="text-[10px] text-slate-600">{duration}s</span>}
</div>
<div className="flex gap-3 mt-1 text-[11px] text-slate-500 flex-wrap">
<span>💡 {run.patterns_suggested} suggérés</span>
<span>✚ {run.patterns_added} ajoutés</span>
<span>🎯 {run.patterns_scored} scorés</span>
{run.geo_score != null && <span>🌐 Géo {run.geo_score}/100</span>}
<span>💡 {run.patterns_suggested} suggested</span>
<span> {run.patterns_added} added</span>
<span>🎯 {run.patterns_scored} scored</span>
{run.geo_score != null && <span>🌐 Geo {run.geo_score}/100</span>}
</div>
</div>
</div>
@@ -1409,7 +1409,7 @@ function CyclesSection() {
{commentary && (
<div className="bg-blue-900/10 border border-blue-700/20 rounded p-3 space-y-2">
<div className="flex items-center gap-1.5 text-[10px] text-blue-400 font-semibold uppercase tracking-wide">
<Brain className="w-3 h-3" /> Commentaire IA
<Brain className="w-3 h-3" /> AI Comment
</div>
<p className="text-xs text-slate-300 leading-relaxed">{commentary.commentary}</p>
{commentary.key_risk && (
@@ -1419,13 +1419,13 @@ function CyclesSection() {
)}
{commentary.top_pattern && (
<div className="text-[11px] text-emerald-400/80">
🎯 Pattern clé : <span className="font-semibold">{commentary.top_pattern}</span>
🎯 Key pattern: <span className="font-semibold">{commentary.top_pattern}</span>
</div>
)}
{commentary.lessons_from_report ? (
<div className="flex items-center gap-1.5 text-[10px] text-purple-400/80 bg-purple-900/10 border border-purple-700/20 rounded px-2 py-1">
<BookOpen className="w-3 h-3 shrink-0" />
Leçons du rapport du {commentary.lessons_from_report} intégrées dans ce cycle
Lessons from the {commentary.lessons_from_report} report integrated in this cycle
{commentary.lessons_headline && (
<span className="text-slate-600 ml-1">· {commentary.lessons_headline}</span>
)}
@@ -1433,14 +1433,14 @@ function CyclesSection() {
) : (
<div className="flex items-center gap-1.5 text-[10px] text-slate-700 italic">
<BookOpen className="w-3 h-3 shrink-0" />
Aucun rapport de performance disponible — générer un rapport dans "Rapport IA"
No performance report available generate a report in "AI Report"
</div>
)}
</div>
)}
{!commentary && ok && (
<div className="text-[11px] text-slate-700 italic">Aucun commentaire IA généré pour ce cycle</div>
<div className="text-[11px] text-slate-700 italic">No AI comment generated for this cycle</div>
)}
</div>
)
@@ -1476,8 +1476,8 @@ function SimPortfolioRiskPanel() {
return (
<div className="card text-center py-8 text-slate-500">
<PieChart className="w-8 h-8 mx-auto mb-2 opacity-20" />
<div className="text-sm">Aucune position ouverte dans le portefeuille simulé</div>
<div className="text-xs mt-1">Les trades logués apparaîtront ici après le prochain cycle</div>
<div className="text-sm">No open positions in the simulated portfolio</div>
<div className="text-xs mt-1">Logged trades will appear here after the next cycle</div>
</div>
)
}
@@ -1497,11 +1497,11 @@ function SimPortfolioRiskPanel() {
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<PieChart className="w-4 h-4 text-blue-400" />
<span className="text-sm font-semibold text-white">Portefeuille Simulé — Analyse de Risque</span>
<span className="text-xs text-slate-500">{risk.open_count} positions ouvertes</span>
<span className="text-sm font-semibold text-white">Simulated Portfolio Risk Analysis</span>
<span className="text-xs text-slate-500">{risk.open_count} open positions</span>
{dangers.length > 0 && (
<span className="inline-flex items-center gap-1 text-[10px] font-bold bg-red-900/30 border border-red-600/40 text-red-400 rounded px-1.5 py-0.5">
<ShieldAlert className="w-3 h-3" /> {dangers.length} CONFLIT{dangers.length > 1 ? 'S' : ''}
<ShieldAlert className="w-3 h-3" /> {dangers.length} CONFLICT{dangers.length > 1 ? 'S' : ''}
</span>
)}
</div>
@@ -1512,7 +1512,7 @@ function SimPortfolioRiskPanel() {
{/* Asset class concentration bars */}
<div className="card">
<div className="text-xs text-slate-500 mb-3 font-semibold uppercase tracking-wide">Répartition par classe d'actif</div>
<div className="text-xs text-slate-500 mb-3 font-semibold uppercase tracking-wide">Breakdown by asset class</div>
<div className="space-y-2">
{Object.entries(concentration)
.sort(([, a]: any, [, b]: any) => b.pct - a.pct)
@@ -1563,7 +1563,7 @@ function SimPortfolioRiskPanel() {
<ShieldAlert className="w-4 h-4 text-red-400 shrink-0 mt-0.5" />
<div className="flex-1 min-w-0">
<div className="text-xs font-semibold text-red-300 mb-1">
Conflit directionnel {c.underlying}
Directional conflict {c.underlying}
</div>
<div className="space-y-0.5">
{c.trades.map((t: any) => (
@@ -1589,7 +1589,7 @@ function SimPortfolioRiskPanel() {
{warnings.length > 0 && (
<div className="card border-amber-700/30 bg-amber-900/10">
<div className="text-xs font-semibold text-amber-300 mb-1.5 flex items-center gap-1">
<AlertTriangle className="w-3.5 h-3.5" /> Alertes de concentration
<AlertTriangle className="w-3.5 h-3.5" /> Concentration alerts
</div>
<div className="space-y-1">
{warnings.map((a: any, i: number) => (
@@ -1609,7 +1609,7 @@ function SimPortfolioRiskPanel() {
<div className="card border-blue-700/30 bg-blue-900/10">
<div className="flex items-center justify-between mb-2">
<div className="text-xs font-semibold text-blue-300 flex items-center gap-1.5">
<Brain className="w-3.5 h-3.5" /> Recommandations IA Moniteur de portefeuille
<Brain className="w-3.5 h-3.5" /> AI Recommendations Portfolio Monitor
</div>
{aiTs && (
<span className="text-[10px] text-slate-600">{aiTs.slice(0, 16).replace('T', ' ')}</span>
@@ -1650,7 +1650,7 @@ function SimPortfolioRiskPanel() {
{alerts.length === 0 && (
<div className="card border-emerald-700/30 bg-emerald-900/10 text-center py-3">
<CheckCircle className="w-5 h-5 text-emerald-400 mx-auto mb-1" />
<div className="text-xs text-emerald-300">Portefeuille équilibré aucun conflit ni sur-concentration détectés</div>
<div className="text-xs text-emerald-300">Balanced portfolio no conflicts or over-concentration detected</div>
</div>
)}
</div>
@@ -1658,13 +1658,13 @@ function SimPortfolioRiskPanel() {
}
const TABS = [
{ key: 'cycles', label: 'Cycles IA', icon: Zap },
{ key: 'macro', label: 'Régimes Macro', icon: Activity },
{ key: 'ideas', label: 'Idées de trade', icon: Target },
{ key: 'mtm', label: 'Ouverts', icon: TrendingUp },
{ key: 'closed', label: 'Fermés', icon: Lock },
{ key: 'geo', label: 'Alertes Géo', icon: AlertTriangle },
{ key: 'skipped', label: 'Non loggés', icon: XCircle },
{ key: 'cycles', label: 'AI Cycles', icon: Zap },
{ key: 'macro', label: 'Macro Regimes', icon: Activity },
{ key: 'ideas', label: 'Trade Ideas', icon: Target },
{ key: 'mtm', label: 'Open', icon: TrendingUp },
{ key: 'closed', label: 'Closed', icon: Lock },
{ key: 'geo', label: 'Geo Alerts', icon: AlertTriangle },
{ key: 'skipped', label: 'Not logged', icon: XCircle },
] as const
function SkippedTradesSection({ days }: { days: number }) {
@@ -1706,7 +1706,7 @@ function SkippedTradesSection({ days }: { days: number }) {
return (
<div className="card text-center py-8 text-slate-500 text-sm">
<XCircle className="w-6 h-6 mx-auto mb-2 text-slate-600" />
Aucune suggestion non loggée sur {days}j tous les trades scorés passent au moins un profil de risque.
No unlogged suggestions in the last {days}d all scored trades match at least one risk profile.
</div>
)
}
@@ -1716,9 +1716,9 @@ function SkippedTradesSection({ days }: { days: number }) {
<div className="card">
<div className="section-title flex items-center gap-1 mb-3">
<XCircle className="w-3 h-3 text-amber-400" />
Trades suggérés non loggés {trades.length}/{allTrades.length} sur {days}j
Suggested trades not logged {trades.length}/{allTrades.length} over {days}d
<span className="ml-2 text-slate-600 font-normal text-xs">
(score ou gain insuffisant pour tout profil actif)
(score or gain insufficient for any active profile)
</span>
</div>
@@ -1743,7 +1743,7 @@ function SkippedTradesSection({ days }: { days: number }) {
<button onClick={() => setReasonFilter('all')}
className={clsx('px-2 py-1 rounded text-xs transition-colors',
reasonFilter === 'all' ? 'bg-slate-600 text-white' : 'text-slate-400 hover:text-slate-200')}>
Tous
All
</button>
{allReasons.map(r => (
<button key={r} onClick={() => setReasonFilter(r)}
@@ -1757,7 +1757,7 @@ function SkippedTradesSection({ days }: { days: number }) {
</div>
{trades.length === 0 ? (
<div className="text-center py-6 text-slate-600 text-sm">Aucun résultat pour ces filtres</div>
<div className="text-center py-6 text-slate-600 text-sm">No results for these filters</div>
) : (
<div className="overflow-x-auto rounded-lg border border-slate-700/40">
<table className="w-full text-xs">
@@ -1765,11 +1765,11 @@ function SkippedTradesSection({ days }: { days: number }) {
<tr className="border-b border-slate-700/60 text-slate-600 text-[10px] uppercase tracking-wide">
<th className="pl-3 py-2 text-left">Pattern</th>
<th className="px-2 py-2 text-left">Ticker</th>
<th className="px-2 py-2 text-left">Stratégie</th>
<th className="px-2 py-2 text-left">Strategy</th>
<th className="px-2 py-2 text-right">Score</th>
<th className="px-2 py-2 text-right">Gain attendu</th>
<th className="px-2 py-2 text-left">Classe</th>
<th className="pr-3 py-2 text-left">Raison du skip</th>
<th className="px-2 py-2 text-right">Expected gain</th>
<th className="px-2 py-2 text-left">Class</th>
<th className="pr-3 py-2 text-left">Skip reason</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/40">
@@ -1827,7 +1827,7 @@ export default function JournalDeBord() {
setResetting(true)
try {
await api.delete('/journal/reset')
setResetMsg('Journal réinitialisé')
setResetMsg('Journal reset')
setConfirmReset(false)
// Invalidate all journal queries
await qc.invalidateQueries({ queryKey: ['journal-summary'] })
@@ -1837,7 +1837,7 @@ export default function JournalDeBord() {
await qc.invalidateQueries({ queryKey: ['cycle-history'] })
setTimeout(() => setResetMsg(''), 3000)
} catch {
setResetMsg('Erreur lors du reset')
setResetMsg('Reset error')
setTimeout(() => setResetMsg(''), 3000)
} finally {
setResetting(false)
@@ -1850,10 +1850,10 @@ export default function JournalDeBord() {
<div className="flex items-start justify-between">
<div>
<h1 className="text-xl font-bold text-white flex items-center gap-2">
<BookOpen className="w-5 h-5 text-blue-400" /> Journal de Bord
<BookOpen className="w-5 h-5 text-blue-400" /> Trading Journal
</h1>
<p className="text-xs text-slate-500 mt-0.5">
Historique des régimes macro · Mark-to-market des trades · Évolution du risque géopolitique
Macro regime history · Trade mark-to-market · Geopolitical risk evolution
</p>
</div>
@@ -1866,7 +1866,7 @@ export default function JournalDeBord() {
</span>
)}
{confirmReset && !resetting && (
<span className="text-xs text-amber-400">Confirmer ?</span>
<span className="text-xs text-amber-400">Confirm?</span>
)}
<button
onClick={confirmReset ? handleReset : () => setConfirmReset(true)}
@@ -1879,7 +1879,7 @@ export default function JournalDeBord() {
: 'border-slate-700/40 text-slate-600 hover:text-slate-400 hover:border-slate-600'
)}>
<Trash2 className="w-3.5 h-3.5" />
{resetting ? 'Réinit...' : confirmReset ? 'Effacer tout' : 'Reset journal'}
{resetting ? 'Resetting...' : confirmReset ? 'Clear all' : 'Reset journal'}
</button>
</div>
@@ -1891,7 +1891,7 @@ export default function JournalDeBord() {
'bg-blue-600 text-white': days === d,
'text-slate-400 hover:text-slate-200': days !== d,
})}>
{d}j
{d}d
</button>
))}
</div>
@@ -1903,14 +1903,14 @@ export default function JournalDeBord() {
<div className="grid grid-cols-4 gap-3">
<div className="card text-center">
<div className="text-2xl font-bold text-white">{s.macro_snapshots ?? 0}</div>
<div className="text-xs text-slate-600 mt-0.5">snapshots macro</div>
<div className="text-xs text-slate-600 mt-0.5">macro snapshots</div>
{s.current_dominant && (
<div className="mt-1"><ScenarioBadge dominant={s.current_dominant} /></div>
)}
</div>
<div className="card text-center">
<div className="text-2xl font-bold text-white">{s.regime_transitions?.length ?? 0}</div>
<div className="text-xs text-slate-600 mt-0.5">transitions de régime</div>
<div className="text-xs text-slate-600 mt-0.5">regime transitions</div>
{s.regime_transitions?.length > 0 && (
<div className="text-[10px] text-amber-400 mt-1">
{s.regime_transitions[0].from} {s.regime_transitions[0].to}
@@ -1922,23 +1922,23 @@ export default function JournalDeBord() {
(s.avg_geo_score ?? 0) >= 70 ? 'text-red-400' : (s.avg_geo_score ?? 0) >= 40 ? 'text-orange-400' : 'text-emerald-400')}>
{s.avg_geo_score != null ? s.avg_geo_score.toFixed(0) : '—'}
</div>
<div className="text-xs text-slate-600 mt-0.5">score géo moyen</div>
<div className="text-xs text-slate-600 mt-0.5">avg geo score</div>
{s.max_geo_score != null && (
<div className="text-[10px] text-slate-600 mt-1">max {s.max_geo_score}</div>
)}
</div>
<div className="card text-center">
<div className="text-2xl font-bold text-white">{s.trade_entries_logged ?? 0}</div>
<div className="text-xs text-slate-600 mt-0.5">trades logués</div>
<div className="text-xs text-slate-600 mt-0.5">logged trades</div>
</div>
{riskAlertCount > 0 && (
<div className="card text-center border-red-700/40 bg-red-900/10 col-span-full">
<div className="flex items-center justify-center gap-2">
<ShieldAlert className="w-4 h-4 text-red-400" />
<span className="text-sm font-semibold text-red-300">
{riskAlertCount} conflit{riskAlertCount > 1 ? 's' : ''} directionnel{riskAlertCount > 1 ? 's' : ''} détecté{riskAlertCount > 1 ? 's' : ''}
{riskAlertCount} directional conflict{riskAlertCount > 1 ? 's' : ''} detected
</span>
<span className="text-xs text-slate-500"> voir Risk Dashboard Simulé</span>
<span className="text-xs text-slate-500"> see Risk Dashboard Simulated</span>
</div>
</div>
)}

View File

@@ -18,12 +18,12 @@ const SCENARIO_ORDER = [
] as const
const BLOC_INFO: Record<string, { label: string; emoji: string; description: string }> = {
liquidite: { label: 'Liquidité mondiale', emoji: '💧', description: 'Carburant des marchés — DXY↓ = conditions mondiales détendues ; pente = signal récession/reprise' },
credit: { label: 'Crédit & Stress', emoji: '⚡', description: 'Thermomètre systémique — HYG (HY, obligataires détectent avant les actions), LQD (IG), VIX' },
energie: { label: 'Énergie', emoji: '⛽', description: 'Premier moteur d\'inflation — Brent↑↑ = stagflation/choc ; Brent↓ = désinflationniste' },
metaux: { label: 'Métaux stratégiques', emoji: '🥇', description: 'Cuivre = "Dr Copper" (croissance mondiale) ; Or = refuge/taux réels ; ratio Or/Cu = biais craint vs expansion' },
croissance: { label: 'Croissance mondiale', emoji: '📊', description: 'Cycle réel — S&P, Russell 2000 (breadth risk-on), XLI (proxy ISM mfg), Baltic Dry (secondaire)' },
derive: { label: 'Indicateurs dérivés', emoji: '🔗', description: 'Pente 10Y3M (récession) · Or/Cu (peur vs expansion) · S&P vs 200j · Russell vs S&P (breadth)' },
liquidite: { label: 'Global Liquidity', emoji: '💧', description: 'Market fuel — DXY↓ = loose global conditions; yield curve = recession/recovery signal' },
credit: { label: 'Credit & Stress', emoji: '⚡', description: 'Systemic thermometer — HYG (HY, bonds lead equities), LQD (IG), VIX' },
energie: { label: 'Energy', emoji: '⛽', description: 'Primary inflation driver — Brent↑↑ = stagflation/shock; Brent↓ = disinflationary' },
metaux: { label: 'Strategic Metals', emoji: '🥇', description: 'Copper = "Dr Copper" (global growth); Gold = haven/real rates; Gold/Cu ratio = fear vs expansion bias' },
croissance: { label: 'Global Growth', emoji: '📊', description: 'Real cycle — S&P, Russell 2000 (breadth risk-on), XLI (ISM mfg proxy), Baltic Dry (secondary)' },
derive: { label: 'Derived Indicators', emoji: '🔗', description: 'Slope 10Y3M (recession) · Gold/Cu (fear vs expansion) · S&P vs 200d · Russell vs S&P (breadth)' },
}
const ASSET_BIAS_COLORS: Record<string, string> = {
@@ -31,12 +31,12 @@ const ASSET_BIAS_COLORS: Record<string, string> = {
'bearish': '#f97316', 'bearish+': '#ef4444', 'defensive': '#f59e0b',
}
const ASSET_BIAS_LABELS: Record<string, string> = {
'bullish+': '★★ Très favorable', 'bullish': '★ Favorable', 'neutral': '→ Neutre',
'bearish': '✗ favorable', 'bearish+': '✗✗ Contre', 'defensive': '⚠ Défensif',
'bullish+': '★★ Very Favorable', 'bullish': '★ Favorable', 'neutral': '→ Neutral',
'bearish': '✗ Unfavorable', 'bearish+': '✗✗ Against', 'defensive': '⚠ Defensive',
}
const ASSET_CLASS_LABELS: Record<string, string> = {
energy: 'Énergie ⛽', metals: 'Métaux 🥇', agriculture: 'Agriculture 🌾',
indices: 'Indices 📊', equities: 'Actions 📈', forex: 'Forex 💱',
energy: 'Energy ⛽', metals: 'Metals 🥇', agriculture: 'Agriculture 🌾',
indices: 'Indices 📊', equities: 'Equities 📈', forex: 'Forex 💱',
}
function GaugeRow({ g }: { g: MacroGauge }) {
@@ -153,7 +153,7 @@ export default function MacroRegime() {
setSavedNarration(entry)
localStorage.setItem('geo-options:macro-narration', JSON.stringify(entry))
} catch {
const entry = { text: 'Erreur lors de la génération — vérifier la clé OpenAI.', at: new Date().toISOString() }
const entry = { text: 'Error during generation — please check your OpenAI key.', at: new Date().toISOString() }
setSavedNarration(entry)
} finally {
setLoadingNarration(false)
@@ -166,10 +166,10 @@ export default function MacroRegime() {
<div className="flex items-start justify-between">
<div>
<h1 className="text-xl font-bold text-white flex items-center gap-2">
<Activity className="w-5 h-5 text-blue-400" /> Régime Macro
<Activity className="w-5 h-5 text-blue-400" /> Macro Regime
</h1>
<p className="text-xs text-slate-500 mt-0.5">
30 compteurs institutionnels · Détection de régime · Compatibilité scénarios
30 institutional gauges · Regime detection · Scenario compatibility
</p>
</div>
<div className="flex items-center gap-3">
@@ -184,14 +184,14 @@ export default function MacroRegime() {
className="flex items-center gap-1.5 text-xs text-slate-400 hover:text-white border border-slate-700 rounded px-2.5 py-1.5 transition-colors hover:border-slate-500 disabled:opacity-40"
>
<RefreshCw className={clsx('w-3.5 h-3.5', isFetching && 'animate-spin')} />
Actualiser
Refresh
</button>
</div>
</div>
{isLoading ? (
<div className="card flex items-center justify-center py-12 text-slate-600 text-sm">
<RefreshCw className="w-4 h-4 animate-spin mr-2" /> Chargement des compteurs macro...
<RefreshCw className="w-4 h-4 animate-spin mr-2" /> Loading macro gauges...
</div>
) : (
<>
@@ -201,7 +201,7 @@ export default function MacroRegime() {
style={{ borderColor: `${dominantMeta.color}44`, background: `${dominantMeta.color}0d` }}>
<div className="text-4xl">{dominantMeta.emoji}</div>
<div className="flex-1">
<div className="text-xs text-slate-500 uppercase tracking-widest mb-0.5">Scénario dominant</div>
<div className="text-xs text-slate-500 uppercase tracking-widest mb-0.5">Dominant Scenario</div>
<div className="text-xl font-bold" style={{ color: dominantMeta.color }}>{dominantMeta.label}</div>
<div className="text-xs text-slate-500 mt-1 flex flex-wrap gap-1">
{(reasons[dominant] ?? []).map((r, i) => (
@@ -216,16 +216,16 @@ export default function MacroRegime() {
<div className="text-3xl font-bold font-mono" style={{ color: dominantMeta.color }}>
{scores[dominant]}%
</div>
<div className="text-xs text-slate-600">score de régime</div>
<div className="text-xs text-slate-600">regime score</div>
{regimeStability > 0 && (
<div className="text-[10px] mt-1 px-1.5 py-0.5 rounded-full text-center"
style={{ background: `${dominantMeta.color}22`, color: dominantMeta.color }}>
stable {regimeStability}j
stable {regimeStability}d
</div>
)}
{historyDays > 0 && (
<div className="text-[10px] text-slate-700 mt-0.5 text-center">
{historyDays}j d'historique
{historyDays}d history
</div>
)}
</div>
@@ -236,7 +236,7 @@ export default function MacroRegime() {
{/* Left: Gauge blocs */}
<div className="col-span-2 space-y-3">
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide">
Compteurs par bloc ({Object.values(gauges).length} indicateurs)
Gauges by bloc ({Object.values(gauges).length} indicators)
</div>
{['liquidite', 'credit', 'energie', 'metaux', 'croissance', 'derive'].map(bloc => (
<BlocCard key={bloc} id={bloc} gauges={byBloc[bloc] ?? []} />
@@ -248,7 +248,7 @@ export default function MacroRegime() {
{/* Scenario ranking */}
<div className="card">
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-3">
Classement des scénarios
Scenario Ranking
</div>
<div className="space-y-3">
{ranked.map(([key, score]) => {
@@ -291,7 +291,7 @@ export default function MacroRegime() {
{dominant !== 'incertain' && Object.keys(dominantBias).length > 0 && (
<div className="card">
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-3">
Classes d'actifs · {dominantMeta.emoji} {dominantMeta.label}
Asset Classes · {dominantMeta.emoji} {dominantMeta.label}
</div>
<div className="space-y-1.5">
{Object.entries(dominantBias).map(([cls, bias]) => {
@@ -312,7 +312,7 @@ export default function MacroRegime() {
<div className="card">
<div className="flex items-center justify-between mb-2">
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide flex items-center gap-1.5">
<Brain className="w-3.5 h-3.5 text-blue-400" /> Analyse IA
<Brain className="w-3.5 h-3.5 text-blue-400" /> AI Analysis
</div>
{savedNarration?.at && (
<span className="text-[10px] text-slate-700">
@@ -326,7 +326,7 @@ export default function MacroRegime() {
<p className="text-xs text-slate-300 leading-relaxed mb-3">{savedNarration.text}</p>
) : (
<p className="text-xs text-slate-600 leading-relaxed mb-3">
GPT-4o peut interpréter le régime actuel, identifier les incohérences entre compteurs et proposer des biais de trading.
GPT-4o can interpret the current regime, identify inconsistencies between gauges and suggest trading biases.
</p>
)}
{aiStatus?.enabled ? (
@@ -336,25 +336,25 @@ export default function MacroRegime() {
className="w-full text-xs bg-blue-600/20 hover:bg-blue-600/40 border border-blue-500/30 hover:border-blue-500/60 text-blue-300 rounded py-1.5 transition-all disabled:opacity-40 flex items-center justify-center gap-1.5"
>
{loadingNarration ? (
<><RefreshCw className="w-3 h-3 animate-spin" /> Analyse en cours...</>
<><RefreshCw className="w-3 h-3 animate-spin" /> Analysis in progress...</>
) : (
<><Brain className="w-3 h-3" /> {savedNarration ? 'Ré-analyser' : 'Demander à l\'IA'}</>
<><Brain className="w-3 h-3" /> {savedNarration ? 'Re-analyze' : 'Ask AI'}</>
)}
</button>
) : (
<div className="text-xs text-slate-700 text-center">OpenAI non configuré</div>
<div className="text-xs text-slate-700 text-center">OpenAI not configured</div>
)}
</div>
{/* Reading order reminder */}
<div className="card bg-dark-700/30">
<div className="text-xs font-semibold text-slate-600 mb-2">Ordre de lecture (5 min)</div>
<div className="text-xs font-semibold text-slate-600 mb-2">Reading order (5 min)</div>
<div className="space-y-0.5 text-[10px] text-slate-600">
<div>1. <span className="text-slate-500">Liquidité</span> Bilan Fed, DXY, taux réels</div>
<div>2. <span className="text-slate-500">Crédit</span> HY spreads, VIX, MOVE</div>
<div>3. <span className="text-slate-500">Énergie</span> Brent, gaz naturel</div>
<div>4. <span className="text-slate-500">Métaux</span> Or/Cuivre ratio</div>
<div>5. <span className="text-slate-500">Synthèse</span> Scénario dominant</div>
<div>1. <span className="text-slate-500">Liquidity</span> Fed balance sheet, DXY, real rates</div>
<div>2. <span className="text-slate-500">Credit</span> HY spreads, VIX, MOVE</div>
<div>3. <span className="text-slate-500">Energy</span> Brent, natural gas</div>
<div>4. <span className="text-slate-500">Metals</span> Gold/Copper ratio</div>
<div>5. <span className="text-slate-500">Synthesis</span> Dominant scenario</div>
</div>
</div>
</div>

View File

@@ -8,11 +8,11 @@ import { TrendingUp, TrendingDown, BarChart2, RefreshCw, Search } from 'lucide-r
const ASSET_CLASSES: { key: AssetClass; label: string; emoji: string }[] = [
{ key: 'energy', label: 'Énergie', emoji: '⛽' },
{ key: 'metals', label: 'Métaux', emoji: '🥇' },
{ key: 'energy', label: 'Energy', emoji: '⛽' },
{ key: 'metals', label: 'Metals', emoji: '🥇' },
{ key: 'agriculture', label: 'Agriculture', emoji: '🌾' },
{ key: 'indices', label: 'Indices', emoji: '📊' },
{ key: 'equities', label: 'Actions', emoji: '📈' },
{ key: 'equities', label: 'Equities', emoji: '📈' },
{ key: 'forex', label: 'Forex', emoji: '💱' },
]
@@ -56,8 +56,8 @@ const PERIOD_OPTIONS = [
{ label: '1M', value: '1mo' },
{ label: '3M', value: '3mo' },
{ label: '6M', value: '6mo' },
{ label: '1A', value: '1y' },
{ label: '2A', value: '2y' },
{ label: '1Y', value: '1y' },
{ label: '2Y', value: '2y' },
]
function PriceChart({ symbol, name }: { symbol: string; name: string }) {
@@ -127,7 +127,7 @@ function PriceChart({ symbol, name }: { symbol: string; name: string }) {
<Tooltip
contentStyle={{ background: '#0f1623', border: '1px solid #1e2d4d', fontSize: 11 }}
labelStyle={{ color: '#94a3b8' }}
formatter={(v: number) => [v.toFixed(4), 'Prix']}
formatter={(v: number) => [v.toFixed(4), 'Price']}
/>
<Area
type="monotone" dataKey="close"
@@ -139,7 +139,7 @@ function PriceChart({ symbol, name }: { symbol: string; name: string }) {
</ResponsiveContainer>
) : (
<div className="h-48 flex items-center justify-center text-slate-600 text-xs">
Aucune donnée disponible
No data available
</div>
)}
</div>
@@ -192,17 +192,17 @@ export default function Markets() {
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold text-white flex items-center gap-2">
<BarChart2 className="w-5 h-5 text-blue-400" /> Marchés & Prix
<BarChart2 className="w-5 h-5 text-blue-400" /> Markets & Prices
</h1>
<p className="text-xs text-slate-500 mt-0.5">
Données en temps réel · Volatilité implicite · Opportunités options
Real-time data · Implied volatility · Options opportunities
</p>
</div>
<button
onClick={() => refetch()}
className="flex items-center gap-1.5 text-xs text-slate-400 hover:text-slate-200 transition-colors"
>
<RefreshCw className="w-3.5 h-3.5" /> Actualiser
<RefreshCw className="w-3.5 h-3.5" /> Refresh
</button>
</div>
@@ -227,7 +227,7 @@ export default function Markets() {
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
onKeyDown={e => { if (e.key === 'Escape') setSearchQuery('') }}
placeholder="Rechercher un ticker..."
placeholder="Search ticker..."
className="bg-dark-700 border border-slate-700 rounded pl-8 pr-3 py-1.5 text-sm text-white placeholder-slate-600 focus:outline-none focus:border-blue-500 w-52"
/>
{searchResults.length > 0 && (
@@ -282,7 +282,7 @@ export default function Markets() {
))
) : (
<div className="card text-slate-500 text-xs text-center py-6">
Démarrer le backend pour charger les prix
Start the backend to load prices
</div>
)}
</div>
@@ -295,8 +295,8 @@ export default function Markets() {
<div className="card h-full flex items-center justify-center text-slate-600 text-sm">
<div className="text-center">
<BarChart2 className="w-8 h-8 mx-auto mb-2 opacity-30" />
<div>Sélectionner un instrument</div>
<div className="text-xs mt-1">pour voir le graphique de prix</div>
<div>Select an instrument</div>
<div className="text-xs mt-1">to view the price chart</div>
</div>
</div>
)}
@@ -305,16 +305,16 @@ export default function Markets() {
{/* Market overview table */}
<div className="card">
<div className="section-title">Vue d'ensemble — Tous marchés</div>
<div className="section-title">Overview All Markets</div>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="text-slate-600 border-b border-slate-700/40">
<th className="text-left pb-2">Instrument</th>
<th className="text-left pb-2">Classe</th>
<th className="text-right pb-2">Prix</th>
<th className="text-right pb-2">Var. 1j</th>
<th className="text-right pb-2">Vol. IV (est.)</th>
<th className="text-left pb-2">Class</th>
<th className="text-right pb-2">Price</th>
<th className="text-right pb-2">1d Chg</th>
<th className="text-right pb-2">IV Vol (est.)</th>
<th className="text-right pb-2">Signal</th>
</tr>
</thead>
@@ -337,9 +337,9 @@ export default function Markets() {
{q.iv ? `${(q.iv * 100).toFixed(1)}%` : '—'}
</td>
<td className="py-1.5 text-right">
{q.change_pct > 1.5 ? <span className="badge-green badge"> Haussier</span>
: q.change_pct < -1.5 ? <span className="badge-red badge"> Baissier</span>
: <span className="badge badge-blue"> Neutre</span>}
{q.change_pct > 1.5 ? <span className="badge-green badge"> Bullish</span>
: q.change_pct < -1.5 ? <span className="badge-red badge"> Bearish</span>
: <span className="badge badge-blue"> Neutral</span>}
</td>
</tr>
))

View File

@@ -25,9 +25,9 @@ function ivRankBg(rank: number | null | undefined): string {
function ivSignalLabel(rank: number | null | undefined) {
if (rank == null) return null
if (rank >= 80) return { text: 'Vendre de la vol', icon: <TrendingDown className="w-3 h-3" />, cls: 'text-red-400' }
if (rank < 20) return { text: 'Acheter de la vol', icon: <TrendingUp className="w-3 h-3" />, cls: 'text-blue-400' }
return { text: 'Neutre', icon: <Minus className="w-3 h-3" />, cls: 'text-slate-500' }
if (rank >= 80) return { text: 'Sell vol', icon: <TrendingDown className="w-3 h-3" />, cls: 'text-red-400' }
if (rank < 20) return { text: 'Buy vol', icon: <TrendingUp className="w-3 h-3" />, cls: 'text-blue-400' }
return { text: 'Neutral', icon: <Minus className="w-3 h-3" />, cls: 'text-slate-500' }
}
function StructureBadge({ structure }: { structure: string | null | undefined }) {
@@ -93,7 +93,7 @@ function TickerDetail({ ticker }: { ticker: string }) {
<div>
<div className="text-[9px] text-slate-500 uppercase tracking-wide font-semibold mb-2">Term Structure</div>
<div className="space-y-1.5">
{([['30j', ts.iv_30d], ['60j', ts.iv_60d], ['90j', ts.iv_90d], ['180j', ts.iv_180d]] as [string, number | undefined][]).map(([label, iv]) =>
{([['30d', ts.iv_30d], ['60d', ts.iv_60d], ['90d', ts.iv_90d], ['180d', ts.iv_180d]] as [string, number | undefined][]).map(([label, iv]) =>
iv ? (
<div key={label} className="flex items-center gap-2">
<span className="text-[9px] text-slate-600 w-7 shrink-0">{label}</span>
@@ -130,7 +130,7 @@ function TickerDetail({ ticker }: { ticker: string }) {
)}
</div>
) : (
<div className="text-[9px] text-slate-700 italic">Données insuffisantes</div>
<div className="text-[9px] text-slate-700 italic">Insufficient data</div>
)}
</div>
@@ -150,7 +150,7 @@ function TickerDetail({ ticker }: { ticker: string }) {
{flow.gamma_bias && <div className="text-[9px] text-amber-500/70">{flow.gamma_bias}</div>}
{flow.unusual_strikes?.length > 0 && (
<div className="mt-1 pt-1 border-t border-slate-700/30">
<div className="text-[8px] text-slate-600 mb-0.5">Strikes inhabituels</div>
<div className="text-[8px] text-slate-600 mb-0.5">Unusual strikes</div>
{flow.unusual_strikes.slice(0, 2).map((s: any) => (
<div key={s.strike} className="text-[8px] font-mono text-slate-500">
{s.strike} {s.type.toUpperCase()} OI={s.total_oi.toLocaleString()}
@@ -161,19 +161,19 @@ function TickerDetail({ ticker }: { ticker: string }) {
)}
</div>
) : (
<div className="text-[9px] text-slate-700 italic">Pas d'OI disponible</div>
<div className="text-[9px] text-slate-700 italic">No OI available</div>
)}
</div>
{/* History sparkline */}
{history.length > 4 && (
<div className="col-span-3 flex items-center gap-3 pt-1 border-t border-slate-700/20">
<span className="text-[9px] text-slate-600 shrink-0">IV 90j</span>
<span className="text-[9px] text-slate-600 shrink-0">IV 90d</span>
<IvSparkline history={history} />
<div className="text-[9px] text-slate-600">
min <span className="text-slate-400">{snap.iv_min_52w_pct}%</span>
{' · '}max <span className="text-slate-400">{snap.iv_max_52w_pct}%</span>
{' · '}{snap.history_days}j d'historique
{' · '}{snap.history_days}d of history
</div>
</div>
)}
@@ -203,7 +203,7 @@ function WatchlistRow({ item }: { item: any }) {
<span className={clsx('text-sm font-bold font-mono', ivRankColor(item.iv_rank))}>
{item.iv_current_pct != null ? `${item.iv_source === 'history' ? '~' : ''}${item.iv_current_pct}%` : '—'}
</span>
<div className="text-[8px] text-slate-600">{item.iv_source === 'history' ? 'IV estimée' : 'IV actuelle'}</div>
<div className="text-[8px] text-slate-600">{item.iv_source === 'history' ? 'Estimated IV' : 'Current IV'}</div>
</div>
<div className="flex-1 min-w-0 space-y-1">
@@ -230,7 +230,7 @@ function WatchlistRow({ item }: { item: any }) {
</div>
)}
{item.history_days > 0 && (
<div className="text-[8px] text-slate-500">{item.history_days}j historique</div>
<div className="text-[8px] text-slate-500">{item.history_days}d history</div>
)}
</div>
@@ -286,15 +286,15 @@ function WatchlistManager() {
<div className="card space-y-3">
<div className="flex items-center gap-2">
<List className="w-4 h-4 text-slate-400" />
<span className="text-sm font-semibold text-slate-300">Gestion de la Watchlist IV</span>
<span className="ml-auto text-xs text-slate-600">{active.length} tickers actifs</span>
<span className="text-sm font-semibold text-slate-300">IV Watchlist Manager</span>
<span className="ml-auto text-xs text-slate-600">{active.length} active tickers</span>
</div>
{/* Add ticker */}
<div className="flex gap-2">
<input
className="flex-1 bg-slate-800 border border-slate-700 rounded px-3 py-1.5 text-xs text-slate-300 placeholder:text-slate-600"
placeholder="Ajouter ticker (ex: AAPL, FXI, EEM…)"
placeholder="Add ticker (e.g. AAPL, FXI, EEM…)"
value={input}
onChange={e => setInput(e.target.value.toUpperCase())}
onKeyDown={e => e.key === 'Enter' && handleAdd()}
@@ -305,10 +305,10 @@ function WatchlistManager() {
className="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-blue-600/20 hover:bg-blue-600/30 border border-blue-600/40 text-blue-400 rounded disabled:opacity-40 transition-all"
>
<Plus className="w-3.5 h-3.5" />
{adding ? 'Ajout...' : 'Ajouter'}
{adding ? 'Adding...' : 'Add'}
</button>
</div>
<p className="text-[10px] text-slate-600">Le système bootstrappe automatiquement 1 an d'historique IV pour chaque nouveau ticker ajouté.</p>
<p className="text-[10px] text-slate-600">The system automatically bootstraps 1 year of IV history for each new ticker added.</p>
{/* Active tickers */}
<div className="flex flex-wrap gap-1.5">
@@ -331,12 +331,12 @@ function WatchlistManager() {
<div className="text-[9px] text-slate-700 flex gap-3">
<span className="text-slate-800 border-b border-slate-700"></span> Builtins &nbsp;
<span className="text-blue-700 border-b border-blue-700"></span> Auto (cycle) &nbsp;
<span className="text-emerald-700 border-b border-emerald-700">■</span> Manuel
<span className="text-emerald-700 border-b border-emerald-700"></span> Manual
</div>
{inactive.length > 0 && (
<button onClick={() => setShowInactive(!showInactive)} className="text-[10px] text-slate-600 hover:text-slate-400">
{showInactive ? 'Masquer' : `Voir ${inactive.length} ticker(s) désactivés`}
{showInactive ? 'Hide' : `Show ${inactive.length} disabled ticker(s)`}
</button>
)}
{showInactive && inactive.map(e => (
@@ -365,10 +365,10 @@ export default function OptionsLab() {
setBootstrapMsg(null)
try {
await api.post('/options-vol/bootstrap-history')
setBootstrapMsg('Bootstrap lancé (~60s) actualise dans 1 minute pour voir les IV Rank mis à jour.')
setBootstrapMsg('Bootstrap started (~60s) — refresh in 1 minute to see updated IV Ranks.')
setTimeout(() => refetch(), 70_000)
} catch {
setBootstrapMsg('Erreur lors du bootstrap.')
setBootstrapMsg('Error during bootstrap.')
} finally {
setBootstrapping(false)
}
@@ -381,14 +381,14 @@ export default function OptionsLab() {
<div className="p-6 space-y-5">
{needsBootstrap && !bootstrapMsg && (
<div className="flex items-center justify-between gap-3 px-4 py-3 rounded border border-amber-700/40 bg-amber-900/10 text-xs text-amber-300">
<span>⚠️ Historique IV insuffisant — IV Rank bloqué à 50%. Bootstrappe 1 an de données réalisées pour calibrer le Rank.</span>
<span> Insufficient IV history IV Rank stuck at 50%. Bootstrap 1 year of realized data to calibrate the Rank.</span>
<button
onClick={handleBootstrap}
disabled={bootstrapping}
className="flex items-center gap-1.5 text-xs bg-amber-700/30 hover:bg-amber-700/50 border border-amber-600/50 text-amber-200 px-3 py-1.5 rounded transition-all whitespace-nowrap disabled:opacity-50"
>
<Database className={clsx('w-3.5 h-3.5', bootstrapping && 'animate-pulse')} />
{bootstrapping ? 'Chargement...' : 'Initialiser historique'}
{bootstrapping ? 'Loading...' : 'Initialize history'}
</button>
</div>
)}
@@ -413,7 +413,7 @@ export default function OptionsLab() {
className="flex items-center gap-1.5 text-xs border border-slate-600 text-slate-400 hover:text-slate-200 hover:border-slate-500 px-3 py-1.5 rounded transition-all disabled:opacity-50"
>
<RefreshCw className={clsx('w-3.5 h-3.5', isFetching && 'animate-spin')} />
{isFetching ? 'Chargement...' : 'Actualiser'}
{isFetching ? 'Loading...' : 'Refresh'}
</button>
</div>
@@ -422,22 +422,22 @@ export default function OptionsLab() {
<div className="card bg-red-900/10 border-red-700/20 text-xs">
<div className="flex items-center gap-2 mb-1">
<TrendingDown className="w-3.5 h-3.5 text-red-400" />
<span className="font-semibold text-red-400">IV Rank &gt; 80% — Vendre de la vol</span>
<span className="font-semibold text-red-400">IV Rank &gt; 80% Sell vol</span>
</div>
<div className="text-slate-500 leading-snug">Credit spreads, iron condors, covered calls. La prime est élevée → avantage au vendeur.</div>
<div className="text-slate-500 leading-snug">Credit spreads, iron condors, covered calls. Premium is high seller has the edge.</div>
</div>
<div className="card bg-blue-900/10 border-blue-700/20 text-xs">
<div className="flex items-center gap-2 mb-1">
<TrendingUp className="w-3.5 h-3.5 text-blue-400" />
<span className="font-semibold text-blue-400">IV Rank &lt; 20% — Acheter de la convexité</span>
<span className="font-semibold text-blue-400">IV Rank &lt; 20% Buy convexity</span>
</div>
<div className="text-slate-500 leading-snug">Long calls, long puts, straddles, strangles. La prime est cheap → bon moment pour acheter du gamma.</div>
<div className="text-slate-500 leading-snug">Long calls, long puts, straddles, strangles. Premium is cheap good time to buy gamma.</div>
</div>
</div>
<div className="text-[10px] text-slate-600 -mt-2 px-1">
<strong className="text-slate-500">Contango</strong> = IV courte &lt; longue (marché calme, bon pour vendre du court terme) ·
<strong className="text-slate-500"> Backwardation</strong> = IV courte &gt; longue (stress, bon pour acheter de la protection courte) ·
<strong className="text-slate-500"> Skew +</strong> = puts plus chers que calls (biais baissier institutionnel)
<strong className="text-slate-500">Contango</strong> = short IV &lt; long IV (calm market, good for selling short term) ·
<strong className="text-slate-500"> Backwardation</strong> = short IV &gt; long IV (stress, good for buying short protection) ·
<strong className="text-slate-500"> Skew +</strong> = puts more expensive than calls (institutional bearish bias)
</div>
{isLoading ? (
@@ -447,14 +447,14 @@ export default function OptionsLab() {
) : items.length === 0 ? (
<div className="card text-center py-12 text-slate-600">
<Activity className="w-8 h-8 mx-auto mb-3 opacity-30" />
<div className="font-semibold mb-1 text-slate-500">Aucune donnée IV disponible</div>
<div className="font-semibold mb-1 text-slate-500">No IV data available</div>
<div className="text-xs text-slate-600">
Les ETFs US sont requis (SPY, QQQ, GLD...).
<br />L'IV Rank nécessite de l'historique — les données s'accumulent à chaque actualisation.
US ETFs are required (SPY, QQQ, GLD...).
<br />IV Rank requires history data accumulates with each refresh.
</div>
<button onClick={() => refetch()} disabled={isFetching}
className="mt-4 text-xs bg-blue-600 hover:bg-blue-500 text-white px-4 py-1.5 rounded disabled:opacity-50">
Lancer la collecte
Start collection
</button>
</div>
) : (
@@ -462,7 +462,7 @@ export default function OptionsLab() {
{sellVol.length > 0 && (
<div>
<div className="text-xs font-semibold text-red-400 uppercase tracking-wide mb-2 flex items-center gap-1.5">
<TrendingDown className="w-3.5 h-3.5" /> Vendre de la vol IVR &gt; 80% ({sellVol.length})
<TrendingDown className="w-3.5 h-3.5" /> Sell vol IVR &gt; 80% ({sellVol.length})
</div>
<div className="space-y-1.5">{sellVol.map(item => <WatchlistRow key={item.ticker} item={item} />)}</div>
</div>
@@ -470,7 +470,7 @@ export default function OptionsLab() {
{buyVol.length > 0 && (
<div>
<div className="text-xs font-semibold text-blue-400 uppercase tracking-wide mb-2 flex items-center gap-1.5">
<TrendingUp className="w-3.5 h-3.5" /> Acheter de la vol IVR &lt; 20% ({buyVol.length})
<TrendingUp className="w-3.5 h-3.5" /> Buy vol IVR &lt; 20% ({buyVol.length})
</div>
<div className="space-y-1.5">{buyVol.map(item => <WatchlistRow key={item.ticker} item={item} />)}</div>
</div>
@@ -478,14 +478,14 @@ export default function OptionsLab() {
{neutral.length > 0 && (
<div>
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 flex items-center gap-1.5">
<Minus className="w-3.5 h-3.5" /> Zone neutre IVR 2080% ({neutral.length})
<Minus className="w-3.5 h-3.5" /> Neutral zone IVR 2080% ({neutral.length})
</div>
<div className="space-y-1.5">{neutral.map(item => <WatchlistRow key={item.ticker} item={item} />)}</div>
</div>
)}
{noData.length > 0 && (
<div>
<div className="text-xs text-slate-500 uppercase tracking-wide mb-2">Sans historique ({noData.length})</div>
<div className="text-xs text-slate-500 uppercase tracking-wide mb-2">No history ({noData.length})</div>
<div className="space-y-1.5">{noData.map(item => <WatchlistRow key={item.ticker} item={item} />)}</div>
</div>
)}

View File

@@ -16,14 +16,14 @@ function jaccard(a: string[], b: string[]): number {
const TRIGGERS = ['military', 'sanctions', 'elections', 'natural_disaster', 'health_crisis', 'resource_scarcity', 'trade_war', 'energy', 'political_speech', 'financial_crisis']
const ASSET_CLASSES = ['energy', 'metals', 'agriculture', 'equities', 'indices', 'forex', 'rates']
const ASSET_CLASS_LABELS: Record<string, string> = {
energy: '⚡ Énergie', metals: '🥇 Métaux', agriculture: '🌾 Agri',
equities: '📈 Actions', indices: '📊 Indices', forex: '💱 Forex', rates: '📉 Taux',
energy: '⚡ Energy', metals: '🥇 Metals', agriculture: '🌾 Agri',
equities: '📈 Equities', indices: '📊 Indices', forex: '💱 Forex', rates: '📉 Rates',
}
const TRIGGER_LABELS: Record<string, string> = {
military: '⚔️ Militaire', sanctions: '🚫 Sanctions', elections: '🗳️ Élections',
natural_disaster: '🌪️ Catastrophe', health_crisis: '🏥 Santé',
resource_scarcity: '⚠️ Ressources', trade_war: '🤝 Commerce',
energy: '⚡ Énergie', political_speech: '🎙️ Discours', financial_crisis: '💸 Finance',
military: '⚔️ Military', sanctions: '🚫 Sanctions', elections: '🗳️ Elections',
natural_disaster: '🌪️ Disaster', health_crisis: '🏥 Health',
resource_scarcity: '⚠️ Resources', trade_war: '🤝 Trade',
energy: '⚡ Energy', political_speech: '🎙️ Speech', financial_crisis: '💸 Finance',
}
const EMPTY_PATTERN = {
@@ -35,7 +35,7 @@ const EMPTY_PATTERN = {
function QualityBadge({ score }: { score: number }) {
const color = score >= 75 ? 'badge-green' : score >= 50 ? 'badge-yellow' : 'badge-red'
const label = score >= 75 ? 'Excellent' : score >= 50 ? 'Bon' : 'Faible'
const label = score >= 75 ? 'Excellent' : score >= 50 ? 'Good' : 'Weak'
return <span className={clsx('badge', color)}>{score}/100 {label}</span>
}
@@ -72,7 +72,7 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
<div className="flex items-center gap-2 mb-0.5 flex-wrap">
<div className="text-sm font-semibold text-white">{p.name}</div>
{isCustom && <span className="badge badge-purple text-xs">Custom</span>}
{!isActive && <span className="badge badge-red text-xs">Désactivé</span>}
{!isActive && <span className="badge badge-red text-xs">Disabled</span>}
{p.ai_quality_score && <QualityBadge score={p.ai_quality_score} />}
<ReliabilityBadge rel={reliability} />
</div>
@@ -83,15 +83,15 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
<div className={clsx('text-sm font-bold', p.expected_move_pct > 0 ? 'positive' : 'negative')}>
{p.expected_move_pct > 0 ? '+' : ''}{p.expected_move_pct}%
</div>
<div className="text-xs text-slate-600">{p.horizon_days}j</div>
<div className="text-xs text-slate-600">{p.horizon_days}d</div>
</div>
<button onClick={onEdit} title="Modifier" className="text-slate-500 hover:text-blue-400 transition-colors">
<button onClick={onEdit} title="Edit" className="text-slate-500 hover:text-blue-400 transition-colors">
<Edit3 className="w-4 h-4" />
</button>
<button onClick={onToggle} title={isActive ? 'Désactiver' : 'Activer'} className={clsx('transition-colors', isActive ? 'text-slate-500 hover:text-yellow-400' : 'text-yellow-500 hover:text-yellow-300')}>
<button onClick={onToggle} title={isActive ? 'Disable' : 'Enable'} className={clsx('transition-colors', isActive ? 'text-slate-500 hover:text-yellow-400' : 'text-yellow-500 hover:text-yellow-300')}>
{isActive ? <Eye className="w-4 h-4" /> : <EyeOff className="w-4 h-4" />}
</button>
<button onClick={onDelete} title="Supprimer" className="text-slate-500 hover:text-red-400 transition-colors">
<button onClick={onDelete} title="Delete" className="text-slate-500 hover:text-red-400 transition-colors">
<Trash2 className="w-4 h-4" />
</button>
</div>
@@ -101,7 +101,7 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
<div className="flex flex-wrap gap-1 mb-2">
{similarTo.map((s, i) => (
<span key={i} className="inline-flex items-center gap-1 text-[10px] bg-amber-900/30 border border-amber-600/30 text-amber-400 rounded px-1.5 py-0.5">
Similaire à <span className="font-semibold max-w-[140px] truncate">{s.name}</span>
Similar to <span className="font-semibold max-w-[140px] truncate">{s.name}</span>
<span className="font-mono">{Math.round(s.similarity * 100)}%</span>
</span>
))}
@@ -115,7 +115,7 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
<span className="badge badge-blue text-xs">{p.asset_class}</span>
{aiScore != null ? (
<span className={clsx('badge text-xs font-bold', aiScore >= 50 ? 'badge-green' : aiScore >= 25 ? 'badge-yellow' : 'badge-red')}>
{aiScore}/100 IA
{aiScore}/100 AI
</span>
) : (
<span className="badge badge-purple text-xs">{Math.round(p.probability * 100)}% prob.</span>
@@ -123,14 +123,14 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
</div>
<button onClick={() => setExpanded(!expanded)} className="text-xs text-slate-600 hover:text-slate-400">
{expanded ? '▲ Réduire' : '▼ Voir détails'}
{expanded ? '▲ Collapse' : '▼ See details'}
</button>
{expanded && (
<div className="mt-3 space-y-2">
{p.keywords?.length > 0 && (
<div>
<div className="text-xs text-slate-600 mb-1">Mots-clés de détection</div>
<div className="text-xs text-slate-600 mb-1">Detection keywords</div>
<div className="flex flex-wrap gap-1">
{p.keywords.map((kw: string) => (
<span key={kw} className="text-xs bg-dark-700 text-slate-400 px-1.5 py-0.5 rounded">#{kw}</span>
@@ -140,7 +140,7 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
)}
{p.historical_instances?.length > 0 && (
<div>
<div className="text-xs text-slate-600 mb-1">Instances historiques</div>
<div className="text-xs text-slate-600 mb-1">Historical instances</div>
{p.historical_instances.map((h: any, i: number) => (
<div key={i} className="card-sm mb-1">
<span className="text-xs text-slate-500">{h.date}</span>
@@ -151,7 +151,7 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
)}
{p.suggested_trades?.length > 0 && (
<div>
<div className="text-xs text-slate-600 mb-1">Trades suggérés</div>
<div className="text-xs text-slate-600 mb-1">Suggested trades</div>
{p.suggested_trades.map((t: any, i: number) => (
<div key={i} className="flex items-center gap-2 text-xs mb-1 flex-wrap">
<span className="badge badge-green">{t.strategy}</span>
@@ -165,11 +165,11 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
{(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
<ShieldAlert className="w-3 h-3" /> Counter-thesis & 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}
<span className="text-slate-500">Counter-thesis:</span> {p.counter_thesis}
</div>
)}
{p.invalidation_trigger && (
@@ -184,7 +184,7 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
)}
{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>
<div className="text-xs text-blue-400 font-semibold mb-1">AI Evaluation</div>
{p.ai_evaluation.strengths?.length > 0 && (
<div className="text-xs text-emerald-400 mb-1"> {p.ai_evaluation.strengths[0]}</div>
)}
@@ -233,10 +233,10 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
<div className="flex items-center justify-between p-4 border-b border-slate-700/30">
<div>
<h2 className="text-base font-semibold text-white flex items-center gap-2">
<Sparkles className="w-4 h-4 text-blue-400" /> Suggérer des patterns par l'IA
<Sparkles className="w-4 h-4 text-blue-400" /> Suggest patterns with AI
</h2>
<p className="text-xs text-slate-500 mt-0.5">
GPT-4o analyse l'actualité géo, les prix, le calendrier et le <span className="text-blue-400">régime macro actuel</span> pour proposer des patterns cohérents avec le scénario dominant
GPT-4o analyzes geo news, prices, calendar and the <span className="text-blue-400">current macro regime</span> to propose patterns aligned with the dominant scenario
</p>
</div>
<button onClick={onClose} className="text-slate-500 hover:text-white"><X className="w-5 h-5" /></button>
@@ -248,11 +248,11 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
<div className="text-center py-12">
<Sparkles className="w-10 h-10 text-blue-400/40 mx-auto mb-3" />
<p className="text-sm text-slate-400 mb-4">
L'IA va analyser les news géopolitiques actuelles, les mouvements de prix et le calendrier économique pour proposer des patterns pertinents <em>aujourd'hui</em>.
The AI will analyze current geopolitical news, price movements, and the economic calendar to propose patterns relevant <em>today</em>.
</p>
<button onClick={() => suggest()}
className="bg-blue-600 hover:bg-blue-500 text-white px-6 py-2 rounded text-sm font-semibold">
Lancer l'analyse (GPT-4o)
Run analysis (GPT-4o)
</button>
</div>
)}
@@ -260,7 +260,7 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
{isPending && (
<div className="text-center py-12">
<div className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto mb-3"></div>
<p className="text-sm text-slate-400">GPT-4o analyse le contexte du moment…</p>
<p className="text-sm text-slate-400">GPT-4o analyzing current context</p>
</div>
)}
@@ -277,7 +277,7 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
<span className={clsx('badge text-xs font-bold', score >= 50 ? 'badge-green' : score >= 25 ? 'badge-yellow' : 'badge-red')}>
{score}/100 IA
</span>
<span className="text-xs text-slate-500">{p.horizon_days}j · {p.expected_move_pct > 0 ? '+' : ''}{p.expected_move_pct}%</span>
<span className="text-xs text-slate-500">{p.horizon_days}d · {p.expected_move_pct > 0 ? '+' : ''}{p.expected_move_pct}%</span>
</div>
</div>
<button
@@ -287,7 +287,7 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
'bg-emerald-700/30 border border-emerald-700/40 text-emerald-400 cursor-default': saved.has(idx),
'bg-blue-600 hover:bg-blue-500 text-white': !saved.has(idx),
})}>
{saved.has(idx) ? <><Check className="w-3 h-3" /> Sauvegardé</> : <><Save className="w-3 h-3" /> Sauvegarder</>}
{saved.has(idx) ? <><Check className="w-3 h-3" /> Saved</> : <><Save className="w-3 h-3" /> Save</>}
</button>
</div>
@@ -295,7 +295,7 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
<div className="flex flex-wrap gap-1 mb-2">
{similars.map((s, i) => (
<span key={i} className="inline-flex items-center gap-1 text-[10px] bg-amber-900/30 border border-amber-600/30 text-amber-400 rounded px-1.5 py-0.5">
Similaire à <span className="font-semibold max-w-[140px] truncate">{s.name}</span>
Similar to <span className="font-semibold max-w-[140px] truncate">{s.name}</span>
<span className="font-mono">{Math.round(s.similarity * 100)}%</span>
</span>
))}
@@ -312,7 +312,7 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
{(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>
<div className="text-slate-300"><span className="text-red-400/80"> Counter-thesis:</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>
@@ -336,7 +336,7 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
{data && suggestions.length === 0 && (
<div className="card text-center py-8 text-slate-500 text-sm">
Aucun pattern suggéré. Réessayer.
No patterns suggested. Try again.
</div>
)}
</div>
@@ -345,18 +345,18 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
<div className="p-4 border-t border-slate-700/30 flex justify-between items-center">
{suggestions.length > 0 && (
<span className="text-xs text-slate-500">
{saved.size}/{suggestions.length} sauvegardé{saved.size > 1 ? 's' : ''}
{saved.size}/{suggestions.length} saved
</span>
)}
<div className="flex gap-2 ml-auto">
{data && suggestions.length > 0 && (
<button onClick={() => suggest()} disabled={isPending}
className="flex items-center gap-1 border border-slate-700 text-slate-400 hover:text-slate-200 px-3 py-1.5 rounded text-xs">
<RotateCcw className="w-3 h-3" /> Régénérer
<RotateCcw className="w-3 h-3" /> Regenerate
</button>
)}
<button onClick={onClose} className="border border-slate-700 text-slate-400 hover:text-slate-200 px-4 py-1.5 rounded text-xs">
Fermer
Close
</button>
</div>
</div>
@@ -431,18 +431,18 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
{/* AI Suggest from context */}
{aiStatus?.enabled && (
<div className="card border-blue-500/30">
<div className="section-title flex items-center gap-1"><Brain className="w-3 h-3 text-blue-400" /> Générer depuis un contexte (IA)</div>
<div className="section-title flex items-center gap-1"><Brain className="w-3 h-3 text-blue-400" /> Generate from context (AI)</div>
<div className="flex gap-2">
<textarea
value={suggestInput}
onChange={e => setSuggestInput(e.target.value)}
placeholder="Décris le contexte géopolitique... ex: 'Quand la Chine impose des restrictions sur les terres rares, que se passe-t-il sur le marché des semi-conducteurs et des métaux ?'"
placeholder="Describe the geopolitical context... e.g. 'When China imposes restrictions on rare earths, what happens to semiconductor and metals markets?'"
rows={2}
className="flex-1 bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500 resize-none"
/>
<button onClick={suggestFromContext} disabled={suggestLoading || !suggestInput}
className="shrink-0 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-3 rounded text-xs">
{suggestLoading ? '...' : ' Générer'}
{suggestLoading ? '...' : '⚡ Generate'}
</button>
</div>
</div>
@@ -451,22 +451,22 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
{/* Form fields */}
<div className="grid grid-cols-2 gap-3">
<div className="col-span-2">
<label className="text-xs text-slate-500 mb-1 block">Nom du pattern</label>
<label className="text-xs text-slate-500 mb-1 block">Pattern name</label>
<input value={form.name} onChange={e => set('name', e.target.value)}
placeholder="Ex: Chine tensions Taiwan → Semi-conducteurs spike"
placeholder="E.g.: China Taiwan tensions → Semiconductors spike"
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-blue-500" />
</div>
<div className="col-span-2">
<label className="text-xs text-slate-500 mb-1 block">Description</label>
<textarea value={form.description} onChange={e => set('description', e.target.value)}
rows={2} placeholder="Mécanisme géopolitique → impact marché..."
rows={2} placeholder="Geopolitical mechanism → market impact..."
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-blue-500 resize-none" />
</div>
</div>
{/* Triggers */}
<div>
<label className="text-xs text-slate-500 mb-2 block">Déclencheurs</label>
<label className="text-xs text-slate-500 mb-2 block">Triggers</label>
<div className="flex flex-wrap gap-1.5">
{TRIGGERS.map(t => (
<button key={t} onClick={() => toggleTrigger(t)}
@@ -482,11 +482,11 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
{/* Keywords */}
<div>
<label className="text-xs text-slate-500 mb-1 block">Mots-clés de détection (anglais)</label>
<label className="text-xs text-slate-500 mb-1 block">Detection keywords (English)</label>
<div className="flex gap-2 mb-2">
<input value={kwInput} onChange={e => setKwInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && addKeyword()}
placeholder="Ajouter un mot-clé..."
placeholder="Add a keyword..."
className="flex-1 bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500" />
<button onClick={addKeyword} className="bg-dark-600 border border-slate-700 hover:border-slate-500 text-slate-300 px-3 rounded text-xs">+</button>
</div>
@@ -504,48 +504,48 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
{/* Metrics */}
<div className="grid grid-cols-4 gap-3">
<div>
<label className="text-xs text-slate-500 mb-1 block">Classe d'actif</label>
<label className="text-xs text-slate-500 mb-1 block">Asset class</label>
<select value={form.asset_class} onChange={e => set('asset_class', e.target.value)}
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-blue-500">
{ASSET_CLASSES.map(a => <option key={a}>{a}</option>)}
</select>
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Move attendu (%)</label>
<label className="text-xs text-slate-500 mb-1 block">Expected move (%)</label>
<input type="number" value={form.expected_move_pct} onChange={e => set('expected_move_pct', Number(e.target.value))}
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-blue-500" />
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Probabilité (0-1)</label>
<label className="text-xs text-slate-500 mb-1 block">Probability (0-1)</label>
<input type="number" step="0.05" min="0" max="1" value={form.probability} onChange={e => set('probability', Number(e.target.value))}
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-blue-500" />
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Horizon (jours)</label>
<label className="text-xs text-slate-500 mb-1 block">Horizon (days)</label>
<input type="number" value={form.horizon_days} onChange={e => set('horizon_days', Number(e.target.value))}
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-blue-500" />
</div>
</div>
{/* Contre-thèse & Invalidation */}
{/* Counter-thesis & 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)
<ShieldAlert className="w-3 h-3 text-red-400" /> Counter-thesis (main adverse scenario)
</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…"
rows={2} placeholder="E.g.: unexpected peace deal, inflation below 3%, Fed pivot…"
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 &amp; mesurable)</label>
<label className="text-xs text-slate-500 mb-1 block">Invalidation trigger (precise &amp; measurable event)</label>
<input value={form.invalidation_trigger || ''} onChange={e => set('invalidation_trigger', e.target.value)}
placeholder="Ex: prix pétrole < 70$/b 3j consécutifs"
placeholder="E.g.: oil price < $70/b for 3 consecutive days"
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>
<label className="text-xs text-slate-500 mb-1 block">Invalidation prob. (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"
@@ -559,19 +559,19 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
<div className="card border-blue-700/40">
<div className="flex items-center justify-between mb-2">
<div className="text-sm font-semibold text-blue-400 flex items-center gap-1">
<Brain className="w-4 h-4" /> Évaluation IA
<Brain className="w-4 h-4" /> AI Evaluation
</div>
<QualityBadge score={aiResult.quality_score || 0} />
</div>
<div className="grid grid-cols-2 gap-3 text-xs">
<div>
<div className="text-slate-500 mb-1 font-semibold">✓ Points forts</div>
<div className="text-slate-500 mb-1 font-semibold"> Strengths</div>
{aiResult.strengths?.map((s: string, i: number) => (
<div key={i} className="text-emerald-400 mb-0.5"> {s}</div>
))}
</div>
<div>
<div className="text-slate-500 mb-1 font-semibold">⚠ Faiblesses</div>
<div className="text-slate-500 mb-1 font-semibold"> Weaknesses</div>
{aiResult.weaknesses?.map((w: string, i: number) => (
<div key={i} className="text-orange-400 mb-0.5"> {w}</div>
))}
@@ -579,7 +579,7 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
</div>
{aiResult.suggested_improvements?.additional_keywords?.length > 0 && (
<div className="mt-2">
<div className="text-xs text-slate-500 mb-1">Mots-clés suggérés par l'IA</div>
<div className="text-xs text-slate-500 mb-1">Keywords suggested by AI</div>
<div className="flex flex-wrap gap-1">
{aiResult.suggested_improvements.additional_keywords.map((kw: string) => (
<button key={kw} onClick={() => set('keywords', [...form.keywords, kw])}
@@ -597,7 +597,7 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
)}
{aiResult.counter_scenarios?.length > 0 && (
<div className="mt-2">
<div className="text-xs text-slate-500 mb-1">Scénarios d'invalidation</div>
<div className="text-xs text-slate-500 mb-1">Invalidation scenarios</div>
{aiResult.counter_scenarios.map((s: string, i: number) => (
<div key={i} className="text-xs text-red-400 mb-0.5"> {s}</div>
))}
@@ -611,15 +611,15 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
<button onClick={evaluateWithAI} disabled={aiLoading || !form.name}
className="flex items-center gap-1.5 border border-blue-500/50 text-blue-400 hover:bg-blue-900/20 px-3 py-1.5 rounded text-sm transition-colors disabled:opacity-40">
<Brain className="w-3.5 h-3.5" />
{aiLoading ? 'Évaluation IA...' : 'Évaluer avec IA'}
{aiLoading ? 'AI Evaluation...' : 'Evaluate with AI'}
</button>
)}
<button onClick={onCancel} className="flex items-center gap-1.5 border border-slate-700 text-slate-400 hover:text-slate-200 px-3 py-1.5 rounded text-sm">
<RotateCcw className="w-3.5 h-3.5" /> Annuler
<RotateCcw className="w-3.5 h-3.5" /> Cancel
</button>
<button onClick={() => onSave(form)} disabled={!form.name || form.triggers.length === 0}
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-1.5 rounded text-sm font-semibold ml-auto">
<Save className="w-3.5 h-3.5" /> Sauvegarder le pattern
<Save className="w-3.5 h-3.5" /> Save pattern
</button>
</div>
</div>
@@ -725,23 +725,23 @@ export default function PatternEditor() {
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold text-white flex items-center gap-2">
<Zap className="w-5 h-5 text-blue-400" /> Patterns Géopolitiques
<Zap className="w-5 h-5 text-blue-400" /> Geopolitical Patterns
</h1>
<p className="text-xs text-slate-500 mt-0.5">
{patterns?.length ?? 0} patterns ·{' '}
{patterns?.filter((p: any) => p.source === 'custom').length ?? 0} personnalisés
{patterns?.filter((p: any) => p.source === 'custom').length ?? 0} custom
</p>
</div>
<div className="flex gap-2">
{aiStatus?.enabled && (
<button onClick={() => setShowAiSuggest(true)}
className="flex items-center gap-1.5 border border-blue-500/50 text-blue-400 hover:bg-blue-900/20 px-3 py-1.5 rounded text-sm font-semibold transition-colors">
<Sparkles className="w-4 h-4" /> Suggérer par l'IA
<Sparkles className="w-4 h-4" /> Suggest with AI
</button>
)}
<button onClick={() => { setCreating(true); setEditing(null) }}
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 text-white px-3 py-1.5 rounded text-sm font-semibold">
<Plus className="w-4 h-4" /> Créer un pattern
<Plus className="w-4 h-4" /> Create pattern
</button>
</div>
</div>
@@ -749,7 +749,7 @@ export default function PatternEditor() {
{(creating || editing) && (
<div className="card border-blue-500/30">
<div className="text-sm font-semibold text-white mb-4">
{editing ? `Modifier: ${editing.name}` : 'Nouveau pattern géopolitique'}
{editing ? `Edit: ${editing.name}` : 'New geopolitical pattern'}
</div>
<PatternForm
initial={editing}
@@ -763,9 +763,9 @@ export default function PatternEditor() {
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex gap-1 bg-dark-700 p-1 rounded">
{[
{ key: 'all', label: `Tous (${patterns?.length ?? 0})` },
{ key: 'builtin', label: `Intégrés (${patterns?.filter((p: any) => p.source === 'builtin').length ?? 0})` },
{ key: 'custom', label: `Mes patterns (${patterns?.filter((p: any) => p.source === 'custom').length ?? 0})` },
{ key: 'all', label: `All (${patterns?.length ?? 0})` },
{ key: 'builtin', label: `Built-in (${patterns?.filter((p: any) => p.source === 'builtin').length ?? 0})` },
{ key: 'custom', label: `My patterns (${patterns?.filter((p: any) => p.source === 'custom').length ?? 0})` },
].map(({ key, label }) => (
<button key={key} onClick={() => setTab(key as any)}
className={clsx('px-3 py-1.5 rounded text-sm', {
@@ -799,9 +799,9 @@ export default function PatternEditor() {
{/* Period filter */}
<div className="flex gap-1 bg-dark-700 p-1 rounded">
{([
{ key: null, label: 'Tout' },
{ key: 7, label: '7j' },
{ key: 30, label: '30j' },
{ key: null, label: 'All' },
{ key: 7, label: '7d' },
{ key: 30, label: '30d' },
] as const).map(({ key, label }) => (
<button key={String(key)} onClick={() => setFilterDays(key)}
className={clsx('px-2 py-1 rounded text-xs transition-colors', {
@@ -834,7 +834,7 @@ export default function PatternEditor() {
'bg-slate-600/40 border-slate-500/60 text-white': filterAsset === 'all',
'border-slate-700/40 text-slate-500 hover:border-slate-500 hover:text-slate-300': filterAsset !== 'all',
})}>
Tous types
All types
</button>
{ASSET_CLASSES.map(ac => (
<button key={ac} onClick={() => setFilterAsset(filterAsset === ac ? 'all' : ac)}
@@ -847,7 +847,7 @@ export default function PatternEditor() {
))}
{(filterAsset !== 'all' || filterDays !== null) && (
<span className="text-xs text-slate-600 ml-1">
{displayPatterns.length} résultat{displayPatterns.length !== 1 ? 's' : ''}
{displayPatterns.length} result{displayPatterns.length !== 1 ? 's' : ''}
</span>
)}
</div>
@@ -874,12 +874,12 @@ export default function PatternEditor() {
<Zap className="w-8 h-8 mx-auto mb-2 opacity-20" />
<div>
{filterAsset !== 'all'
? `Aucun pattern dans "${ASSET_CLASS_LABELS[filterAsset]}"`
? `No patterns in "${ASSET_CLASS_LABELS[filterAsset]}"`
: filterDays !== null
? `Aucun pattern dans les ${filterDays} derniers jours`
: 'Aucun pattern personnalisé'}
? `No patterns in the last ${filterDays} days`
: 'No custom patterns'}
</div>
<div className="text-xs mt-1">Créer un pattern avec l'aide de l'IA</div>
<div className="text-xs mt-1">Create a pattern with AI assistance</div>
</div>
)}
</div>

View File

@@ -86,7 +86,7 @@ function AddPositionModal({ prefill, onClose }: AddModalProps) {
onSuccess: onClose,
onError: (err: unknown) => {
const detail = (err as { response?: { data?: { detail?: string } } })?.response?.data?.detail
setAddError(detail || 'Erreur lors de l\'ajout de la position')
setAddError(detail || 'Error adding position')
}
})
}
@@ -96,38 +96,38 @@ function AddPositionModal({ prefill, onClose }: AddModalProps) {
<div className="card w-full max-w-lg max-h-[90vh] overflow-y-auto">
<div className="flex items-center justify-between mb-4">
<h2 className="text-base font-bold text-white flex items-center gap-2">
<Plus className="w-4 h-4 text-blue-400" /> Ajouter au portefeuille
<Plus className="w-4 h-4 text-blue-400" /> Add to portfolio
</h2>
<button onClick={onClose} className="text-slate-500 hover:text-slate-200"><X className="w-4 h-4" /></button>
</div>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs text-slate-500 mb-1 block">Titre</label>
<label className="text-xs text-slate-500 mb-1 block">Title</label>
<input value={form.title} onChange={e => set('title', e.target.value)}
placeholder="Ex: Long Call GLD Q3 2026"
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-blue-500" />
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">
Sous-jacent <span className="text-slate-600">(ticker Yahoo Finance)</span>
Underlying <span className="text-slate-600">(Yahoo Finance ticker)</span>
</label>
<input value={form.underlying} onChange={e => { set('underlying', e.target.value.toUpperCase()); setAddError(null) }}
placeholder="^GSPC, GLD, CL=F..."
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-blue-500" />
<div className="text-xs text-slate-600 mt-0.5">S&amp;P 500: ^GSPC · Or: GC=F · WTI: CL=F · GLD · USO</div>
<div className="text-xs text-slate-600 mt-0.5">S&amp;P 500: ^GSPC · Gold: GC=F · WTI: CL=F · GLD · USO</div>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs text-slate-500 mb-1 block">Stratégie</label>
<label className="text-xs text-slate-500 mb-1 block">Strategy</label>
<select value={form.strategy} onChange={e => set('strategy', e.target.value)}
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-blue-500">
{STRATEGIES.map(s => <option key={s}>{s}</option>)}
</select>
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Classe d'actif</label>
<label className="text-xs text-slate-500 mb-1 block">Asset class</label>
<select value={form.asset_class} onChange={e => set('asset_class', e.target.value)}
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-blue-500">
{ASSET_CLASSES.map(s => <option key={s}>{s}</option>)}
@@ -138,7 +138,7 @@ function AddPositionModal({ prefill, onClose }: AddModalProps) {
<div>
<label className="text-xs text-slate-500 mb-1 block">Strike</label>
<input type="number" value={form.strike} onChange={e => set('strike', e.target.value)}
placeholder="Prix d'exercice"
placeholder="Exercise price"
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-blue-500" />
</div>
<div>
@@ -150,45 +150,45 @@ function AddPositionModal({ prefill, onClose }: AddModalProps) {
</select>
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Prime payée</label>
<label className="text-xs text-slate-500 mb-1 block">Premium paid</label>
<input type="number" value={form.premium} onChange={e => set('premium', e.target.value)}
placeholder="Prix option"
placeholder="Option price"
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-blue-500" />
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs text-slate-500 mb-1 block">Capital investi (€)</label>
<label className="text-xs text-slate-500 mb-1 block">Capital invested ()</label>
<input type="number" value={form.capital_invested} onChange={e => set('capital_invested', Number(e.target.value))}
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-blue-500" />
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Durée (jours)</label>
<label className="text-xs text-slate-500 mb-1 block">Duration (days)</label>
<input type="number" value={form.expiry_days} onChange={e => set('expiry_days', Number(e.target.value))}
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-blue-500" />
</div>
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Déclencheur géopolitique</label>
<label className="text-xs text-slate-500 mb-1 block">Geopolitical trigger</label>
<input value={form.geo_trigger} onChange={e => set('geo_trigger', e.target.value)}
placeholder="Ex: Middle East escalation → oil spike"
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-blue-500" />
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Raisonnement</label>
<label className="text-xs text-slate-500 mb-1 block">Rationale</label>
<textarea value={form.rationale} onChange={e => set('rationale', e.target.value)}
rows={2} placeholder="Pourquoi ce trade..."
rows={2} placeholder="Why this trade..."
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-blue-500 resize-none" />
</div>
{isStraddle && (
<div className="bg-blue-900/30 border border-blue-700/30 rounded p-2 text-xs text-blue-300">
Straddle : 2 legs seront créés automatiquement (CALL + PUT au même strike)
Straddle: 2 legs will be created automatically (CALL + PUT at the same strike)
</div>
)}
<div className="bg-dark-700 rounded p-2 text-xs text-slate-500">
<div className="font-semibold text-slate-400 mb-1">Frais IB simulés</div>
<div>Options: $0.65/contrat (min $1.00) à l'entrée + sortie</div>
<div>1 contrat = <span className="text-white">$0.65</span> · 5 contrats = <span className="text-white">$3.25</span></div>
<div className="font-semibold text-slate-400 mb-1">Simulated IB fees</div>
<div>Options: $0.65/contract (min $1.00) at entry + exit</div>
<div>1 contract = <span className="text-white">$0.65</span> · 5 contracts = <span className="text-white">$3.25</span></div>
</div>
{addError && (
<div className="bg-red-900/40 border border-red-700/40 rounded p-2 text-xs text-red-300">
@@ -197,7 +197,7 @@ function AddPositionModal({ prefill, onClose }: AddModalProps) {
)}
<button onClick={submit} disabled={isPending || !form.underlying}
className="w-full bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white rounded py-2 text-sm font-semibold">
{isPending ? 'Ajout...' : '+ Ajouter au portefeuille'}
{isPending ? 'Adding...' : '+ Add to portfolio'}
</button>
</div>
</div>
@@ -233,7 +233,7 @@ function PositionCard({ pos }: { pos: Record<string, any> }) {
<button
onClick={() => navigate(`/markets?symbol=${pos.underlying}`)}
className="text-xs text-blue-400 hover:text-blue-300 flex items-center gap-0.5 transition-colors"
title="Voir dans les marchés"
title="View in markets"
>
{pos.underlying} <ExternalLink className="w-2.5 h-2.5" />
</button>
@@ -253,7 +253,7 @@ function PositionCard({ pos }: { pos: Record<string, any> }) {
</div>
</div>
<button onClick={() => deletePos(pos.id)} disabled={deleting}
className="text-slate-600 hover:text-red-400 transition-colors" title="Supprimer">
className="text-slate-600 hover:text-red-400 transition-colors" title="Delete">
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
@@ -262,29 +262,29 @@ function PositionCard({ pos }: { pos: Record<string, any> }) {
{/* KPI grid */}
<div className="grid grid-cols-4 gap-2 mb-3">
<div className="card-sm text-center">
<div className="text-xs text-slate-600">{hasPremium ? 'Prime payée' : 'Capital investi'}</div>
<div className="text-xs text-slate-600">{hasPremium ? 'Premium paid' : 'Capital invested'}</div>
<div className="text-xs text-white font-mono mt-0.5">{entryRef.toFixed(2)}</div>
</div>
<div className="card-sm text-center">
<div className="text-xs text-slate-600">Valeur BS actuelle</div>
<div className="text-xs text-slate-600">Current BS value</div>
<div className="text-xs text-white font-mono mt-0.5">
{pos.current_value != null ? `${pos.current_value.toFixed(2)}` : '—'}
</div>
</div>
<div className="card-sm text-center">
<div className="text-xs text-slate-600">Spot sous-jacent</div>
<div className="text-xs text-slate-600">Underlying spot</div>
<div className="text-xs text-white font-mono mt-0.5">
{pos.current_underlying != null ? `$${pos.current_underlying}` : '—'}
</div>
</div>
<div className="card-sm text-center">
<div className="text-xs text-slate-600">Jours restants</div>
<div className="text-xs text-slate-600">Days remaining</div>
<div className={clsx('text-xs font-mono mt-0.5', {
'text-red-400': (pos.days_remaining ?? 99) < 14,
'text-yellow-400': (pos.days_remaining ?? 99) < 30,
'text-white': (pos.days_remaining ?? 99) >= 30,
})}>
{pos.days_remaining != null ? `${pos.days_remaining}j` : '—'}
{pos.days_remaining != null ? `${pos.days_remaining}d` : '—'}
</div>
</div>
</div>
@@ -294,9 +294,9 @@ function PositionCard({ pos }: { pos: Record<string, any> }) {
<div className="flex items-center gap-4 text-xs mb-2">
<span className="text-slate-600">Greeks:</span>
<span>Δ <span className="text-slate-300 font-mono">{pos.greeks.net_delta?.toFixed(3)}</span></span>
<span>Θ <span className="text-red-400 font-mono">{pos.greeks.net_theta?.toFixed(3)}</span>/j</span>
<span>Θ <span className="text-red-400 font-mono">{pos.greeks.net_theta?.toFixed(3)}</span>/d</span>
<span>ν <span className="text-blue-400 font-mono">{pos.greeks.net_vega?.toFixed(3)}</span></span>
<span className="ml-auto text-slate-600">Frais IB: <span className="text-orange-400">{(pos.ib_fees_entry || 0).toFixed(2)}€</span></span>
<span className="ml-auto text-slate-600">IB fees: <span className="text-orange-400">{(pos.ib_fees_entry || 0).toFixed(2)}</span></span>
</div>
)}
@@ -307,7 +307,7 @@ function PositionCard({ pos }: { pos: Record<string, any> }) {
className="flex items-center gap-1 text-xs text-slate-600 hover:text-slate-400 mb-2 transition-colors"
>
{showDetails ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
Détail de la simulation Black-Scholes
Black-Scholes simulation detail
</button>
)}
@@ -316,14 +316,14 @@ function PositionCard({ pos }: { pos: Record<string, any> }) {
<div className="mb-3 bg-dark-700/40 rounded p-2.5 text-xs border border-slate-700/30">
<div className="flex flex-wrap items-center gap-3 mb-2 text-slate-500 border-b border-slate-700/30 pb-1.5">
{pos.entry_underlying_price != null && (
<span>Spot entrée: <span className="text-slate-300 font-mono">${Number(pos.entry_underlying_price).toFixed(2)}</span></span>
<span>Entry spot: <span className="text-slate-300 font-mono">${Number(pos.entry_underlying_price).toFixed(2)}</span></span>
)}
{pos.sigma_used != null && (
<span>σ (IV histo): <span className="text-slate-300 font-mono">{(pos.sigma_used * 100).toFixed(1)}%</span></span>
<span>σ (hist. IV): <span className="text-slate-300 font-mono">{(pos.sigma_used * 100).toFixed(1)}%</span></span>
)}
<span>r: <span className="text-slate-300">5%</span></span>
{pos.expiry_days != null && (
<span>Durée initiale: <span className="text-slate-300">{pos.expiry_days}j</span></span>
<span>Initial duration: <span className="text-slate-300">{pos.expiry_days}d</span></span>
)}
</div>
<table className="w-full mb-2">
@@ -331,9 +331,9 @@ function PositionCard({ pos }: { pos: Record<string, any> }) {
<tr className="text-slate-600">
<th className="text-left pb-1 font-normal">Leg</th>
<th className="text-left pb-1 font-normal">Strike</th>
<th className="text-right pb-1 font-normal">Qté</th>
<th className="text-right pb-1 font-normal">Prime/contrat</th>
<th className="text-right pb-1 font-normal">Coût total</th>
<th className="text-right pb-1 font-normal">Qty</th>
<th className="text-right pb-1 font-normal">Premium/contract</th>
<th className="text-right pb-1 font-normal">Total cost</th>
</tr>
</thead>
<tbody>
@@ -370,7 +370,7 @@ function PositionCard({ pos }: { pos: Record<string, any> }) {
</tbody>
</table>
<div className="text-slate-600 italic">
Black-Scholes · 1 contrat = 100 actions · Prix calculé au moment de l'ajout (spot, IV historique, r=5%)
Black-Scholes · 1 contract = 100 shares · Price computed at entry time (spot, historical IV, r=5%)
</div>
</div>
)}
@@ -403,29 +403,29 @@ function PositionCard({ pos }: { pos: Record<string, any> }) {
<div className="flex gap-2">
<button onClick={() => setShowClose(!showClose)}
className="flex-1 text-xs border border-slate-700 hover:border-emerald-600 text-slate-400 hover:text-emerald-400 rounded py-1.5 transition-colors">
{showClose ? 'Annuler' : '💰 Clôturer la position'}
{showClose ? 'Cancel' : '💰 Close position'}
</button>
</div>
{showClose && (
<div className="mt-2 space-y-1">
<div className="text-xs text-slate-500">Valeur de revente (€) — ex: {pos.current_value?.toFixed(2)}</div>
<div className="text-xs text-slate-500">Resale value () e.g.: {pos.current_value?.toFixed(2)}</div>
<div className="flex gap-2">
<input type="number" value={closeVal} onChange={e => setCloseVal(e.target.value)}
placeholder={`Valeur actuelle: ~${pos.current_value?.toFixed(2)}€`}
placeholder={`Current value: ~${pos.current_value?.toFixed(2)}`}
className="flex-1 bg-dark-700 border border-slate-700 rounded px-2 py-1 text-sm text-white focus:outline-none focus:border-blue-500" />
<button
onClick={() => closePos({ id: pos.id, close_value: parseFloat(closeVal) || 0 },
{ onSuccess: () => setShowClose(false) })}
disabled={closing || !closeVal}
className="bg-emerald-700 hover:bg-emerald-600 disabled:opacity-40 text-white px-3 rounded text-xs font-semibold">
{closing ? '...' : 'Confirmer'}
{closing ? '...' : 'Confirm'}
</button>
</div>
{closeVal && (
<div className={clsx('text-xs', parseFloat(closeVal) - entryRef - (pos.ib_fees_entry||0) - 1 >= 0 ? 'positive' : 'negative')}>
P&L net IB: {(parseFloat(closeVal) - entryRef - (pos.ib_fees_entry||0) - 1).toFixed(2)}€
(frais sortie: ~$1.00)
Net IB P&L: {(parseFloat(closeVal) - entryRef - (pos.ib_fees_entry||0) - 1).toFixed(2)}
(exit fees: ~$1.00)
</div>
)}
</div>
@@ -449,7 +449,7 @@ function ClosedRow({ pos, fees, pnl }: { pos: Record<string, any>; fees: number;
<td className="py-1.5 text-slate-500">{pos.close_date?.slice(0, 10)}</td>
<td className="py-1.5 pl-2">
<button onClick={() => deletePos(pos.id)} disabled={isPending}
className="opacity-0 group-hover:opacity-100 text-slate-600 hover:text-red-400 transition-all" title="Supprimer">
className="opacity-0 group-hover:opacity-100 text-slate-600 hover:text-red-400 transition-all" title="Delete">
<Trash2 className="w-3.5 h-3.5" />
</button>
</td>
@@ -470,19 +470,19 @@ export default function Portfolio() {
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold text-white flex items-center gap-2">
<DollarSign className="w-5 h-5 text-blue-400" /> Portefeuille
<DollarSign className="w-5 h-5 text-blue-400" /> Portfolio
</h1>
<p className="text-xs text-slate-500 mt-0.5">
Suivi temps réel · Mark-to-market Black-Scholes · Frais IB simulés
Real-time tracking · Mark-to-market Black-Scholes · Simulated IB fees
</p>
</div>
<div className="flex gap-2">
<button onClick={() => refetch()} className="flex items-center gap-1 text-xs text-slate-400 hover:text-slate-200">
<RefreshCw className="w-3.5 h-3.5" /> Actualiser
<RefreshCw className="w-3.5 h-3.5" /> Refresh
</button>
<button onClick={() => setShowModal(true)}
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 text-white px-3 py-1.5 rounded text-sm font-semibold">
<Plus className="w-4 h-4" /> Nouvelle position
<Plus className="w-4 h-4" /> New position
</button>
</div>
</div>
@@ -491,11 +491,11 @@ export default function Portfolio() {
{summary && (
<div className="grid grid-cols-5 gap-3">
{[
{ label: 'Positions ouvertes', value: summary.open_positions, color: 'text-white' },
{ label: 'Capital engagé', value: `${summary.total_invested}€`, color: 'text-white' },
{ label: 'P&L latent', value: `${summary.unrealized_pnl >= 0 ? '+' : ''}${summary.unrealized_pnl}€`, color: summary.unrealized_pnl >= 0 ? 'text-emerald-400' : 'text-red-400' },
{ label: 'P&L réalisé', value: `${summary.realized_pnl >= 0 ? '+' : ''}${summary.realized_pnl}€`, color: summary.realized_pnl >= 0 ? 'text-emerald-400' : 'text-red-400' },
{ label: 'Frais IB total', value: `${summary.total_fees_paid}€`, color: 'text-orange-400' },
{ label: 'Open positions', value: summary.open_positions, color: 'text-white' },
{ label: 'Capital deployed', value: `${summary.total_invested}`, color: 'text-white' },
{ label: 'Unrealized P&L', value: `${summary.unrealized_pnl >= 0 ? '+' : ''}${summary.unrealized_pnl}`, color: summary.unrealized_pnl >= 0 ? 'text-emerald-400' : 'text-red-400' },
{ label: 'Realized P&L', value: `${summary.realized_pnl >= 0 ? '+' : ''}${summary.realized_pnl}`, color: summary.realized_pnl >= 0 ? 'text-emerald-400' : 'text-red-400' },
{ label: 'Total IB fees', value: `${summary.total_fees_paid}`, color: 'text-orange-400' },
].map(({ label, value, color }) => (
<div key={label} className="card-sm text-center">
<div className="stat-label">{label}</div>
@@ -508,9 +508,9 @@ export default function Portfolio() {
{/* Tabs */}
<div className="flex gap-1 bg-dark-700 p-1 rounded w-fit">
{[
{ key: 'open', label: `Positions ouvertes (${positions?.length ?? 0})` },
{ key: 'closed', label: `Clôturées (${closed?.length ?? 0})` },
{ key: 'history', label: 'Courbe P&L' },
{ key: 'open', label: `Open positions (${positions?.length ?? 0})` },
{ key: 'closed', label: `Closed (${closed?.length ?? 0})` },
{ key: 'history', label: 'P&L curve' },
].map(({ key, label }) => (
<button key={key} onClick={() => setTab(key as any)}
className={clsx('px-3 py-1.5 rounded text-sm transition-colors', {
@@ -538,8 +538,8 @@ export default function Portfolio() {
) : (
<div className="card text-center py-12 text-slate-500">
<DollarSign className="w-10 h-10 mx-auto mb-3 opacity-20" />
<div>Aucune position ouverte</div>
<div className="text-xs mt-1">Ajouter un trade depuis les idées ou manuellement</div>
<div>No open positions</div>
<div className="text-xs mt-1">Add a trade from ideas or manually</div>
</div>
)}
</>
@@ -552,12 +552,12 @@ export default function Portfolio() {
<thead>
<tr className="text-slate-600 border-b border-slate-700/40">
<th className="text-left pb-2">Position</th>
<th className="text-left pb-2">Stratégie</th>
<th className="text-right pb-2">Investi</th>
<th className="text-right pb-2">Clôture</th>
<th className="text-right pb-2">Frais IB</th>
<th className="text-right pb-2">P&L net</th>
<th className="text-left pb-2">Date clôture</th>
<th className="text-left pb-2">Strategy</th>
<th className="text-right pb-2">Invested</th>
<th className="text-right pb-2">Close</th>
<th className="text-right pb-2">IB fees</th>
<th className="text-right pb-2">Net P&L</th>
<th className="text-left pb-2">Close date</th>
<th className="pb-2"></th>
</tr>
</thead>
@@ -570,7 +570,7 @@ export default function Portfolio() {
)
})}
{(!closed || closed.length === 0) && (
<tr><td colSpan={8} className="py-8 text-center text-slate-600">Aucun trade clôturé</td></tr>
<tr><td colSpan={8} className="py-8 text-center text-slate-600">No closed trades</td></tr>
)}
</tbody>
</table>
@@ -580,7 +580,7 @@ export default function Portfolio() {
{/* P&L history */}
{tab === 'history' && (
<div className="card">
<div className="section-title">Courbe de P&L cumulé (trades réalisés)</div>
<div className="section-title">Cumulative P&L curve (realized trades)</div>
{pnlHistory && pnlHistory.length > 0 ? (
<>
<ResponsiveContainer width="100%" height={250}>
@@ -596,7 +596,7 @@ export default function Portfolio() {
<YAxis tick={{ fill: '#475569', fontSize: 9 }} tickLine={false} axisLine={false}
tickFormatter={v => `${v.toFixed(0)}`} />
<Tooltip contentStyle={{ background: '#0f1623', border: '1px solid #1e2d4d', fontSize: 11 }}
formatter={(v: number) => [`${v.toFixed(2)}€`, 'P&L cumulé']} />
formatter={(v: number) => [`${v.toFixed(2)}`, 'Cumulative P&L']} />
<ReferenceLine y={0} stroke="#475569" strokeDasharray="4 4" />
<Area type="monotone" dataKey="cumulative" stroke="#10b981" fill="url(#pnl-grad)" strokeWidth={2} dot={false} />
</AreaChart>
@@ -615,7 +615,7 @@ export default function Portfolio() {
</>
) : (
<div className="h-40 flex items-center justify-center text-slate-600 text-sm">
Clôturer des positions pour voir la courbe P&L
Close positions to see the P&L curve
</div>
)}
</div>

View File

@@ -140,13 +140,13 @@ function DiffView({ diffData }: { diffData: any }) {
{pd.positions_opened > 0 && (
<div className="text-center">
<div className="text-emerald-400 font-bold text-lg">+{pd.positions_opened}</div>
<div className="text-[10px] text-slate-500">ouvertes</div>
<div className="text-[10px] text-slate-500">opened</div>
</div>
)}
{pd.positions_closed > 0 && (
<div className="text-center">
<div className="text-red-400 font-bold text-lg">-{pd.positions_closed}</div>
<div className="text-[10px] text-slate-500">fermées</div>
<div className="text-[10px] text-slate-500">closed</div>
</div>
)}
</div>
@@ -159,7 +159,7 @@ function DiffView({ diffData }: { diffData: any }) {
<div className="flex items-center gap-2 mb-3">
<Plus className="w-4 h-4 text-emerald-400" />
<h3 className="text-sm font-semibold text-emerald-400">
Nouvelles positions ({new_positions.length})
New positions ({new_positions.length})
</h3>
</div>
<div className="space-y-1.5">
@@ -170,7 +170,7 @@ function DiffView({ diffData }: { diffData: any }) {
<span className="text-slate-400 ml-2">{p.strategy}</span>
</div>
<div className="flex gap-4">
<span className="text-slate-500">Entrée: {p.entry_price?.toFixed(2) ?? '—'}</span>
<span className="text-slate-500">Entry: {p.entry_price?.toFixed(2) ?? '—'}</span>
<span className={clsx('font-semibold', (p.pnl_pct ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400')}>
{fmt(p.pnl_pct)}
</span>
@@ -187,7 +187,7 @@ function DiffView({ diffData }: { diffData: any }) {
<div className="flex items-center gap-2 mb-3">
<Minus className="w-4 h-4 text-red-400" />
<h3 className="text-sm font-semibold text-red-400">
Positions fermées ({closed_positions.length})
Closed positions ({closed_positions.length})
</h3>
</div>
<div className="space-y-1.5">
@@ -198,7 +198,7 @@ function DiffView({ diffData }: { diffData: any }) {
<span className="text-slate-500 ml-2">{p.strategy}</span>
</div>
<div className="flex gap-4">
<span className="text-slate-500">Dernière val.: {fmt(p.pnl_pct)}</span>
<span className="text-slate-500">Last val.: {fmt(p.pnl_pct)}</span>
<span className={clsx('font-semibold', (p.pnl_pct ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400')}>
{fmtEur(p.pnl_eur)}
</span>
@@ -213,7 +213,7 @@ function DiffView({ diffData }: { diffData: any }) {
{changed_positions.length > 0 && (
<div className="bg-dark-800 border border-slate-700/40 rounded-xl p-4">
<h3 className="text-sm font-semibold text-white mb-3">
Évolution des positions ({changed_positions.length})
Position changes ({changed_positions.length})
</h3>
<div className="overflow-x-auto">
<table className="w-full text-xs">
@@ -265,7 +265,7 @@ function SnapshotDetail({ snapId }: { snapId: number }) {
staleTime: 300_000,
})
if (isLoading) return <div className="text-slate-600 text-sm p-4">Chargement</div>
if (isLoading) return <div className="text-slate-600 text-sm p-4">Loading</div>
if (!data) return null
const trades: any[] = data.trades_snapshot ?? []
@@ -277,7 +277,7 @@ function SnapshotDetail({ snapId }: { snapId: number }) {
<div>
<h3 className="text-sm font-semibold text-white">{fmtDate(data.snapped_at)} UTC</h3>
{macro?.dominant && (
<span className="text-xs text-slate-500 capitalize">Régime : {macro.dominant}</span>
<span className="text-xs text-slate-500 capitalize">Regime: {macro.dominant}</span>
)}
</div>
<div className="flex gap-4 text-xs">
@@ -294,7 +294,7 @@ function SnapshotDetail({ snapId }: { snapId: number }) {
</span>
</div>
<div>
<span className="text-slate-500">{data.n_open} ouvertes · {data.n_closed} fermées</span>
<span className="text-slate-500">{data.n_open} open · {data.n_closed} closed</span>
</div>
</div>
</div>
@@ -304,9 +304,9 @@ function SnapshotDetail({ snapId }: { snapId: number }) {
<thead>
<tr className="text-slate-500 border-b border-slate-700/40">
<th className="text-left pb-2 font-medium">Ticker</th>
<th className="text-left pb-2 font-medium">Stratégie</th>
<th className="text-right pb-2 font-medium">Entrée</th>
<th className="text-right pb-2 font-medium">Prix actuel</th>
<th className="text-left pb-2 font-medium">Strategy</th>
<th className="text-right pb-2 font-medium">Entry</th>
<th className="text-right pb-2 font-medium">Current price</th>
<th className="text-right pb-2 font-medium">PnL %</th>
<th className="text-right pb-2 font-medium">PnL </th>
</tr>
@@ -329,7 +329,7 @@ function SnapshotDetail({ snapId }: { snapId: number }) {
</tbody>
</table>
) : (
<div className="text-slate-600 text-sm">Aucune position dans ce snapshot</div>
<div className="text-slate-600 text-sm">No positions in this snapshot</div>
)}
</div>
)
@@ -397,11 +397,11 @@ export default function PositionHistory() {
<div>
<div className="flex items-center gap-3 mb-1">
<History className="w-6 h-6 text-violet-400" />
<h1 className="text-xl font-bold text-white">Historique des Positions</h1>
<h1 className="text-xl font-bold text-white">Position History</h1>
</div>
<p className="text-slate-400 text-sm">
{snapshots.length} snapshot{snapshots.length !== 1 ? 's' : ''} PnL enregistré{snapshots.length !== 1 ? 's' : ''}
{snapshots.length > 0 && ` · de ${fmtDate(snapshots[snapshots.length - 1]?.snapped_at)} à ${fmtDate(snapshots[0]?.snapped_at)}`}
{snapshots.length} PnL snapshot{snapshots.length !== 1 ? 's' : ''} recorded
{snapshots.length > 0 && ` · from ${fmtDate(snapshots[snapshots.length - 1]?.snapped_at)} to ${fmtDate(snapshots[0]?.snapped_at)}`}
</p>
</div>
<div className="flex gap-2 flex-wrap">
@@ -412,7 +412,7 @@ export default function PositionHistory() {
className={clsx('px-3 py-1.5 rounded text-xs font-semibold transition-colors',
viewMode === 'detail' ? 'bg-slate-600 text-white' : 'bg-dark-700 text-slate-400 hover:text-white')}
>
Détail
Detail
</button>
<button
onClick={() => setViewMode('diff')}
@@ -428,12 +428,12 @@ export default function PositionHistory() {
onClick={() => { setSelectedA(null); setSelectedB(null); setViewMode('detail') }}
className="px-3 py-1.5 rounded text-xs text-slate-500 hover:text-slate-300 bg-dark-700"
>
Effacer sélection
Clear selection
</button>
)}
<button onClick={() => refetch()}
className="flex items-center gap-1 px-3 py-1.5 rounded text-xs bg-dark-700 text-slate-400 hover:text-white">
<RefreshCw className="w-3.5 h-3.5" /> Actualiser
<RefreshCw className="w-3.5 h-3.5" /> Refresh
</button>
</div>
</div>
@@ -448,24 +448,24 @@ export default function PositionHistory() {
{/* Selection banner */}
{(selectedA || selectedB) && (
<div className="flex items-center gap-3 bg-dark-800 border border-slate-700/40 rounded-xl px-4 py-3 text-xs flex-wrap">
<span className="text-slate-500">Sélection diff :</span>
<span className="text-slate-500">Diff selection:</span>
{selectedA ? (
<span className="flex items-center gap-1.5 bg-blue-900/30 text-blue-400 border border-blue-700/40 px-2.5 py-1 rounded">
<span className="font-bold">A</span>
{fmtDate(snapshots.find(s => s.id === selectedA)?.snapped_at ?? '')}
</span>
) : <span className="text-slate-600">A : non sélectionné</span>}
) : <span className="text-slate-600">A: not selected</span>}
<ArrowRight className="w-3.5 h-3.5 text-slate-600" />
{selectedB ? (
<span className="flex items-center gap-1.5 bg-violet-900/30 text-violet-400 border border-violet-700/40 px-2.5 py-1 rounded">
<span className="font-bold">B</span>
{fmtDate(snapshots.find(s => s.id === selectedB)?.snapped_at ?? '')}
</span>
) : <span className="text-slate-600">B : non sélectionné</span>}
) : <span className="text-slate-600">B: not selected</span>}
{canDiff && (
<button onClick={() => setViewMode('diff')}
className="ml-auto bg-violet-600 hover:bg-violet-500 text-white px-3 py-1 rounded text-xs font-semibold">
Voir le diff
View diff
</button>
)}
</div>
@@ -475,10 +475,10 @@ export default function PositionHistory() {
{!isLoading && snapshots.length === 0 && (
<div className="text-center py-20">
<History className="w-14 h-14 text-slate-700 mx-auto mb-4" />
<div className="text-slate-400 font-semibold mb-2">Aucun snapshot PnL disponible</div>
<div className="text-slate-400 font-semibold mb-2">No PnL snapshots available</div>
<p className="text-slate-600 text-sm max-w-md mx-auto">
Activez le Scheduler PnL dans la Configuration ou déclenchez un snapshot manuellement
depuis la page Configuration Auto-Cycle &amp; Logging.
Enable the PnL Scheduler in Configuration or trigger a snapshot manually
from the Configuration Auto-Cycle &amp; Logging page.
</p>
</div>
)}
@@ -491,7 +491,7 @@ export default function PositionHistory() {
{/* Mini sparkline */}
{sparkData.length > 2 && (
<div className="bg-dark-800 border border-slate-700/40 rounded-xl p-3">
<div className="text-xs text-slate-500 mb-2">PnL portfolio dans le temps</div>
<div className="text-xs text-slate-500 mb-2">Portfolio PnL over time</div>
<ResponsiveContainer width="100%" height={80}>
<LineChart data={sparkData} margin={{ top: 4, right: 4, left: -30, bottom: 0 }}>
<XAxis dataKey="date" hide />
@@ -507,7 +507,7 @@ export default function PositionHistory() {
{/* Snapshot list */}
<div className="bg-dark-800 border border-slate-700/40 rounded-xl p-3 space-y-1.5 max-h-[calc(100vh-320px)] overflow-y-auto">
<div className="text-[10px] text-slate-600 uppercase tracking-wider px-1 mb-2">
{snapshots.length} snapshots cliquez pour détail · A/B pour diff
{snapshots.length} snapshots click for detail · A/B to diff
</div>
{snapshots.map(s => (
<div key={s.id} onClick={() => { setActiveDetail(s.id); setViewMode('detail') }}>
@@ -527,7 +527,7 @@ export default function PositionHistory() {
<div className="lg:col-span-2">
{viewMode === 'diff' && canDiff && (
diffLoading
? <div className="text-slate-500 text-sm p-4">Calcul du diff</div>
? <div className="text-slate-500 text-sm p-4">Computing diff</div>
: diffError
? <div className="text-red-400 text-sm p-4">{String(diffError)}</div>
: diffData ? <DiffView diffData={diffData} />
@@ -542,8 +542,8 @@ export default function PositionHistory() {
<div className="flex flex-col items-center justify-center py-20 text-center">
<TrendingUp className="w-12 h-12 text-slate-700 mb-4" />
<div className="text-slate-500 text-sm">
Cliquez sur un snapshot pour voir le détail<br />
ou sélectionnez A et B pour comparer deux valuations
Click a snapshot to see the detail<br />
or select A and B to compare two valuations
</div>
</div>
)}
@@ -552,7 +552,7 @@ export default function PositionHistory() {
<div className="flex flex-col items-center justify-center py-20 text-center">
<TrendingDown className="w-12 h-12 text-slate-700 mb-4" />
<div className="text-slate-500 text-sm">
Sélectionnez deux snapshots (A et B) depuis la liste pour voir le diff
Select two snapshots (A and B) from the list to view the diff
</div>
</div>
)}

View File

@@ -9,14 +9,14 @@ import {
const SCENARIO_META: Record<string, { label: string; color: string; emoji: string }> = {
goldilocks: { label: 'Goldilocks', color: '#22c55e', emoji: '🌟' },
desinflation: { label: 'Désinflation', color: '#06b6d4', emoji: '❄️' },
desinflation: { label: 'Disinflation', color: '#06b6d4', emoji: '❄️' },
soft_landing: { label: 'Soft Landing', color: '#3b82f6', emoji: '🛬' },
reflation: { label: 'Reflation', color: '#f97316', emoji: '🔥' },
stagflation: { label: 'Stagflation', color: '#eab308', emoji: '⚡' },
inflation_shock: { label: 'Inflation Shock', color: '#ef4444', emoji: '💥' },
recession: { label: 'Récession', color: '#8b5cf6', emoji: '📉' },
crise_liquidite: { label: 'Crise Liquidité', color: '#ec4899', emoji: '🚨' },
incertain: { label: 'Incertain', color: '#64748b', emoji: '❓' },
recession: { label: 'Recession', color: '#8b5cf6', emoji: '📉' },
crise_liquidite: { label: 'Liquidity Crisis', color: '#ec4899', emoji: '🚨' },
incertain: { label: 'Uncertain', color: '#64748b', emoji: '❓' },
}
function RegimeBadge({ dominant }: { dominant?: string }) {
@@ -107,7 +107,7 @@ function DeltaRow({ emoji, label, count, items, colorClass }: {
<div className="text-slate-600 text-[10px] mt-0.5 line-clamp-2">{item.description}</div>
)}
{item.triggers?.length > 0 && (
<div className="text-slate-700 text-[10px]">Déclencheurs: {item.triggers.join(', ')}</div>
<div className="text-slate-700 text-[10px]">Triggers: {item.triggers.join(', ')}</div>
)}
{item.pnl_pct != null && (
<span className={clsx('font-mono text-[10px] ml-1', item.pnl_pct >= 0 ? 'text-emerald-400' : 'text-red-400')}>
@@ -157,18 +157,18 @@ export default function RapportCycle() {
{/* Left panel — history list */}
<div className="w-56 shrink-0">
<div className="section-title flex items-center gap-1 mb-3">
<BookOpen className="w-3.5 h-3.5" /> Historique
<BookOpen className="w-3.5 h-3.5" /> History
</div>
{listLoading ? (
<div className="text-xs text-slate-600">Chargement</div>
<div className="text-xs text-slate-600">Loading</div>
) : reports.length === 0 ? (
<div className="text-xs text-slate-600">Aucun rapport. Le premier sera généré au prochain cycle.</div>
<div className="text-xs text-slate-600">No reports. The first will be generated at the next cycle.</div>
) : (
<div className="space-y-1">
{reports.map((r: any) => {
const dt = r.generated_at ? new Date(r.generated_at.endsWith('Z') ? r.generated_at : r.generated_at + 'Z') : null
const dateStr = dt
? dt.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
? dt.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
: '—'
const isActive = selectedRunId === r.run_id || (!selectedRunId && r === reports[0])
const m = SCENARIO_META[r.macro_dominant] ?? SCENARIO_META.incertain
@@ -204,13 +204,13 @@ export default function RapportCycle() {
<div className="flex-1 min-w-0">
{isLoading ? (
<div className="flex items-center gap-2 text-slate-500 text-sm mt-12 justify-center">
<RefreshCw className="w-4 h-4 animate-spin" /> Chargement
<RefreshCw className="w-4 h-4 animate-spin" /> Loading
</div>
) : !report ? (
<div className="card text-center py-12">
<Brain className="w-10 h-10 text-slate-700 mx-auto mb-3" />
<div className="text-slate-500 text-sm mb-1">Aucun rapport de cycle disponible</div>
<div className="text-xs text-slate-600">Les rapports sont générés automatiquement à la fin de chaque cycle.</div>
<div className="text-slate-500 text-sm mb-1">No cycle report available</div>
<div className="text-xs text-slate-600">Reports are generated automatically at the end of each cycle.</div>
</div>
) : (
<>
@@ -221,18 +221,18 @@ export default function RapportCycle() {
<div className="flex items-center gap-2 mb-1">
<RegimeBadge dominant={report.macro_dominant} />
<span className="text-xs text-slate-500 font-mono">
Géo {report.geo_score ?? '—'}/100
Geo {report.geo_score ?? '—'}/100
</span>
</div>
<div className="text-[10px] text-slate-600 font-mono">
Cycle du {report.generated_at
Cycle of {report.generated_at
? new Date(report.generated_at.endsWith('Z') ? report.generated_at : report.generated_at + 'Z')
.toLocaleString('fr-FR')
.toLocaleString('en-GB')
: '—'}
{report.prev_cycle_at && (
<span className="ml-2 text-slate-700">
· vs cycle du {new Date(report.prev_cycle_at.endsWith('Z') ? report.prev_cycle_at : report.prev_cycle_at + 'Z')
.toLocaleDateString('fr-FR')}
· vs cycle of {new Date(report.prev_cycle_at.endsWith('Z') ? report.prev_cycle_at : report.prev_cycle_at + 'Z')
.toLocaleDateString('en-GB')}
</span>
)}
</div>
@@ -260,7 +260,7 @@ export default function RapportCycle() {
<div className="grid grid-cols-2 gap-3 mt-2">
{report.pnl_summary && Object.keys(report.pnl_summary).length > 0 && (
<div className="bg-dark-700/50 rounded px-3 py-2">
<div className="text-[9px] text-slate-600 mb-1 uppercase tracking-wide">PnL au moment du cycle</div>
<div className="text-[9px] text-slate-600 mb-1 uppercase tracking-wide">PnL at cycle time</div>
<div className="flex items-baseline gap-2">
<span className={clsx('text-lg font-bold font-mono',
(report.pnl_summary.total_pnl_pct ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400')}>
@@ -276,13 +276,13 @@ export default function RapportCycle() {
)}
</div>
<div className="text-[9px] text-slate-600 mt-0.5">
{report.pnl_summary.n_open ?? 0} ouvertes · {report.pnl_summary.n_closed ?? 0} fermées
{report.pnl_summary.n_open ?? 0} open · {report.pnl_summary.n_closed ?? 0} closed
</div>
</div>
)}
{report.var_summary && Object.keys(report.var_summary).length > 0 && (
<div className="bg-dark-700/50 rounded px-3 py-2">
<div className="text-[9px] text-slate-600 mb-1 uppercase tracking-wide">VaR 95% au moment du cycle</div>
<div className="text-[9px] text-slate-600 mb-1 uppercase tracking-wide">VaR 95% at cycle time</div>
<div className="grid grid-cols-3 gap-1">
{[
{ label: 'Hist.', val: report.var_summary.hist_var_1d_pct, color: 'text-blue-400' },
@@ -303,15 +303,15 @@ export default function RapportCycle() {
</div>
{/* Delta section */}
<Section icon={<GitCompare className="w-4 h-4" />} title="Changements du cycle">
<Section icon={<GitCompare className="w-4 h-4" />} title="Cycle changes">
<DeltaRow
emoji="⭐" label="patterns ajoutés"
emoji="⭐" label="patterns added"
count={report.patterns_added ?? 0}
items={report.patterns_added_list ?? []}
colorClass="text-blue-400"
/>
<DeltaRow
emoji="📥" label="trades loggés"
emoji="📥" label="trades logged"
count={report.trades_logged ?? 0}
items={(report.trades_logged_list ?? []).map((t: any) => ({
underlying: t.underlying, strategy: t.strategy,
@@ -320,7 +320,7 @@ export default function RapportCycle() {
colorClass="text-emerald-400"
/>
<DeltaRow
emoji="✅" label="trades fermés depuis le cycle précédent"
emoji="✅" label="trades closed since last cycle"
count={report.trades_closed ?? 0}
items={(report.trades_closed_list ?? []).map((t: any) => ({
underlying: t.underlying, strategy: t.strategy,
@@ -331,7 +331,7 @@ export default function RapportCycle() {
{/* Top scored */}
{(report.top_scored ?? []).length > 0 && (
<div className="mt-3 pt-3 border-t border-slate-700/30">
<div className="text-xs text-slate-500 mb-2">Top patterns scorés ce cycle</div>
<div className="text-xs text-slate-500 mb-2">Top scored patterns this cycle</div>
<div className="space-y-1.5">
{(report.top_scored as any[]).map((p: any, i: number) => (
<div key={i} className="flex items-start gap-2 text-xs">
@@ -355,7 +355,7 @@ export default function RapportCycle() {
{/* Context narrative */}
{report.context_narrative?.narrative && (
<Section icon={<Brain className="w-4 h-4" />} title="Raisonnement IA — ce cycle">
<Section icon={<Brain className="w-4 h-4" />} title="AI reasoning — this cycle">
<p className="text-sm text-slate-300 leading-relaxed mb-4">
{report.context_narrative.narrative}
</p>
@@ -363,7 +363,7 @@ export default function RapportCycle() {
{(report.context_narrative.context_driven ?? []).length > 0 && (
<div>
<div className="text-[10px] text-emerald-400 font-semibold uppercase tracking-wide mb-2">
📡 Du contexte transmis
📡 From transmitted context
</div>
<ul className="space-y-1">
{(report.context_narrative.context_driven as string[]).map((s, i) => (
@@ -377,7 +377,7 @@ export default function RapportCycle() {
{(report.context_narrative.general_knowledge ?? []).length > 0 && (
<div>
<div className="text-[10px] text-blue-400 font-semibold uppercase tracking-wide mb-2">
🧠 Connaissance générale
🧠 General knowledge
</div>
<ul className="space-y-1">
{(report.context_narrative.general_knowledge as string[]).map((s, i) => (
@@ -391,7 +391,7 @@ export default function RapportCycle() {
</div>
{(report.context_narrative.key_signals ?? []).length > 0 && (
<div className="mt-3 pt-3 border-t border-slate-700/30">
<div className="text-[10px] text-slate-500 mb-2">Signaux déterminants</div>
<div className="text-[10px] text-slate-500 mb-2">Determining signals</div>
<div className="flex flex-wrap gap-1.5">
{(report.context_narrative.key_signals as string[]).map((s, i) => (
<span key={i} className="text-[10px] bg-slate-800 text-slate-300 border border-slate-700/50 px-2 py-0.5 rounded">
@@ -404,7 +404,7 @@ export default function RapportCycle() {
{/* Context log */}
{report.context_narrative.context_log && (
<div className="mt-3 pt-3 border-t border-slate-700/30">
<div className="text-[10px] text-slate-500 mb-2">Log contexte état transmis à l'IA</div>
<div className="text-[10px] text-slate-500 mb-2">Context log state transmitted to AI</div>
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-[10px]">
{Object.entries(report.context_narrative.context_log.key_gauges ?? {}).map(([k, v]: [string, any]) => (
<div key={k} className="flex items-center gap-1.5">
@@ -415,7 +415,7 @@ export default function RapportCycle() {
</div>
{(report.context_narrative.context_log.dominant_news ?? []).length > 0 && (
<div className="mt-2">
<div className="text-[9px] text-slate-700 mb-1">News transmises (top impact)</div>
<div className="text-[9px] text-slate-700 mb-1">Transmitted news (top impact)</div>
{(report.context_narrative.context_log.dominant_news as string[]).map((n, i) => (
<div key={i} className="text-[9px] text-slate-600 truncate">· {n}</div>
))}
@@ -428,7 +428,7 @@ export default function RapportCycle() {
{/* Cycle commentary */}
{report.commentary?.commentary && (
<Section icon={<Layers className="w-4 h-4" />} title="Analyse macro du cycle">
<Section icon={<Layers className="w-4 h-4" />} title="Cycle macro analysis">
<p className="text-sm text-slate-300 leading-relaxed mb-3">{report.commentary.commentary}</p>
{report.commentary.key_risk && (
<div className="flex items-start gap-2 text-xs text-orange-400 bg-orange-900/10 border border-orange-800/20 rounded px-3 py-2">
@@ -438,7 +438,7 @@ export default function RapportCycle() {
)}
{report.commentary.top_pattern && (
<div className="mt-2 text-xs text-slate-500">
Pattern le plus pertinent : <span className="text-blue-400 font-medium">{report.commentary.top_pattern}</span>
Most relevant pattern: <span className="text-blue-400 font-medium">{report.commentary.top_pattern}</span>
</div>
)}
</Section>
@@ -446,7 +446,7 @@ export default function RapportCycle() {
{/* Options Technical Agent */}
{report.options_technical && (report.options_technical.assessments?.length > 0 || report.options_technical.global_assessment) && (
<Section icon={<BarChart2 className="w-4 h-4" />} title="Validation Technique Options">
<Section icon={<BarChart2 className="w-4 h-4" />} title="Options Technical Validation">
{/* Global score + summary */}
<div className="flex items-start gap-3 mb-4">
{report.options_technical.global_score != null && (
@@ -548,7 +548,7 @@ export default function RapportCycle() {
<div className="flex items-start gap-3 mt-1.5 pt-1.5 border-t border-slate-700/20 text-[10px]">
{a.optimal_strategy && a.optimal_strategy !== a.strategy && (
<span className="text-slate-500">
Stratégie optimale: <span className="text-blue-400 font-medium">{a.optimal_strategy}</span>
Optimal strategy: <span className="text-blue-400 font-medium">{a.optimal_strategy}</span>
</span>
)}
{a.when_to_enter && (
@@ -563,7 +563,7 @@ export default function RapportCycle() {
{/* Risk / portfolio monitor */}
{report.portfolio_monitor && (
<Section icon={<ShieldAlert className="w-4 h-4" />} title="Risque & Alertes portefeuille">
<Section icon={<ShieldAlert className="w-4 h-4" />} title="Risk & Portfolio alerts">
<p className="text-sm text-slate-400 mb-3">{report.portfolio_monitor.assessment}</p>
{(report.portfolio_monitor.actions ?? []).length > 0 && (
<div className="space-y-2">

View File

@@ -17,7 +17,7 @@ function ConcentrationGauge({ label, pct, threshold = 50 }: { label: string; pct
<div className={clsx('h-full rounded-full transition-all', color)} style={{ width: `${Math.min(pct, 100)}%` }} />
</div>
{pct > threshold && (
<div className="text-[10px] text-red-400 mt-0.5"> Saturé (&gt;{threshold}%)</div>
<div className="text-[10px] text-red-400 mt-0.5"> Saturated (&gt;{threshold}%)</div>
)}
</div>
)
@@ -28,7 +28,7 @@ function EquityCurve({ timeline }: { timeline: any[] }) {
if (!timeline || timeline.length < 2) {
return (
<div className="h-24 flex items-center justify-center text-slate-600 text-xs">
Pas encore de données de P&L (nécessite des trades matures)
No P&L data yet (requires mature trades)
</div>
)
}
@@ -119,7 +119,7 @@ function RecommendationCard({ rec }: { rec: any }) {
'text-emerald-400': isOk,
})}>
{isOk ? <CheckCircle className="w-4 h-4" /> : <AlertTriangle className="w-4 h-4" />}
Recommandation Risk Committee
Risk Committee Recommendation
</div>
<div className="space-y-1.5">
{(rec.messages ?? []).map((msg: string, i: number) => (
@@ -150,8 +150,8 @@ function SimRiskPanel() {
if (!risk || risk.open_count === 0) return (
<div className="card text-center py-10 text-slate-500">
<PieChart className="w-8 h-8 mx-auto mb-2 opacity-20" />
<div className="text-sm">Aucune position ouverte dans le portefeuille simulé</div>
<div className="text-xs mt-1">Les trades logués apparaîtront ici après le prochain cycle IA</div>
<div className="text-sm">No open positions in the simulated portfolio</div>
<div className="text-xs mt-1">Logged trades will appear here after the next AI cycle</div>
</div>
)
@@ -169,25 +169,25 @@ function SimRiskPanel() {
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div className="card text-center">
<div className="text-2xl font-bold text-white font-mono">{risk.open_count}</div>
<div className="text-xs text-slate-500 mt-1">Positions simulées</div>
<div className="text-xs text-slate-500 mt-1">Simulated positions</div>
</div>
<div className="card text-center">
<div className={clsx('text-2xl font-bold font-mono', dangers.length > 0 ? 'text-red-400' : 'text-emerald-400')}>
{dangers.length}
</div>
<div className="text-xs text-slate-500 mt-1">Conflits directionnels</div>
<div className="text-xs text-slate-500 mt-1">Directional conflicts</div>
</div>
<div className="card text-center">
<div className={clsx('text-2xl font-bold font-mono', warnings.length > 0 ? 'text-amber-400' : 'text-emerald-400')}>
{warnings.length}
</div>
<div className="text-xs text-slate-500 mt-1">Alertes concentration</div>
<div className="text-xs text-slate-500 mt-1">Concentration alerts</div>
</div>
<div className="card text-center">
<div className="text-2xl font-bold font-mono text-slate-300">
{Object.keys(concentration).filter(ac => ac !== 'unknown').length}
</div>
<div className="text-xs text-slate-500 mt-1">Classes d'actifs</div>
<div className="text-xs text-slate-500 mt-1">Asset classes</div>
</div>
</div>
@@ -197,7 +197,7 @@ function SimRiskPanel() {
<div className="card">
<div className="text-sm font-semibold text-white mb-3 flex items-center justify-between">
<span className="flex items-center gap-2">
<GitBranch className="w-4 h-4 text-blue-400" /> Répartition par classe d'actif
<GitBranch className="w-4 h-4 text-blue-400" /> Allocation by asset class
</span>
<button onClick={() => refetch()} className="text-slate-600 hover:text-slate-400">
<RefreshCw className="w-3.5 h-3.5" />
@@ -243,14 +243,14 @@ function SimRiskPanel() {
{conflicts.length === 0 && warnings.length === 0 ? (
<div className="card border-emerald-700/30 bg-emerald-900/10 flex items-center gap-3 py-4">
<CheckCircle className="w-5 h-5 text-emerald-400 shrink-0" />
<div className="text-xs text-emerald-300">Portefeuille équilibré aucun conflit ni sur-concentration détectés</div>
<div className="text-xs text-emerald-300">Balanced portfolio no conflicts or over-concentration detected</div>
</div>
) : null}
{conflicts.map((c: any, i: number) => (
<div key={i} className="card border-red-700/40 bg-red-900/10">
<div className="text-xs font-semibold text-red-300 mb-2 flex items-center gap-1">
<ShieldAlert className="w-3.5 h-3.5" /> Conflit directionnel {c.underlying}
<ShieldAlert className="w-3.5 h-3.5" /> Directional conflict {c.underlying}
</div>
<div className="space-y-1">
{c.trades.map((t: any) => (
@@ -271,7 +271,7 @@ function SimRiskPanel() {
{warnings.length > 0 && (
<div className="card border-amber-700/30 bg-amber-900/10">
<div className="text-xs font-semibold text-amber-300 mb-2 flex items-center gap-1">
<AlertTriangle className="w-3.5 h-3.5" /> Alertes de concentration
<AlertTriangle className="w-3.5 h-3.5" /> Concentration alerts
</div>
<div className="space-y-1">
{warnings.map((a: any, i: number) => (
@@ -290,7 +290,7 @@ function SimRiskPanel() {
<div className="card border-blue-700/30 bg-blue-900/10">
<div className="flex items-center justify-between mb-2">
<div className="text-xs font-semibold text-blue-300 flex items-center gap-1.5">
<Brain className="w-3.5 h-3.5" /> Recommandations IA Moniteur de portefeuille simulé
<Brain className="w-3.5 h-3.5" /> AI Recommendations Simulated portfolio monitor
</div>
{aiTs && <span className="text-[10px] text-slate-600">{aiTs.slice(0, 16).replace('T', ' ')}</span>}
</div>
@@ -349,8 +349,8 @@ export default function RiskDashboard() {
</h1>
<p className="text-xs text-slate-500 mt-0.5">
{mode === 'real'
? 'Concentration · Clusters · Corrélations · Position Sizing — Portefeuille IBKR'
: 'Conflits directionnels · Concentration · Recommandations IA — Trades loggés'}
? 'Concentration · Clusters · Correlations · Position Sizing — IBKR Portfolio'
: 'Directional conflicts · Concentration · AI Recommendations — Logged trades'}
</p>
</div>
{/* Mode toggle */}
@@ -360,14 +360,14 @@ export default function RiskDashboard() {
'bg-blue-600 text-white': mode === 'real',
'text-slate-400 hover:text-slate-200': mode !== 'real',
})}>
Portefeuille Réel
Real Portfolio
</button>
<button onClick={() => setMode('sim')}
className={clsx('flex items-center gap-1.5 px-3 py-1.5 rounded text-sm font-semibold transition-colors', {
'bg-blue-600 text-white': mode === 'sim',
'text-slate-400 hover:text-slate-200': mode !== 'sim',
})}>
Simulé (loggés)
Simulated (logged)
{simConflicts > 0 && (
<span className="text-[10px] font-bold bg-red-500 text-white rounded-full w-4 h-4 flex items-center justify-center">
{simConflicts}
@@ -387,14 +387,14 @@ export default function RiskDashboard() {
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<div className="card text-center">
<div className="text-2xl font-bold text-white font-mono">{d.open_trades ?? 0}</div>
<div className="text-xs text-slate-500 mt-1">Positions ouvertes</div>
<div className="text-xs text-slate-500 mt-1">Open positions</div>
</div>
<div className="card text-center">
<div className={clsx('text-2xl font-bold font-mono',
d.diversification_score >= 60 ? 'text-emerald-400'
: d.diversification_score >= 35 ? 'text-amber-400' : 'text-red-400'
)}>{d.diversification_score ?? '—'}%</div>
<div className="text-xs text-slate-500 mt-1">Score diversification</div>
<div className="text-xs text-slate-500 mt-1">Diversification score</div>
<div className="text-[10px] text-slate-600">N effectif = {d.effective_n_positions ?? '—'}</div>
</div>
<div className="card text-center">
@@ -402,7 +402,7 @@ export default function RiskDashboard() {
(d.expected_drawdown_pct ?? 0) < 15 ? 'text-emerald-400'
: (d.expected_drawdown_pct ?? 0) < 25 ? 'text-amber-400' : 'text-red-400'
)}>{d.expected_drawdown_pct ?? '—'}%</div>
<div className="text-xs text-slate-500 mt-1">Drawdown attendu</div>
<div className="text-xs text-slate-500 mt-1">Expected drawdown</div>
</div>
<div className="card text-center">
<div className={clsx('text-2xl font-bold font-mono',
@@ -410,7 +410,7 @@ export default function RiskDashboard() {
)}>
{d.saturated_factors?.length ?? 0}
</div>
<div className="text-xs text-slate-500 mt-1">Facteurs saturés</div>
<div className="text-xs text-slate-500 mt-1">Saturated factors</div>
{d.saturated_factors?.length > 0 && (
<div className="text-[10px] text-red-400">{d.saturated_factors.join(', ')}</div>
)}
@@ -439,10 +439,10 @@ export default function RiskDashboard() {
{/* Exposure by class */}
<div className="card">
<div className="text-sm font-semibold text-white mb-3 flex items-center gap-2">
<GitBranch className="w-4 h-4 text-blue-400" /> Exposition par classe d'actif
<GitBranch className="w-4 h-4 text-blue-400" /> Exposure by asset class
</div>
{Object.keys(d.exposure_by_class ?? {}).length === 0 ? (
<div className="text-xs text-slate-600 text-center py-4">Aucune position ouverte</div>
<div className="text-xs text-slate-600 text-center py-4">No open positions</div>
) : (
<div className="space-y-3">
{Object.entries(d.exposure_by_class ?? {})
@@ -457,10 +457,10 @@ export default function RiskDashboard() {
{/* Risk factors */}
<div className="card">
<div className="text-sm font-semibold text-white mb-3 flex items-center gap-2">
<Activity className="w-4 h-4 text-orange-400" /> Exposition par facteur de risque
<Activity className="w-4 h-4 text-orange-400" /> Exposure by risk factor
</div>
{(d.risk_clusters ?? []).length === 0 ? (
<div className="text-xs text-slate-600 text-center py-4">Aucune position ouverte</div>
<div className="text-xs text-slate-600 text-center py-4">No open positions</div>
) : (
<div className="space-y-3">
{(d.risk_clusters ?? []).map((c: any) => (
@@ -475,14 +475,14 @@ export default function RiskDashboard() {
<div className="card">
<div className="flex items-center justify-between mb-3">
<div className="text-sm font-semibold text-white flex items-center gap-2">
<TrendingUp className="w-4 h-4 text-blue-400" /> Courbe P&L cumulé (trades matures)
<TrendingUp className="w-4 h-4 text-blue-400" /> Cumulative P&L curve (mature trades)
</div>
<select value={tlDays} onChange={e => setTlDays(Number(e.target.value))}
className="bg-dark-700 border border-slate-700 rounded px-2 py-0.5 text-xs text-slate-300">
<option value={30}>30j</option>
<option value={90}>90j</option>
<option value={180}>180j</option>
<option value={365}>1 an</option>
<option value={30}>30d</option>
<option value={90}>90d</option>
<option value={180}>180d</option>
<option value={365}>1 yr</option>
</select>
</div>
<EquityCurve timeline={tlData?.timeline ?? []} />
@@ -491,11 +491,11 @@ export default function RiskDashboard() {
{/* Correlation pairs */}
<div className="card">
<div className="text-sm font-semibold text-white mb-3">
Corrélations entre patterns (P&L historique)
Pattern correlations (historical P&L)
</div>
{pairs.length === 0 ? (
<div className="text-xs text-slate-600 text-center py-4">
Nécessite 3 trades matures par pattern pour calculer les corrélations
Requires 3 mature trades per pattern to calculate correlations
</div>
) : (
<div className="space-y-1.5">
@@ -503,13 +503,13 @@ export default function RiskDashboard() {
<CorrelationPairRow key={i} pair={pair} />
))}
{pairs.length > 10 && (
<div className="text-xs text-slate-600 text-center pt-1">{pairs.length - 10} autres paires</div>
<div className="text-xs text-slate-600 text-center pt-1">{pairs.length - 10} more pairs</div>
)}
</div>
)}
{pairs.length > 0 && (
<div className="text-[10px] text-slate-600 mt-2">
Corrélation &gt; 0.7 = risque concentré ces patterns partagent le me facteur sous-jacent
Correlation &gt; 0.7 = concentrated risk these patterns share the same underlying factor
</div>
)}
</div>

View File

@@ -14,9 +14,9 @@ import {
// ─── Status badge ─────────────────────────────────────────────────────────────
function StatusBadge({ status }: { status: string }) {
const map: Record<string, { icon: React.ReactNode; color: string; label: string }> = {
active: { icon: <CheckCircle className="w-3 h-3" />, color: 'text-emerald-400 bg-emerald-400/10 border-emerald-400/20', label: 'Validé' },
active: { icon: <CheckCircle className="w-3 h-3" />, color: 'text-emerald-400 bg-emerald-400/10 border-emerald-400/20', label: 'Validated' },
tentative: { icon: <HelpCircle className="w-3 h-3" />, color: 'text-amber-400 bg-amber-400/10 border-amber-400/20', label: 'Tentative' },
invalidated: { icon: <XCircle className="w-3 h-3" />, color: 'text-rose-400 bg-rose-400/10 border-rose-400/20', label: 'Invalidé' },
invalidated: { icon: <XCircle className="w-3 h-3" />, color: 'text-rose-400 bg-rose-400/10 border-rose-400/20', label: 'Invalidated' },
}
const s = map[status] || map['tentative']
return (
@@ -61,10 +61,10 @@ function KbEntry({ entry, onStatusChange, onDelete }: {
</button>
<button
onClick={() => {
if (confirm(`Supprimer "${entry.title}" ?`)) onDelete(entry.id)
if (confirm(`Delete "${entry.title}"?`)) onDelete(entry.id)
}}
className="opacity-0 group-hover:opacity-100 p-1 rounded text-slate-600 hover:text-red-400 hover:bg-red-400/10 transition-all mt-0.5"
title="Supprimer cette entrée"
title="Delete this entry"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
@@ -90,12 +90,12 @@ function KbEntry({ entry, onStatusChange, onDelete }: {
: 'border-slate-700 text-slate-500 hover:border-slate-500 hover:text-slate-300'
}`}
>
{s === 'active' ? 'Valider' : s === 'tentative' ? 'Tentative' : 'Invalider'}
{s === 'active' ? 'Validate' : s === 'tentative' ? 'Tentative' : 'Invalidate'}
</button>
))}
</div>
<p className="text-[10px] text-slate-600">
Vu le {entry.first_seen_at?.slice(0, 10)} · Confirmé {entry.confirmation_count}×
First seen {entry.first_seen_at?.slice(0, 10)} · Confirmed {entry.confirmation_count}×
</p>
</div>
)}
@@ -105,11 +105,11 @@ function KbEntry({ entry, onStatusChange, onDelete }: {
// ─── Category section ─────────────────────────────────────────────────────────
const CAT_META: Record<string, { icon: React.ReactNode; color: string; label: string }> = {
régimes: { icon: <Activity className="w-4 h-4" />, color: 'text-blue-400', label: 'Régimes macro' },
régimes: { icon: <Activity className="w-4 h-4" />, color: 'text-blue-400', label: 'Macro regimes' },
patterns: { icon: <BarChart2 className="w-4 h-4" />, color: 'text-purple-400', label: 'Patterns' },
erreurs: { icon: <AlertTriangle className="w-4 h-4" />, color: 'text-rose-400', label: 'Erreurs récurrentes' },
corrélations: { icon: <TrendingUp className="w-4 h-4" />, color: 'text-emerald-400', label: 'Corrélations' },
général: { icon: <BookOpen className="w-4 h-4" />, color: 'text-slate-400', label: 'Général' },
erreurs: { icon: <AlertTriangle className="w-4 h-4" />, color: 'text-rose-400', label: 'Recurring errors' },
corrélations: { icon: <TrendingUp className="w-4 h-4" />, color: 'text-emerald-400', label: 'Correlations' },
général: { icon: <BookOpen className="w-4 h-4" />, color: 'text-slate-400', label: 'General' },
}
function CategorySection({
@@ -132,7 +132,7 @@ function CategorySection({
<div className="flex items-center gap-2">
<span className={meta.color}>{meta.icon}</span>
<span className="text-sm font-medium text-slate-200">{meta.label}</span>
<span className="text-xs text-slate-500">{active}/{entries.length} actifs</span>
<span className="text-xs text-slate-500">{active}/{entries.length} active</span>
</div>
{open ? <ChevronUp className="w-4 h-4 text-slate-500" /> : <ChevronDown className="w-4 h-4 text-slate-500" />}
</button>
@@ -236,14 +236,14 @@ export default function SuperContexte() {
<Brain className="w-6 h-6 text-violet-400" />
<div>
<h1 className="text-xl font-bold text-slate-100">Super Contexte</h1>
<p className="text-xs text-slate-500">Base de raisonnement évolutive cerveau du système</p>
<p className="text-xs text-slate-500">Evolving reasoning base system brain</p>
</div>
</div>
<div className="flex items-center gap-3">
{currentState && (
<div className="text-right text-xs text-slate-500">
<div>v{currentState.version} · {currentState.created_at?.slice(0, 16)}</div>
<div>{currentState.reports_used} rapports · {currentState.trades_analyzed} trades analysés</div>
<div>{currentState.reports_used} reports · {currentState.trades_analyzed} trades analyzed</div>
</div>
)}
<button
@@ -262,7 +262,7 @@ export default function SuperContexte() {
) : (
<Brain className="w-4 h-4" />
)}
{synthesize.isPending ? 'Synthèse en cours…' : 'Lancer synthèse GPT-4o'}
{synthesize.isPending ? 'Synthesis in progress…' : 'Run GPT-4o synthesis'}
</button>
</div>
</div>
@@ -277,7 +277,7 @@ export default function SuperContexte() {
onClick={() => setSelectedHistoryId(null)}
className={`w-full text-left px-3 py-2 rounded-lg text-xs transition-colors ${!selectedHistoryId ? 'bg-violet-600/20 border border-violet-500/30 text-violet-300' : 'border border-slate-700/50 text-slate-400 hover:border-slate-600'}`}
>
Dernière version
Latest version
</button>
{history.map((h: any) => (
<div
@@ -290,18 +290,18 @@ export default function SuperContexte() {
>
<div className="font-medium">v{h.version}</div>
<div className="text-slate-500">{h.created_at?.slice(0, 16)}</div>
<div className="text-slate-600">{h.reports_used} rapports · {h.trades_analyzed} trades</div>
<div className="text-slate-600">{h.reports_used} reports · {h.trades_analyzed} trades</div>
</button>
<button
onClick={(e) => {
e.stopPropagation()
if (confirm(`Supprimer la version v${h.version} du Super Contexte ?`)) {
if (confirm(`Delete Super Context version v${h.version}?`)) {
if (selectedHistoryId === h.id) setSelectedHistoryId(null)
deleteState.mutate(h.id)
}
}}
className="absolute top-1.5 right-1.5 p-1 rounded opacity-0 group-hover:opacity-100 text-slate-600 hover:text-red-400 hover:bg-red-400/10 transition-all"
title="Supprimer cette version"
title="Delete this version"
>
<Trash2 className="w-3 h-3" />
</button>
@@ -314,17 +314,17 @@ export default function SuperContexte() {
<div className="flex-1 min-w-0 space-y-6">
{stateLoading && (
<div className="flex items-center justify-center h-40 text-slate-500">
<RefreshCw className="w-5 h-5 animate-spin mr-2" /> Chargement
<RefreshCw className="w-5 h-5 animate-spin mr-2" /> Loading
</div>
)}
{!currentState && !stateLoading && (
<div className="border border-violet-500/20 rounded-xl p-8 text-center bg-violet-500/5">
<Brain className="w-12 h-12 text-violet-400/50 mx-auto mb-3" />
<p className="text-slate-300 font-medium mb-1">Aucune synthèse disponible</p>
<p className="text-slate-300 font-medium mb-1">No synthesis available</p>
<p className="text-slate-500 text-sm mb-4">
Lance la première synthèse GPT-4o pour initialiser la base de raisonnement.
Le système analysera tous les rapports et trades disponibles.
Run the first GPT-4o synthesis to initialize the reasoning base.
The system will analyze all available reports and trades.
</p>
<button
onClick={() => handleSynthesize()}
@@ -332,7 +332,7 @@ export default function SuperContexte() {
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-violet-600 hover:bg-violet-500 disabled:opacity-50 text-sm font-medium transition-colors"
>
{synthesize.isPending ? <RefreshCw className="w-4 h-4 animate-spin" /> : <Brain className="w-4 h-4" />}
{synthesize.isPending ? 'Synthèse en cours…' : 'Initialiser le Super Contexte'}
{synthesize.isPending ? 'Synthesis in progress…' : 'Initialize Super Context'}
</button>
</div>
)}
@@ -341,10 +341,10 @@ export default function SuperContexte() {
<div className="border border-amber-500/30 bg-amber-500/5 rounded-xl p-4 flex items-start gap-3">
<Clock className="w-5 h-5 text-amber-400 flex-shrink-0 mt-0.5" />
<div className="flex-1 text-sm">
<span className="text-amber-300 font-medium">Synthèse non relancée. </span>
<span className="text-amber-300 font-medium">Synthesis not rerun. </span>
<span className="text-slate-400">
Dernière synthèse il y a {synthResult?.age_hours}h (moins de 6h).
Les trades immatures n'apportent pas encore d'information fiable inutile de recalculer.
Last synthesis {synthResult?.age_hours}h ago (less than 6h).
Immature trades don't yet provide reliable data — no need to recompute.
</span>
<div className="mt-2">
<button
@@ -352,7 +352,7 @@ export default function SuperContexte() {
disabled={synthesize.isPending}
className="text-xs px-3 py-1.5 rounded-lg border border-amber-500/40 text-amber-400 hover:bg-amber-500/10 transition-colors"
>
Forcer quand même la synthèse
Force synthesis anyway
</button>
</div>
</div>
@@ -363,11 +363,11 @@ export default function SuperContexte() {
<div className="border border-emerald-500/30 bg-emerald-500/5 rounded-xl p-4 flex items-center gap-3">
<CheckCircle className="w-5 h-5 text-emerald-400 flex-shrink-0" />
<div className="text-sm">
<span className="text-emerald-300 font-medium">Synthèse complète. </span>
<span className="text-emerald-300 font-medium">Synthesis complete. </span>
<span className="text-slate-400">
{synthResult?.kb_entries_added ?? 0} entrées KB ajoutées ·{' '}
{synthResult?.sources?.reports ?? 0} rapports ·{' '}
{synthResult?.sources?.trades ?? 0} trades analysés.
{synthResult?.kb_entries_added ?? 0} KB entries added ·{' '}
{synthResult?.sources?.reports ?? 0} reports ·{' '}
{synthResult?.sources?.trades ?? 0} trades analyzed.
</span>
</div>
</div>
@@ -379,7 +379,7 @@ export default function SuperContexte() {
<div className="border border-violet-500/20 bg-violet-500/5 rounded-xl p-5 space-y-3">
<div className="flex items-center gap-2 text-violet-300">
<Brain className="w-5 h-5" />
<span className="text-sm font-semibold">Narrative de raisonnement</span>
<span className="text-sm font-semibold">Reasoning narrative</span>
{selectedHistoryId && (
<span className="text-xs text-slate-500 ml-auto">v{currentState.version} (archivé)</span>
)}
@@ -393,7 +393,7 @@ export default function SuperContexte() {
{synthesis.regime_insights?.length > 0 && (
<div className="border border-slate-700/50 rounded-xl p-4 space-y-3">
<div className="flex items-center gap-2 text-blue-400 text-xs font-semibold uppercase tracking-wide">
<Activity className="w-4 h-4" />Régimes macro identifiés
<Activity className="w-4 h-4" />Identified macro regimes
</div>
<div className="space-y-2">
{synthesis.regime_insights.map((r: any, i: number) => (
@@ -407,7 +407,7 @@ export default function SuperContexte() {
{synthesis.pattern_insights?.length > 0 && (
<div className="border border-slate-700/50 rounded-xl p-4 space-y-3">
<div className="flex items-center gap-2 text-purple-400 text-xs font-semibold uppercase tracking-wide">
<BarChart2 className="w-4 h-4" />Patterns documentés
<BarChart2 className="w-4 h-4" />Documented patterns
</div>
<div className="space-y-2">
{synthesis.pattern_insights.map((p: any, i: number) => (
@@ -427,7 +427,7 @@ export default function SuperContexte() {
{synthesis.macro_correlations?.length > 0 && (
<div className="border border-slate-700/50 rounded-xl p-4 space-y-3">
<div className="flex items-center gap-2 text-emerald-400 text-xs font-semibold uppercase tracking-wide">
<TrendingUp className="w-4 h-4" />Corrélations macro/géo
<TrendingUp className="w-4 h-4" />Macro/geo correlations
</div>
<div className="space-y-2">
{synthesis.macro_correlations.map((c: any, i: number) => (
@@ -448,25 +448,25 @@ export default function SuperContexte() {
{/* Risk params + insights */}
<div className="border border-slate-700/50 rounded-xl p-4 space-y-4">
<InsightList
title="Priorités stratégiques"
title="Strategic priorities"
icon={<Target className="w-4 h-4" />}
items={synthesis.strategic_priorities || []}
color="text-violet-400"
/>
<InsightList
title="Forces identifiées"
title="Identified strengths"
icon={<Zap className="w-4 h-4" />}
items={synthesis.strengths || []}
color="text-emerald-400"
/>
<InsightList
title="Angles morts"
title="Blind spots"
icon={<AlertTriangle className="w-4 h-4" />}
items={synthesis.blind_spots || []}
color="text-amber-400"
/>
<InsightList
title="Erreurs récurrentes"
title="Recurring mistakes"
icon={<TrendingDown className="w-4 h-4" />}
items={(synthesis.recurring_mistakes || []).map((m: any) =>
typeof m === 'string' ? m : `${m.mistake}${m.mitigation}`
@@ -476,13 +476,13 @@ export default function SuperContexte() {
{synthesis.risk_parameters && (
<>
<InsightList
title="Préférer quand"
title="Prefer when"
icon={<Shield className="w-4 h-4" />}
items={synthesis.risk_parameters.prefer_when || []}
color="text-blue-400"
/>
<InsightList
title="Éviter quand"
title="Avoid when"
icon={<XCircle className="w-4 h-4" />}
items={synthesis.risk_parameters.avoid_when || []}
color="text-rose-400"
@@ -499,7 +499,7 @@ export default function SuperContexte() {
<div className="space-y-3">
<div className="flex items-center gap-2">
<BookOpen className="w-4 h-4 text-slate-400" />
<h2 className="text-sm font-semibold text-slate-300">Base de connaissances ({totalEntries} entrées)</h2>
<h2 className="text-sm font-semibold text-slate-300">Knowledge base ({totalEntries} entries)</h2>
</div>
<div className="space-y-3">
{Object.entries(byCategory).map(([cat, entries]: [string, any]) => (

View File

@@ -80,7 +80,7 @@ function LogRow({ log }: { log: any }) {
const AI_CALL_CONFIG: Record<string, { label: string; color: string; icon: React.ReactNode }> = {
suggest: { label: 'Suggestion patterns', color: 'text-purple-300 border-purple-700/40 bg-purple-900/20', icon: <Brain className="w-3 h-3" /> },
score_batch: { label: 'Scoring batch', color: 'text-cyan-300 border-cyan-700/40 bg-cyan-900/20', icon: <BarChart2 className="w-3 h-3" /> },
unknown: { label: 'Appel IA', color: 'text-slate-300 border-slate-700/40 bg-slate-800', icon: <Zap className="w-3 h-3" /> },
unknown: { label: 'AI call', color: 'text-slate-300 border-slate-700/40 bg-slate-800', icon: <Zap className="w-3 h-3" /> },
}
function AiCallRow({ call }: { call: any }) {
@@ -126,19 +126,19 @@ function AiCallRow({ call }: { call: any }) {
{/* Token breakdown */}
{totalTokens > 0 && (
<div className="flex gap-4 px-3 py-2 bg-slate-900/50 text-[10px] font-mono text-slate-500 border-b border-slate-800">
<span>Modèle: <span className="text-slate-300">{call.model || 'gpt-4o'}</span></span>
<span>Model: <span className="text-slate-300">{call.model || 'gpt-4o'}</span></span>
<span>Prompt: <span className="text-amber-300">{(call.tokens_prompt ?? 0).toLocaleString()} tok</span></span>
<span>Completion: <span className="text-green-300">{(call.tokens_completion ?? 0).toLocaleString()} tok</span></span>
<span>Total: <span className="text-white">{totalTokens.toLocaleString()} tok</span></span>
<span>Durée: <span className="text-purple-300">{(call.duration_ms / 1000).toFixed(2)}s</span></span>
<span>Duration: <span className="text-purple-300">{(call.duration_ms / 1000).toFixed(2)}s</span></span>
</div>
)}
{/* Pane selector */}
<div className="flex gap-0 border-b border-slate-800">
{([
{ key: 'user' as const, label: 'Prompt utilisateur', icon: <MessageSquare className="w-3 h-3" /> },
{ key: 'system' as const, label: 'Prompt système', icon: <Brain className="w-3 h-3" /> },
{ key: 'response' as const, label: 'ponse IA', icon: <Zap className="w-3 h-3" /> },
{ key: 'user' as const, label: 'User prompt', icon: <MessageSquare className="w-3 h-3" /> },
{ key: 'system' as const, label: 'System prompt', icon: <Brain className="w-3 h-3" /> },
{ key: 'response' as const, label: 'AI response', icon: <Zap className="w-3 h-3" /> },
]).map(p => (
<button
key={p.key}
@@ -157,13 +157,13 @@ function AiCallRow({ call }: { call: any }) {
{/* Content */}
<div className="p-3 bg-black/40 max-h-[500px] overflow-y-auto">
{activePane === 'user' && (
<pre className="text-[10px] font-mono text-slate-300 whitespace-pre-wrap leading-relaxed">{call.user_prompt || '(vide)'}</pre>
<pre className="text-[10px] font-mono text-slate-300 whitespace-pre-wrap leading-relaxed">{call.user_prompt || '(empty)'}</pre>
)}
{activePane === 'system' && (
<pre className="text-[10px] font-mono text-slate-400 whitespace-pre-wrap leading-relaxed">{call.system_prompt || '(vide)'}</pre>
<pre className="text-[10px] font-mono text-slate-400 whitespace-pre-wrap leading-relaxed">{call.system_prompt || '(empty)'}</pre>
)}
{activePane === 'response' && (
<pre className="text-[10px] font-mono text-green-300 whitespace-pre-wrap leading-relaxed">{responseStr || '(pas de réponse)'}</pre>
<pre className="text-[10px] font-mono text-green-300 whitespace-pre-wrap leading-relaxed">{responseStr || '(no response)'}</pre>
)}
</div>
</div>
@@ -176,17 +176,17 @@ function AiCallsSection({ runId }: { runId: string }) {
const { data, isLoading } = useAiCallLogs(runId)
const calls: any[] = data?.calls ?? []
if (isLoading) return <div className="p-4 text-xs text-slate-500">Chargement des appels IA</div>
if (isLoading) return <div className="p-4 text-xs text-slate-500">Loading AI calls</div>
if (calls.length === 0) return (
<div className="p-4 text-xs text-slate-600 text-center">
Aucun appel IA enregistré pour ce cycle les logs seront disponibles dès le prochain cycle.
No AI calls recorded for this cycle logs will be available from the next cycle.
</div>
)
return (
<div className="space-y-2">
<div className="text-[10px] text-slate-500 px-1 font-mono">
{calls.length} appel(s) IA cliquer pour voir prompts + réponse complète
{calls.length} AI call(s) click to view prompts + full response
</div>
{calls.map(c => <AiCallRow key={c.id} call={c} />)}
</div>
@@ -221,10 +221,10 @@ function ContextTab() {
{/* Left: list of snapshots */}
<div className="card overflow-y-auto max-h-[700px]">
{loadingList ? (
<div className="p-4 text-xs text-slate-500">Chargement</div>
<div className="p-4 text-xs text-slate-500">Loading</div>
) : snapshots.length === 0 ? (
<div className="p-4 text-xs text-slate-600">
Aucun snapshot les contextes seront sauvegardés lors du prochain cycle.
No snapshot contexts will be saved on the next cycle.
</div>
) : (
<div className="divide-y divide-slate-800">
@@ -250,12 +250,12 @@ function ContextTab() {
{!selectedRunId ? (
<div className="p-8 text-center text-slate-600 text-sm flex-1">
<Brain className="w-8 h-8 mx-auto mb-2 opacity-30" />
Sélectionne un cycle à gauche pour voir le contexte complet envoyé à l'IA.
Select a cycle on the left to view the full context sent to the AI.
</div>
) : loadingSnap ? (
<div className="p-8 text-center text-slate-500 text-sm flex-1">Chargement du contexte…</div>
<div className="p-8 text-center text-slate-500 text-sm flex-1">Loading context</div>
) : !snapData ? (
<div className="p-8 text-center text-slate-500 text-sm flex-1">Erreur lors du chargement.</div>
<div className="p-8 text-center text-slate-500 text-sm flex-1">Loading error.</div>
) : (
<>
{/* Header strip with meta + replay */}
@@ -273,7 +273,7 @@ function ContextTab() {
<div className="flex items-center gap-2 ml-auto">
<input
className="bg-slate-800 border border-slate-700 rounded px-2 py-1 text-[10px] text-slate-300 w-48"
placeholder="Notes de replay (optionnel)"
placeholder="Replay notes (optional)"
value={replayNotes}
onChange={e => setReplayNotes(e.target.value)}
/>
@@ -283,8 +283,8 @@ function ContextTab() {
className="flex items-center gap-1.5 bg-purple-700 hover:bg-purple-600 disabled:opacity-40 text-white px-3 py-1.5 rounded text-xs font-semibold"
>
{replaying
? <><Loader2 className="w-3 h-3 animate-spin" /> Replay en cours…</>
: <><Play className="w-3 h-3" /> Rejouer ce cycle</>}
? <><Loader2 className="w-3 h-3 animate-spin" /> Replaying</>
: <><Play className="w-3 h-3" /> Replay this cycle</>}
</button>
</div>
</div>
@@ -294,7 +294,7 @@ function ContextTab() {
<div className="border-b border-slate-800 bg-green-950/20 p-3">
<div className="flex items-center gap-2 mb-2">
<span className="text-xs font-semibold text-green-400">
Replay terminé — {replayResult.suggestions_count} patterns générés
Replay complete {replayResult.suggestions_count} patterns generated
</span>
<span className="text-[10px] text-slate-500">à {fmtTs(replayResult.replayed_at)}</span>
</div>
@@ -312,8 +312,8 @@ function ContextTab() {
{/* Sub-tabs: Contexte / Appels IA */}
<div className="flex gap-0 border-b border-slate-800 bg-slate-900/40 shrink-0">
{([
{ key: 'context' as const, label: 'Contexte IA', icon: <Brain className="w-3 h-3" /> },
{ key: 'calls' as const, label: 'Appels IA', icon: <Zap className="w-3 h-3" /> },
{ key: 'context' as const, label: 'AI Context', icon: <Brain className="w-3 h-3" /> },
{ key: 'calls' as const, label: 'AI Calls', icon: <Zap className="w-3 h-3" /> },
]).map(t => (
<button
key={t.key}
@@ -376,7 +376,7 @@ function ContextSection({ label, value }: { label: string; value: any }) {
>
{open ? <ChevronDown className="w-3 h-3 text-slate-500 shrink-0" /> : <ChevronRight className="w-3 h-3 text-slate-500 shrink-0" />}
<span className={clsx('text-xs font-mono font-semibold', labelColor[label] ?? 'text-slate-300')}>{label}</span>
<span className="text-[10px] text-slate-600 ml-auto">{lineCount} lignes</span>
<span className="text-[10px] text-slate-600 ml-auto">{lineCount} lines</span>
</button>
{open && (
<pre className="text-[10px] font-mono text-slate-400 bg-slate-950 p-3 overflow-x-auto max-h-80 leading-relaxed">
@@ -415,7 +415,7 @@ export default function SystemLogs() {
}
const handleClear = () => {
if (window.confirm('Supprimer les logs de plus de 30 jours ?')) {
if (window.confirm('Delete logs older than 30 days?')) {
clearLogs(30, { onSuccess: () => qc.invalidateQueries({ queryKey: ['system-logs'] }) })
}
}
@@ -425,8 +425,8 @@ export default function SystemLogs() {
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold text-white">Logs Système</h1>
<p className="text-xs text-slate-500 mt-0.5">Erreurs, avertissements et contextes complets des cycles IA</p>
<h1 className="text-xl font-bold text-white">System Logs</h1>
<p className="text-xs text-slate-500 mt-0.5">Errors, warnings and full AI cycle contexts</p>
</div>
<div className="flex items-center gap-2">
<button
@@ -434,7 +434,7 @@ export default function SystemLogs() {
className="btn-secondary flex items-center gap-1.5 text-xs px-3 py-1.5"
>
<RefreshCw className={clsx('w-3.5 h-3.5', (isLoading || isRefetching) && 'animate-spin')} />
Actualiser
Refresh
</button>
{activeTab === 'logs' && (
<button
@@ -443,7 +443,7 @@ export default function SystemLogs() {
className="btn-secondary flex items-center gap-1.5 text-xs px-3 py-1.5 text-red-400 border-red-800/40 hover:bg-red-900/20"
>
<Trash2 className="w-3.5 h-3.5" />
Purger &gt;30j
Purge &gt;30d
</button>
)}
</div>
@@ -452,8 +452,8 @@ export default function SystemLogs() {
{/* Tabs */}
<div className="flex gap-1 border-b border-slate-800">
{([
{ key: 'logs', label: 'Logs système', icon: <Info className="w-3.5 h-3.5" /> },
{ key: 'context', label: 'Contexte IA', icon: <Brain className="w-3.5 h-3.5" /> },
{ key: 'logs', label: 'System Logs', icon: <Info className="w-3.5 h-3.5" /> },
{ key: 'context', label: 'AI Context', icon: <Brain className="w-3.5 h-3.5" /> },
] as const).map(tab => (
<button
key={tab.key}
@@ -493,7 +493,7 @@ export default function SystemLogs() {
{level} · {counts[level as keyof typeof counts] ?? 0}
</button>
))}
<span className="text-xs text-slate-600 ml-auto">{logs.length} entrées</span>
<span className="text-xs text-slate-600 ml-auto">{logs.length} entries</span>
</div>
{/* Filters */}
@@ -505,7 +505,7 @@ export default function SystemLogs() {
value={filters.source ?? ''}
onChange={e => applyFilter('source', e.target.value)}
>
<option value="">Toutes</option>
<option value="">All</option>
{sources.map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
@@ -517,7 +517,7 @@ export default function SystemLogs() {
value={filters.cycle_id ?? ''}
onChange={e => applyFilter('cycle_id', e.target.value)}
>
<option value="">Tous</option>
<option value="">All</option>
{cycles.map(c => (
<option key={c.cycle_id} value={c.cycle_id}>
{c.first_ts ? format(new Date(c.first_ts), 'dd/MM HH:mm') : c.cycle_id.slice(0, 16)}
@@ -540,7 +540,7 @@ export default function SystemLogs() {
<div className="grid grid-cols-2 gap-2">
<div>
<label className="text-[9px] text-slate-500 uppercase tracking-wide block mb-1">Depuis</label>
<label className="text-[9px] text-slate-500 uppercase tracking-wide block mb-1">From</label>
<input
type="date"
className="w-full bg-slate-800 border border-slate-700 rounded px-2 py-1 text-xs text-slate-300"
@@ -549,7 +549,7 @@ export default function SystemLogs() {
/>
</div>
<div>
<label className="text-[9px] text-slate-500 uppercase tracking-wide block mb-1">Jusqu'au</label>
<label className="text-[9px] text-slate-500 uppercase tracking-wide block mb-1">Until</label>
<input
type="date"
className="w-full bg-slate-800 border border-slate-700 rounded px-2 py-1 text-xs text-slate-300"
@@ -563,19 +563,19 @@ export default function SystemLogs() {
{/* Log table */}
<div className="card overflow-hidden">
{isLoading ? (
<div className="p-8 text-center text-slate-500 text-sm">Chargement</div>
<div className="p-8 text-center text-slate-500 text-sm">Loading</div>
) : logs.length === 0 ? (
<div className="p-8 text-center text-slate-600 text-sm">
Aucun log trouvé pour ces filtres.<br />
<span className="text-xs text-slate-700">Les WARNING et ERROR des cycles apparaissent ici automatiquement.</span>
No logs found for these filters.<br />
<span className="text-xs text-slate-700">WARNING and ERROR entries from cycles appear here automatically.</span>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="border-b border-slate-700 text-[9px] uppercase tracking-wide text-slate-500">
<th className="px-3 py-2 text-left font-medium whitespace-nowrap">Horodatage</th>
<th className="px-3 py-2 text-left font-medium">Niveau</th>
<th className="px-3 py-2 text-left font-medium whitespace-nowrap">Timestamp</th>
<th className="px-3 py-2 text-left font-medium">Level</th>
<th className="px-3 py-2 text-left font-medium">Source</th>
<th className="px-3 py-2 text-left font-medium">Cycle</th>
<th className="px-3 py-2 text-left font-medium">Ticker</th>

View File

@@ -11,7 +11,7 @@ import clsx from 'clsx'
async function fetchLatest() {
const r = await fetch('/api/var/latest')
if (!r.ok) throw new Error(`Backend inaccessible (${r.status})`)
if (!r.ok) throw new Error(`Backend unreachable (${r.status})`)
return r.json()
}
@@ -52,7 +52,7 @@ function MetricCard({
<div className="text-[11px] text-slate-500 mb-3">{method}</div>
<div className="space-y-2">
<div>
<div className="text-[10px] text-slate-500 mb-0.5">VaR {horizon}j</div>
<div className="text-[10px] text-slate-500 mb-0.5">VaR {horizon}d</div>
<div className={clsx('text-2xl font-bold', displayed < 0 ? 'text-red-400' : 'text-emerald-400')}>
{pct(displayed)}
</div>
@@ -101,10 +101,10 @@ function IdleState({ onCompute, computing }: { onCompute: () => void; computing:
<div className="flex flex-col items-center justify-center py-24 gap-6">
<ShieldAlert className="w-16 h-16 text-slate-700" />
<div className="text-center">
<h2 className="text-lg font-semibold text-slate-400 mb-2">Aucun snapshot VaR disponible</h2>
<h2 className="text-lg font-semibold text-slate-400 mb-2">No VaR snapshot available</h2>
<p className="text-slate-600 text-sm max-w-md">
Le calcul VaR ne se lance pas automatiquement pour éviter des appels réseau inutiles.
Cliquez sur "Calculer maintenant" ou configurez le scheduler dans la Configuration.
The VaR calculation does not run automatically to avoid unnecessary network calls.
Click "Compute now" or configure the scheduler in Settings.
</p>
</div>
<button
@@ -113,7 +113,7 @@ function IdleState({ onCompute, computing }: { onCompute: () => void; computing:
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white px-6 py-3 rounded-lg font-semibold text-sm"
>
<Play className={clsx('w-4 h-4', computing && 'animate-pulse')} />
{computing ? 'Calcul en cours…' : 'Calculer maintenant'}
{computing ? 'Computing…' : 'Compute now'}
</button>
</div>
)
@@ -180,15 +180,15 @@ export default function VaRAnalysis() {
<div>
<div className="flex items-center gap-3 mb-1">
<ShieldAlert className="w-6 h-6 text-red-400" />
<h1 className="text-xl font-bold text-white">Analyse VaR Value at Risk</h1>
<h1 className="text-xl font-bold text-white">VaR Analysis Value at Risk</h1>
</div>
<p className="text-slate-400 text-sm">
Approche delta Black-Scholes
Delta Black-Scholes approach
{displayedMeta && (
<span className="ml-2 text-slate-500">
· snapshot du {displayedMeta.computed_at?.slice(0, 16).replace('T', ' ')} UTC
· snapshot from {displayedMeta.computed_at?.slice(0, 16).replace('T', ' ')} UTC
{displayedMeta.data_source === 'simulated' && (
<span className="ml-2 text-amber-400"> données simulées</span>
<span className="ml-2 text-amber-400"> simulated data</span>
)}
</span>
)}
@@ -199,7 +199,7 @@ export default function VaRAnalysis() {
<div className="flex flex-wrap gap-3 items-end">
{/* Confidence */}
<div>
<div className="text-[10px] text-slate-500 mb-1 uppercase tracking-wider">Confiance</div>
<div className="text-[10px] text-slate-500 mb-1 uppercase tracking-wider">Confidence</div>
<div className="flex gap-1">
{[0.90, 0.95, 0.99].map(c => (
<button key={c} onClick={() => setConfidence(c)}
@@ -218,14 +218,14 @@ export default function VaRAnalysis() {
<button key={h} onClick={() => setHorizon(h)}
className={clsx('px-2.5 py-1.5 rounded text-xs font-semibold',
horizon === h ? 'bg-violet-600 text-white' : 'bg-dark-700 text-slate-400 hover:text-white')}>
{h}j
{h}d
</button>
))}
</div>
</div>
{/* Lookback */}
<div>
<div className="text-[10px] text-slate-500 mb-1 uppercase tracking-wider">Historique</div>
<div className="text-[10px] text-slate-500 mb-1 uppercase tracking-wider">Lookback</div>
<div className="flex gap-1">
{[63, 126, 252].map(l => (
<button key={l} onClick={() => setLookback(l)}
@@ -238,7 +238,7 @@ export default function VaRAnalysis() {
</div>
{/* IV */}
<div>
<div className="text-[10px] text-slate-500 mb-1 uppercase tracking-wider">IV défaut</div>
<div className="text-[10px] text-slate-500 mb-1 uppercase tracking-wider">Default IV</div>
<div className="flex gap-1">
{[0.15, 0.20, 0.30, 0.40].map(v => (
<button key={v} onClick={() => setIv(v)}
@@ -256,7 +256,7 @@ export default function VaRAnalysis() {
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white px-4 py-2 rounded text-sm font-semibold"
>
<Play className={clsx('w-3.5 h-3.5', computing && 'animate-pulse')} />
{computing ? 'Calcul…' : 'Calculer'}
{computing ? 'Computing…' : 'Compute'}
</button>
</div>
</div>
@@ -265,7 +265,7 @@ export default function VaRAnalysis() {
{backendDown && (
<div className="bg-red-900/20 border border-red-700/40 rounded-xl p-4 text-red-400 flex items-center gap-2 text-sm">
<AlertTriangle className="w-4 h-4 shrink-0" />
Backend inaccessible vérifiez que le serveur FastAPI est démarré sur le port 8000.
Backend unreachable check that the FastAPI server is running on port 8000.
</div>
)}
@@ -281,12 +281,12 @@ export default function VaRAnalysis() {
{snapshots.length > 1 && (
<div className="flex items-center gap-2 flex-wrap">
<Database className="w-3.5 h-3.5 text-slate-500" />
<span className="text-xs text-slate-500">Historique :</span>
<span className="text-xs text-slate-500">History:</span>
<button
onClick={() => setActiveSnap(null)}
className={clsx('text-xs px-2 py-1 rounded', !activeSnap ? 'bg-blue-900/40 text-blue-400 border border-blue-700/40' : 'bg-dark-700 text-slate-500 hover:text-slate-300')}
>
Dernier
Latest
</button>
{snapshots.slice(1).map((s: any) => (
<button
@@ -307,7 +307,7 @@ export default function VaRAnalysis() {
{/* Empty / loading state */}
{loadingLatest && !displayedResult && (
<div className="text-center py-16 text-slate-600">Chargement du dernier snapshot</div>
<div className="text-center py-16 text-slate-600">Loading latest snapshot</div>
)}
{!loadingLatest && !displayedResult && !backendDown && (
@@ -322,9 +322,9 @@ export default function VaRAnalysis() {
{[
{ label: 'Positions', val: String(portfolio.n_positions) },
{ label: 'Notionnel', val: `${portfolio.total_notional_eur.toLocaleString('fr-FR', { maximumFractionDigits: 0 })} €` },
{ label: 'Confiance', val: `${portfolio.confidence_pct}%` },
{ label: 'Lookback', val: `${portfolio.lookback_days}j` },
{ label: 'Source', val: portfolio.data_source === 'simulated' ? '⚠ Simulée' : '✓ Live' },
{ label: 'Confidence', val: `${portfolio.confidence_pct}%` },
{ label: 'Lookback', val: `${portfolio.lookback_days}d` },
{ label: 'Source', val: portfolio.data_source === 'simulated' ? '⚠ Simulated' : '✓ Live' },
].map(({ label, val }) => (
<div key={label} className="bg-dark-800 border border-slate-700/30 rounded-lg px-3 py-2">
<div className="text-slate-500">{label}</div>
@@ -336,21 +336,21 @@ export default function VaRAnalysis() {
{/* 3 VaR method cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<MetricCard
label="Historique" method="Percentile empirique"
label="Historical" method="Empirical percentile"
var1d={varData.historical.var_1d_pct} varNd={varData.historical.var_nd_pct}
cvar={varData.historical.cvar_pct}
varEur={varData.historical.var_1d_eur} cvarEur={varData.historical.cvar_eur}
horizon={horizon} colorClass="border-blue-700/40"
/>
<MetricCard
label="Paramétrique" method="Distribution normale"
label="Parametric" method="Normal distribution"
var1d={varData.parametric.var_1d_pct} varNd={varData.parametric.var_nd_pct}
cvar={varData.parametric.cvar_pct}
varEur={varData.parametric.var_1d_eur} cvarEur={varData.parametric.cvar_eur}
horizon={horizon} colorClass="border-violet-700/40"
/>
<MetricCard
label="Monte Carlo" method="Vol stressée ×1.5"
label="Monte Carlo" method="Stressed vol ×1.5"
var1d={varData.monte_carlo.var_1d_pct} varNd={varData.monte_carlo.var_nd_pct}
cvar={varData.monte_carlo.cvar_pct}
varEur={varData.monte_carlo.var_1d_eur} cvarEur={varData.monte_carlo.cvar_eur}
@@ -364,8 +364,8 @@ export default function VaRAnalysis() {
<div className="bg-dark-800 border border-slate-700/40 rounded-xl p-4">
<div className="flex items-center gap-2 mb-3">
<Activity className="w-4 h-4 text-slate-400" />
<h3 className="text-sm font-semibold text-white">Distribution des Retours</h3>
<span className="text-[10px] text-slate-600 ml-auto">trait rouge = VaR hist.</span>
<h3 className="text-sm font-semibold text-white">Return Distribution</h3>
<span className="text-[10px] text-slate-600 ml-auto">red line = hist. VaR</span>
</div>
{histogram.length > 0 ? (
<ResponsiveContainer width="100%" height={210}>
@@ -383,7 +383,7 @@ export default function VaRAnalysis() {
</BarChart>
</ResponsiveContainer>
) : (
<div className="h-[210px] flex items-center justify-center text-slate-600 text-sm">Aucune donnée</div>
<div className="h-[210px] flex items-center justify-center text-slate-600 text-sm">No data</div>
)}
</div>
@@ -391,7 +391,7 @@ export default function VaRAnalysis() {
<div className="bg-dark-800 border border-slate-700/40 rounded-xl p-4">
<div className="flex items-center gap-2 mb-3">
<TrendingDown className="w-4 h-4 text-red-400" />
<h3 className="text-sm font-semibold text-white">VaR 95% Glissante (fenêtre 30J)</h3>
<h3 className="text-sm font-semibold text-white">Rolling VaR 95% (30d window)</h3>
</div>
{rolling.length > 0 ? (
<ResponsiveContainer width="100%" height={210}>
@@ -406,7 +406,7 @@ export default function VaRAnalysis() {
</LineChart>
</ResponsiveContainer>
) : (
<div className="h-[210px] flex items-center justify-center text-slate-600 text-sm">Historique insuffisant</div>
<div className="h-[210px] flex items-center justify-center text-slate-600 text-sm">Insufficient history</div>
)}
</div>
</div>
@@ -415,7 +415,7 @@ export default function VaRAnalysis() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Positions */}
<div className="bg-dark-800 border border-slate-700/40 rounded-xl p-4">
<h3 className="text-sm font-semibold text-white mb-3">Positions & Deltas</h3>
<h3 className="text-sm font-semibold text-white mb-3">Positions &amp; Deltas</h3>
<div className="space-y-1.5">
{positions.map((p: any, i: number) => (
<div key={i} className="flex items-center justify-between text-xs py-1 border-b border-slate-700/20 last:border-0">
@@ -442,13 +442,13 @@ export default function VaRAnalysis() {
{/* Kupiec backtest */}
{backtest && (
<div className="bg-dark-800 border border-slate-700/40 rounded-xl p-4">
<h3 className="text-sm font-semibold text-white mb-3">Backtest Test de Kupiec</h3>
<h3 className="text-sm font-semibold text-white mb-3">Backtest Kupiec Test</h3>
<div className="space-y-2.5">
{[
{ label: 'Observations', val: String(backtest.n_observations) },
{ label: 'Violations VaR', val: String(backtest.n_breaches) },
{ label: 'Taux réel', val: `${backtest.breach_rate_pct}%`, color: backtest.kupiec_ok ? 'text-emerald-400' : 'text-red-400' },
{ label: 'Taux attendu', val: `${backtest.expected_breach_rate_pct}%`, color: 'text-slate-300' },
{ label: 'VaR Violations', val: String(backtest.n_breaches) },
{ label: 'Actual rate', val: `${backtest.breach_rate_pct}%`, color: backtest.kupiec_ok ? 'text-emerald-400' : 'text-red-400' },
{ label: 'Expected rate', val: `${backtest.expected_breach_rate_pct}%`, color: 'text-slate-300' },
].map(({ label, val, color }) => (
<div key={label} className="flex justify-between text-sm">
<span className="text-slate-400">{label}</span>
@@ -460,11 +460,11 @@ export default function VaRAnalysis() {
? 'bg-emerald-900/20 border border-emerald-700/40 text-emerald-400'
: 'bg-red-900/20 border border-red-700/40 text-red-400')}>
{backtest.kupiec_ok
? '✓ Modèle validé — violations dans la tolérance'
: '✗ Excès de violations — modèle sous-estime le risque'}
? '✓ Model validated — violations within tolerance'
: '✗ Excess violations — model underestimates risk'}
</div>
<div className="text-[10px] text-slate-600">
Règle Kupiec : taux réel {(backtest.expected_breach_rate_pct * 2).toFixed(1)}%
Kupiec rule: actual rate {(backtest.expected_breach_rate_pct * 2).toFixed(1)}%
</div>
</div>
</div>