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) 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( def _is_duplicate_pattern(
@@ -59,8 +59,8 @@ def _is_duplicate_pattern(
jaccard_threshold: float = 0.30, jaccard_threshold: float = 0.30,
) -> bool: ) -> bool:
""" """
Retourne True si le candidat est trop similaire à un pattern existant. Returns True if the candidate is too similar to an existing pattern.
Essaie les embeddings cosinus (Sprint 4.3) avec fallback Jaccard. Tries cosine embeddings (Sprint 4.3) with Jaccard fallback.
""" """
if not existing: if not existing:
return False return False
@@ -77,7 +77,7 @@ def _is_duplicate_pattern(
api_key=api_key, api_key=api_key,
candidate_id=candidate.get("id"), candidate_id=candidate.get("id"),
) )
if sim > 0: # embedding a fonctionné if sim > 0: # embedding worked
return sim >= _EMBED_SIM_THRESHOLD return sim >= _EMBED_SIM_THRESHOLD
except Exception as _emb_err: except Exception as _emb_err:
logger.debug(f"[Cycle] Embedding failed, fallback Jaccard: {_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", [])) n_alerts = len(risk.get("alerts", []))
system_prompt = ( system_prompt = (
"Tu es un risk manager spécialisé dans les portefeuilles d'options géopolitiques. " "You are a risk manager specializing in geopolitical options portfolios. "
"Analyse l'état du portefeuille simulé ci-dessous et génère des recommandations concrètes. " "Analyze the state of the simulated portfolio below and generate concrete recommendations. "
"Réponds en JSON avec ce schéma exact: " "Respond in JSON with this exact schema: "
'{"assessment": "<2 phrases max sur l\'état global>", ' '{"assessment": "<max 2 sentences on the overall state>", '
'"actions": [{"priority": "high|medium", "type": "close_trade|rebalance|monitor", ' '"actions": [{"priority": "high|medium", "type": "close_trade|rebalance|monitor", '
'"trade_id": <int ou null>, "underlying": "<ticker>", "reason": "<raison courte>"}], ' '"trade_id": <int or null>, "underlying": "<ticker>", "reason": "<short reason>"}], '
'"rebalance_suggestion": "<suggestion concrète de rééquilibrage en 1 phrase>"}' '"rebalance_suggestion": "<concrete rebalancing suggestion in 1 sentence>"}'
) )
user_prompt = context 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: 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.""" """Build a prompt section describing current portfolio for injection into AI prompts."""
if not open_trades: 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) total = len(open_trades)
conc_sorted = sorted(concentration.items(), key=lambda x: -x[1]) conc_sorted = sorted(concentration.items(), key=lambda x: -x[1])
conc_str = " | ".join(f"{cls.upper()}: {n}" for cls, n in conc_sorted) 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: for t in open_trades:
sym = t["underlying"] sym = t["underlying"]
@@ -118,27 +118,27 @@ def build_portfolio_context_block(open_trades: List[Dict], concentration: Dict[s
pat = t.get("pattern_name", "") pat = t.get("pattern_name", "")
entry = t.get("entry_price") entry = t.get("entry_price")
cur = t.get("current_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: if ep_str:
line += f" | {ep_str}" line += f" | {ep_str}"
if pat: if pat:
line += f" | thèse: «{pat}»" line += f" | thesis: «{pat}»"
lines.append(line) lines.append(line)
# Identify overweight classes # Identify overweight classes
overweight = [cls for cls, n in conc_sorted if n >= 3] 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 = ( block = (
"\n## PORTEFEUILLE ACTUEL — POSITIONS OUVERTES\n" "\n## CURRENT PORTFOLIO — OPEN POSITIONS\n"
+ "\n".join(lines) + "\n".join(lines)
+ f"\n\nClasses surpondérées (≥3 trades): {ow_str}\n" + f"\n\nOverweight classes (≥3 trades): {ow_str}\n"
+ "\n⚠️ CONSIGNES IMPÉRATIVES (nongociables):\n" + "\n⚠️ MANDATORY RULES (non-negotiable):\n"
+ "1. NE PAS suggérer un nouveau trade sur un sous-jacent déjà en portefeuille — doublement interdit.\n" + "1. DO NOT suggest a new trade on an underlying already in the portfolio — strictly forbidden.\n"
+ "2. Signaler explicitement dans 'rationale' si une suggestion CONTREDIT une position ouverte (signal de clôture potentiel).\n" + "2. Explicitly flag in 'rationale' if a suggestion CONTRADICTS an open position (potential exit signal).\n"
+ "3. Éviter d'alourdir une classe surpondérée (≥3 trades) sauf catalyseur exceptionnel justifié.\n" + "3. Avoid adding to an overweight class (≥3 trades) unless an exceptional justified catalyst exists.\n"
+ "4. Un signal opposé à une position ouverte = opportunité de SORTIE à documenter, pas d'entrée inversée.\n" + "4. A signal opposing an open position = EXIT opportunity to document, not a reverse entry.\n"
) )
return block return block

View File

@@ -33,25 +33,25 @@ const BUCKET_ICONS: Record<string, string> = {
} }
export const CATEGORIES = [ export const CATEGORIES = [
{ key: 'all', label: 'Tous' }, { key: 'all', label: 'All' },
{ key: 'energy', label: '⛽ Énergie' }, { key: 'energy', label: '⛽ Energy' },
{ key: 'metals', label: '🥇 Métaux' }, { key: 'metals', label: '🥇 Metals' },
{ key: 'agriculture', label: '🌾 Agri' }, { key: 'agriculture', label: '🌾 Agri' },
{ key: 'indices', label: '📊 Indices' }, { key: 'indices', label: '📊 Indices' },
{ key: 'equities', label: '📈 Actions' }, { key: 'equities', label: '📈 Equities' },
{ key: 'forex', label: '💱 Forex' }, { key: 'forex', label: '💱 Forex' },
] ]
export const THEMATIC_CATEGORIES = [ export const THEMATIC_CATEGORIES = [
{ key: 'all', label: 'Toutes' }, { key: 'all', label: 'All' },
{ key: 'géopolitique', label: '⚔️ Géopo' }, { key: 'géopolitique', label: '⚔️ Geopo' },
{ key: 'macro_monétaire', label: '🏦 Macro' }, { key: 'macro_monétaire', label: '🏦 Macro' },
{ key: 'technique', label: '📐 Technique' }, { key: 'technique', label: '📐 Technical' },
{ key: 'commodités_supply',label: '🛢️ Supply' }, { key: 'commodités_supply',label: '🛢️ Supply' },
{ key: 'risk_off', label: '🌩️ Risk-off' }, { key: 'risk_off', label: '🌩️ Risk-off' },
{ key: 'flux_saisonnier', label: '📅 Flux' }, { key: 'flux_saisonnier', label: '📅 Flows' },
{ key: 'géo_économique', label: '🌐 Géo-éco' }, { key: 'géo_économique', label: '🌐 Geo-eco' },
{ key: 'crédit_stress', label: '💳 Crédit' }, { key: 'crédit_stress', label: '💳 Credit' },
] ]
const SIGNAL_DIR: Record<string, { label: string; color: string }> = { 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 }> = { const BIAS_DISPLAY: Record<string, { label: string; color: string }> = {
'bullish+': { label: '★★ Compatible', color: '#10b981' }, 'bullish+': { label: '★★ Aligned', color: '#10b981' },
'bullish': { label: '★ Compatible', color: '#34d399' }, 'bullish': { label: '★ Compatible', color: '#34d399' },
'neutral': { label: '→ Neutre', color: '#64748b' }, 'neutral': { label: '→ Neutral', color: '#64748b' },
'bearish': { label: '✗ favorable', color: '#f97316' }, 'bearish': { label: '✗ Unfavorable', color: '#f97316' },
'bearish+': { label: '✗✗ Contra', color: '#ef4444' }, 'bearish+': { label: '✗✗ Contra', color: '#ef4444' },
'defensive':{ label: '⚠ Défensif', color: '#f59e0b' }, 'defensive':{ label: '⚠ Defensive', color: '#f59e0b' },
} }
// ── IBKR helpers ────────────────────────────────────────────────────────────── // ── IBKR helpers ──────────────────────────────────────────────────────────────
@@ -234,33 +234,33 @@ function IBKRTicket({ strategy, underlying, strikeGuidance, expiryDays, entryPri
return ( return (
<div className="bg-blue-950/20 border border-blue-700/30 rounded p-3 space-y-2.5"> <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"> <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 <Terminal className="w-3 h-3" /> IBKR Ticket
{!entryPrice && <span className="ml-auto text-slate-600 font-normal normal-case">Prix non disponible vérifier dans IBKR</span>} {!entryPrice && <span className="ml-auto text-slate-600 font-normal normal-case">Price unavailable check in IBKR</span>}
</div> </div>
<div className="grid grid-cols-3 gap-3 text-[10px]"> <div className="grid grid-cols-3 gap-3 text-[10px]">
<div> <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-white font-mono font-bold text-sm">{underlying}</div>
<div className="text-slate-600 text-[9px]">Security Type: OPT</div> <div className="text-slate-600 text-[9px]">Security Type: OPT</div>
</div> </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> <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> <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"> <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>
<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> </div>
<div className="space-y-1"> <div className="space-y-1">
{legs.map((leg, i) => ( {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]"> <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={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={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-slate-600">Strike</span>
<span className="text-yellow-300 font-bold">{fmtStrike(leg.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>
<div className="grid grid-cols-3 gap-3 text-[10px]"> <div className="grid grid-cols-3 gap-3 text-[10px]">
<div> <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-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> <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-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> <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 className="text-emerald-400 font-mono">{targetGain ? `+${targetGain.toFixed(0)}` : '—'}</div>
</div> </div>
</div> </div>
@@ -373,7 +373,7 @@ export function TradeCard({ item, onAdd, macroInfo, addedInfo, profiles }: {
)} )}
</div> </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> </div>
{convergenceCount > 0 && ( {convergenceCount > 0 && (
@@ -404,7 +404,7 @@ export function TradeCard({ item, onAdd, macroInfo, addedInfo, profiles }: {
<><span className="text-slate-700">·</span> <><span className="text-slate-700">·</span>
{matchedProfile {matchedProfile
? <span className="font-semibold" style={{ color: matchedProfile.color }}> {matchedProfile.name}</span> ? <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> </div>
@@ -435,7 +435,7 @@ export function TradeCard({ item, onAdd, macroInfo, addedInfo, profiles }: {
<button onClick={() => setExpanded(!expanded)} <button onClick={() => setExpanded(!expanded)}
className="flex items-center gap-1 text-xs text-slate-500 hover:text-slate-300 mb-1.5 transition-colors"> 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 ? <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> </button>
{expanded && ( {expanded && (
<div className="mb-2"> <div className="mb-2">
@@ -463,7 +463,7 @@ export function TradeCard({ item, onAdd, macroInfo, addedInfo, profiles }: {
{addedInfo && ( {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"> <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" /> <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> </div>
)} )}
<button onClick={() => onAdd(item)} <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' : 'text-blue-400 hover:text-white hover:bg-blue-600 border-blue-500/30 hover:border-blue-500'
)}> )}>
<Plus className="w-3 h-3" /> <Plus className="w-3 h-3" />
{addedInfo ? 'Ajouter à nouveau' : 'Ajouter au portefeuille'} {addedInfo ? 'Add again' : 'Add to portfolio'}
</button> </button>
</div> </div>
) )
@@ -560,7 +560,7 @@ export function TradeRow({ item, onAdd, macroInfo, addedInfo, profiles, rank }:
)} )}
</div> </div>
</div> </div>
) : <span className="text-slate-600 text-[10px]">à scorer</span>} ) : <span className="text-slate-600 text-[10px]">to score</span>}
</td> </td>
<td className="px-2 py-2 w-20"> <td className="px-2 py-2 w-20">
{effectiveConviction !== null && ( {effectiveConviction !== null && (
@@ -646,7 +646,7 @@ export function TradeRow({ item, onAdd, macroInfo, addedInfo, profiles, rank }:
{addedInfo && ( {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"> <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" /> <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> </div>
)} )}
<button <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' : 'text-blue-400 hover:text-white hover:bg-blue-600 border-blue-500/30 hover:border-blue-500'
)}> )}>
<Plus className="w-3 h-3" /> <Plus className="w-3 h-3" />
{addedInfo ? 'Ajouter à nouveau' : 'Ajouter au portefeuille'} {addedInfo ? 'Add again' : 'Add to portfolio'}
</button> </button>
</div> </div>
</div> </div>
@@ -826,7 +826,7 @@ export function TradeIdeasTab() {
}, { }, {
onSuccess: () => { onSuccess: () => {
refetchPositions() 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 justify-between gap-3 flex-wrap">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<h2 className="section-title flex items-center gap-1 mb-0"> <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> </h2>
{scoredAt && ( {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 && ( {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>
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
@@ -886,7 +886,7 @@ export function TradeIdeasTab() {
'bg-slate-600 text-white': topN === n, 'bg-slate-600 text-white': topN === n,
'text-slate-400 hover:text-slate-200': topN !== n, 'text-slate-400 hover:text-slate-200': topN !== n,
})}> })}>
{n === 0 ? 'Tous' : `Top ${n}`} {n === 0 ? 'All' : `Top ${n}`}
</button> </button>
))} ))}
</div> </div>
@@ -904,10 +904,10 @@ export function TradeIdeasTab() {
<button onClick={() => scorePatterns({})} <button onClick={() => scorePatterns({})}
disabled={scoring} 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"> 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> </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>
</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"> <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="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">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 text-right">Score</th>
<th className="px-2 py-2 w-20"></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">EV</th>
<th className="px-2 py-2 text-right">Gain</th> <th className="px-2 py-2 text-right">Move</th>
<th className="px-2 py-2 text-right">Max/Cible</th> <th className="px-2 py-2 text-right">Max/Target</th>
<th className="px-2 py-2">Profil</th> <th className="px-2 py-2">Profile</th>
<th className="px-2 py-2">Macro</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">Entry</th>
<th className="px-2 py-2 text-right">Durée</th> <th className="px-2 py-2 text-right">Duration</th>
<th className="pr-3 py-2 w-6"></th> <th className="pr-3 py-2 w-6"></th>
</tr> </tr>
</thead> </thead>
@@ -956,7 +956,7 @@ export function TradeIdeasTab() {
{topScored.length > 0 && ( {topScored.length > 0 && (
<div className="flex items-center gap-2 text-xs text-slate-600 mb-3"> <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> <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> <span className="border-b border-slate-700/30 flex-1"></span>
</div> </div>
)} )}
@@ -982,7 +982,7 @@ export function TradeIdeasTab() {
{allPatterns.length === 0 && ( {allPatterns.length === 0 && (
<div className="card text-center py-8 text-slate-500 text-sm"> <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> </div>
)} )}

View File

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

View File

@@ -8,8 +8,8 @@ function ReliabilityTable({ data }: { data: any[] }) {
return ( return (
<div className="card text-center py-10 text-slate-500"> <div className="card text-center py-10 text-slate-500">
<BarChart2 className="w-8 h-8 mx-auto mb-2 opacity-20" /> <BarChart2 className="w-8 h-8 mx-auto mb-2 opacity-20" />
<div>Aucune donnée de fiabilité</div> <div>No reliability data</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 className="text-xs mt-1">Data appears after 3+ mature trades (35% of horizon elapsed) per pattern</div>
</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-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">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">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 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">Max loss</th>
<th className="text-right py-2 px-2 font-medium">Score fiabilité</th> <th className="text-right py-2 px-2 font-medium">Reliability score</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-slate-800/50"> <tbody className="divide-y divide-slate-800/50">
@@ -69,8 +69,8 @@ function CalibrationSection({ data }: { data: any }) {
return ( return (
<div className="card text-center py-10 text-slate-500"> <div className="card text-center py-10 text-slate-500">
<Target className="w-8 h-8 mx-auto mb-2 opacity-20" /> <Target className="w-8 h-8 mx-auto mb-2 opacity-20" />
<div>Pas encore de données de calibration</div> <div>No calibration data yet</div>
<div className="text-xs mt-1">Nécessite des trades matures avec probabilité stockée vs résultat réalisé</div> <div className="text-xs mt-1">Requires mature trades with stored probability vs realized outcome</div>
</div> </div>
) )
} }
@@ -84,31 +84,31 @@ function CalibrationSection({ data }: { data: any }) {
<div className="card text-center"> <div className="card text-center">
<div className={clsx('text-2xl font-bold font-mono', brierColor)}>{brier_score?.toFixed(3) ?? '—'}</div> <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-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>
<div className="card text-center"> <div className="card text-center">
<div className="text-2xl font-bold text-white">{sample_size}</div> <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>
<div className="card text-center"> <div className="card text-center">
<div className={clsx('text-sm font-bold mt-1', brierColor)}>{interpretation ?? '—'}</div> <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>
</div> </div>
{/* Calibration buckets */} {/* Calibration buckets */}
{buckets && buckets.length > 0 && ( {buckets && buckets.length > 0 && (
<div> <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"> <div className="overflow-x-auto">
<table className="w-full text-xs"> <table className="w-full text-xs">
<thead> <thead>
<tr className="text-slate-500 border-b border-slate-700/30"> <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-left py-1.5 pr-3 font-medium">Predicted prob.</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">Actual rate</th>
<th className="text-right py-1.5 px-2 font-medium">Biais</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="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> </tr>
</thead> </thead>
<tbody className="divide-y divide-slate-800/50"> <tbody className="divide-y divide-slate-800/50">
@@ -142,7 +142,7 @@ function CalibrationSection({ data }: { data: any }) {
</table> </table>
</div> </div>
<div className="text-[10px] text-slate-600 mt-2"> <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>
</div> </div>
)} )}
@@ -166,7 +166,7 @@ export default function Analytics() {
<BarChart2 className="w-5 h-5 text-blue-400" /> Analytics & Calibration <BarChart2 className="w-5 h-5 text-blue-400" /> Analytics & Calibration
</h1> </h1>
<p className="text-xs text-slate-500 mt-0.5"> <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> </p>
</div> </div>
@@ -175,18 +175,18 @@ export default function Analytics() {
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4"> <div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
<div className="card"> <div className="card">
<div className="text-2xl font-bold text-white font-mono">{reliability.length}</div> <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>
<div className="card"> <div className="card">
<div className="text-2xl font-bold text-white font-mono"> <div className="text-2xl font-bold text-white font-mono">
{reliability.reduce((s: number, r: any) => s + r.trade_count, 0)} {reliability.reduce((s: number, r: any) => s + r.trade_count, 0)}
</div> </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> </div>
{topPatterns[0] && ( {topPatterns[0] && (
<div className="card border-emerald-700/30"> <div className="card border-emerald-700/30">
<div className="text-xs text-slate-500 mb-1 flex items-center gap-1"> <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>
<div className="text-sm font-semibold text-emerald-400 truncate">{topPatterns[0].pattern_name}</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> <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] && ( {bottomPatterns[0] && (
<div className="card border-red-700/30"> <div className="card border-red-700/30">
<div className="text-xs text-slate-500 mb-1 flex items-center gap-1"> <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>
<div className="text-sm font-semibold text-red-400 truncate">{bottomPatterns[0].pattern_name}</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> <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 */} {/* Reliability table */}
<div className="card"> <div className="card">
<div className="text-sm font-semibold text-white mb-3 flex items-center gap-2"> <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 <BarChart2 className="w-4 h-4 text-blue-400" /> Reliability by pattern
<span className="text-xs text-slate-500 font-normal">(trades ≥35% de l'horizon uniquement)</span> <span className="text-xs text-slate-500 font-normal">(trades 35% of horizon only)</span>
</div> </div>
{loadingR ? ( {loadingR ? (
<div className="space-y-2">{[1,2,3].map(i => <div key={i} className="h-8 bg-dark-700 animate-pulse rounded" />)}</div> <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="card">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<div className="text-sm font-semibold text-white flex items-center gap-2"> <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> </div>
<select <select
value={calDays} value={calDays}
onChange={e => setCalDays(Number(e.target.value))} 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" 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={90}>90 days</option>
<option value={180}>180 jours</option> <option value={180}>180 days</option>
<option value={365}>1 an</option> <option value={365}>1 year</option>
<option value={730}>2 ans</option> <option value={730}>2 years</option>
</select> </select>
</div> </div>
{loadingC ? ( {loadingC ? (

View File

@@ -11,8 +11,8 @@ function BayesianTable({ data }: { data: any[] }) {
return ( return (
<div className="text-center py-10 text-slate-500"> <div className="text-center py-10 text-slate-500">
<Brain className="w-8 h-8 mx-auto mb-2 opacity-20" /> <Brain className="w-8 h-8 mx-auto mb-2 opacity-20" />
<div>Pas encore de données bayésiennes</div> <div>No Bayesian data yet</div>
<div className="text-xs mt-1">Les posteriors se calculent après des trades matures (35% de l'horizon)</div> <div className="text-xs mt-1">Posteriors are computed after mature trades (35% of the horizon)</div>
</div> </div>
) )
} }
@@ -29,11 +29,11 @@ function BayesianTable({ data }: { data: any[] }) {
<tr className="text-slate-500 border-b border-slate-700/30"> <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-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">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">Bayesian WR</th>
<th className="text-right py-2 px-2 font-medium">IC 95%</th> <th className="text-right py-2 px-2 font-medium">CI 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">Mature trades</th>
<th className="text-right py-2 px-2 font-medium">Dérive</th> <th className="text-right py-2 px-2 font-medium">Drift</th>
<th className="text-center py-2 px-2 font-medium">Confiance</th> <th className="text-center py-2 px-2 font-medium">Confidence</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-slate-800/50"> <tbody className="divide-y divide-slate-800/50">
@@ -78,7 +78,7 @@ function BayesianTable({ data }: { data: any[] }) {
)} )}
{withoutData.length > 0 && ( {withoutData.length > 0 && (
<div className="text-xs text-slate-600 mt-1"> <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>
)} )}
</div> </div>
@@ -92,8 +92,8 @@ function TransitionMatrix({ data }: { data: any }) {
return ( return (
<div className="text-center py-8 text-slate-500"> <div className="text-center py-8 text-slate-500">
<GitBranch className="w-8 h-8 mx-auto mb-2 opacity-20" /> <GitBranch className="w-8 h-8 mx-auto mb-2 opacity-20" />
<div>Pas encore de transitions détectées</div> <div>No transitions detected yet</div>
<div className="text-xs mt-1">Les clusters se remplissent au fil des cycles</div> <div className="text-xs mt-1">Clusters fill up as cycles run</div>
</div> </div>
) )
} }
@@ -104,13 +104,13 @@ function TransitionMatrix({ data }: { data: any }) {
return ( return (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<div className="text-xs text-slate-500 mb-2"> <div className="text-xs text-slate-500 mb-2">
Probabilité de transition d'un régime à l'autre (lignes = source, colonnes = destination) Transition probability from one regime to another (rows = source, columns = destination)
· {data.n_transitions} transitions totales · {data.n_transitions} total transitions
</div> </div>
<table className="text-xs border-collapse"> <table className="text-xs border-collapse">
<thead> <thead>
<tr> <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 => ( {ids.map(dst => (
<th key={dst} className="p-2 font-medium text-slate-300 text-center min-w-[90px]"> <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> <div className="text-[10px] text-slate-500">C{dst}</div>
@@ -145,7 +145,7 @@ function TransitionMatrix({ data }: { data: any }) {
</tbody> </tbody>
</table> </table>
<div className="text-[10px] text-slate-600 mt-2"> <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>
</div> </div>
) )
@@ -165,7 +165,7 @@ function ClusterTimeline({ data }: { data: any[] }) {
if (!data || data.length === 0) { if (!data || data.length === 0) {
return ( return (
<div className="text-center py-6 text-slate-500 text-xs"> <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> </div>
) )
} }
@@ -187,7 +187,7 @@ function ClusterTimeline({ data }: { data: any[] }) {
{recent.map((r: any, i: number) => ( {recent.map((r: any, i: number) => (
<div <div
key={i} 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( className={clsx(
'w-5 h-5 rounded cursor-default', 'w-5 h-5 rounded cursor-default',
CLUSTER_COLORS[r.cluster_id] ?? 'bg-slate-600', CLUSTER_COLORS[r.cluster_id] ?? 'bg-slate-600',
@@ -197,20 +197,20 @@ function ClusterTimeline({ data }: { data: any[] }) {
))} ))}
</div> </div>
<div className="text-[10px] text-slate-600"> <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> </div>
{/* Latest cluster detail */} {/* Latest cluster detail */}
{data[0] && ( {data[0] && (
<div className="mt-3 p-3 rounded bg-dark-700/60 border border-slate-700/30 text-xs"> <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"> <div className="flex items-center gap-2">
<span className={clsx('w-3 h-3 rounded-full', CLUSTER_COLORS[data[0].cluster_id])} /> <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-white font-semibold">{data[0].cluster_label}</span>
<span className="text-slate-500">({data[0].dominant_regime})</span> <span className="text-slate-500">({data[0].dominant_regime})</span>
{data[0].anomaly_flag ? ( {data[0].anomaly_flag ? (
<span className="flex items-center gap-1 text-amber-400 font-bold"> <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> </span>
) : null} ) : null}
</div> </div>
@@ -228,21 +228,21 @@ function EmbeddingsSummary({ data }: { data: any[] }) {
return ( return (
<div className="text-center py-6 text-slate-500 text-xs"> <div className="text-center py-6 text-slate-500 text-xs">
<Cpu className="w-6 h-6 mx-auto mb-1 opacity-20" /> <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> </div>
) )
} }
return ( return (
<div className="space-y-2"> <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"> <div className="overflow-x-auto">
<table className="w-full text-xs"> <table className="w-full text-xs">
<thead> <thead>
<tr className="text-slate-500 border-b border-slate-700/30"> <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 pr-3 font-medium">Pattern</th>
<th className="text-left py-1.5 px-2 font-medium">Classe</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">Modèle</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">Mis à jour</th> <th className="text-right py-1.5 px-2 font-medium">Updated</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-slate-800/30"> <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 className="flex items-start justify-between gap-4">
<div> <div>
<h1 className="text-xl font-bold text-white flex items-center gap-2"> <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> </h1>
<p className="text-xs text-slate-500 mt-0.5"> <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> </p>
</div> </div>
<div className="flex gap-2"> <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" 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')} /> <RefreshCw className={clsx('w-3 h-3', regimeDetect.isPending && 'animate-spin')} />
tecter régime Detect regime
</button> </button>
</div> </div>
</div> </div>
@@ -333,25 +333,25 @@ export default function AnalyticsAdvanced() {
<div className="text-2xl font-bold text-purple-400 font-mono"> <div className="text-2xl font-bold text-purple-400 font-mono">
{patternsWithData.length} {patternsWithData.length}
</div> </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>
<div className="card"> <div className="card">
<div className="text-2xl font-bold text-white font-mono"> <div className="text-2xl font-bold text-white font-mono">
{avgBayesWR !== null ? `${avgBayesWR}%` : '—'} {avgBayesWR !== null ? `${avgBayesWR}%` : '—'}
</div> </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>
<div className="card"> <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')}> <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}` : '—'} {latestCluster ? `C${latestCluster.cluster_id}` : '—'}
</div> </div>
<div className="text-xs text-slate-500 mt-1"> <div className="text-xs text-slate-500 mt-1">
{latestCluster?.cluster_label ?? 'Aucun cluster'} {latestCluster?.cluster_label ?? 'No cluster'}
</div> </div>
</div> </div>
<div className="card"> <div className="card">
<div className="text-2xl font-bold text-blue-400 font-mono">{embeddings.length}</div> <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>
</div> </div>
@@ -360,8 +360,8 @@ export default function AnalyticsAdvanced() {
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<div className="text-sm font-semibold text-white flex items-center gap-2"> <div className="text-sm font-semibold text-white flex items-center gap-2">
<Brain className="w-4 h-4 text-purple-400" /> <Brain className="w-4 h-4 text-purple-400" />
Posteriors bayésiens par pattern Bayesian posteriors by pattern
<span className="text-xs text-slate-500 font-normal">Beta(α,β) · IC 95%</span> <span className="text-xs text-slate-500 font-normal">Beta(α,β) · CI 95%</span>
</div> </div>
{bayesUpdate.data && ( {bayesUpdate.data && (
<span className="text-xs text-emerald-400">{bayesUpdate.data.message}</span> <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"> <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 · <strong className="text-slate-400">Reading:</strong> GPT Prior = probability stated by GPT-4o ·
WR Bayésien = win rate observé avec lissage Laplace (β a priori faible) · Bayesian WR = observed win rate with Laplace smoothing (weak β prior) ·
Dérive = écart posterior prior · IC 95% basé sur la distribution Beta Drift = posterior prior gap · CI 95% based on Beta distribution
</div> </div>
</div> </div>
@@ -387,16 +387,16 @@ export default function AnalyticsAdvanced() {
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<div className="text-sm font-semibold text-white flex items-center gap-2"> <div className="text-sm font-semibold text-white flex items-center gap-2">
<Layers className="w-4 h-4 text-blue-400" /> <Layers className="w-4 h-4 text-blue-400" />
Historique des clusters Cluster history
</div> </div>
<select <select
value={regimeDays} value={regimeDays}
onChange={e => setRegimeDays(Number(e.target.value))} 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" 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={30}>30 days</option>
<option value={90}>90 jours</option> <option value={90}>90 days</option>
<option value={180}>180 jours</option> <option value={180}>180 days</option>
</select> </select>
</div> </div>
{loadingClusters ? ( {loadingClusters ? (
@@ -410,8 +410,8 @@ export default function AnalyticsAdvanced() {
<div className="card"> <div className="card">
<div className="text-sm font-semibold text-white flex items-center gap-2 mb-3"> <div className="text-sm font-semibold text-white flex items-center gap-2 mb-3">
<GitBranch className="w-4 h-4 text-blue-400" /> <GitBranch className="w-4 h-4 text-blue-400" />
Matrice de transition Transition matrix
<span className="text-xs text-slate-500 font-normal">P(régime j | régime i)</span> <span className="text-xs text-slate-500 font-normal">P(regime j | regime i)</span>
</div> </div>
{loadingTrans ? ( {loadingTrans ? (
<div className="h-32 bg-dark-700 animate-pulse rounded" /> <div className="h-32 bg-dark-700 animate-pulse rounded" />
@@ -425,9 +425,9 @@ export default function AnalyticsAdvanced() {
<div className="card"> <div className="card">
<div className="text-sm font-semibold text-white flex items-center gap-2 mb-3"> <div className="text-sm font-semibold text-white flex items-center gap-2 mb-3">
<Cpu className="w-4 h-4 text-emerald-400" /> <Cpu className="w-4 h-4 text-emerald-400" />
Embeddings sémantiques Semantic embeddings
<span className="text-xs text-slate-500 font-normal"> <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> </span>
</div> </div>
{loadingEmb ? ( {loadingEmb ? (
@@ -436,9 +436,9 @@ export default function AnalyticsAdvanced() {
<EmbeddingsSummary data={embeddings} /> <EmbeddingsSummary data={embeddings} />
)} )}
<div className="mt-3 p-3 bg-dark-700/40 rounded text-xs text-slate-500 border border-slate-700/20"> <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é <strong className="text-slate-400">How it works:</strong> Each newly suggested pattern is vectorized and compared
aux patterns existants via similarité cosinus (seuil : 0.75). Si la similarité est &gt; 0.75, to existing patterns via cosine similarity (threshold: 0.75). If similarity is &gt; 0.75,
le pattern est reje comme doublon sémantique plus précis que Jaccard qui ne voit que les mots communs. the pattern is rejected as a semantic duplicate more precise than Jaccard which only sees common words.
</div> </div>
</div> </div>
</div> </div>

View File

@@ -63,7 +63,7 @@ export default function Backtest() {
<History className="w-5 h-5 text-blue-400" /> Backtest & Simulation <History className="w-5 h-5 text-blue-400" /> Backtest & Simulation
</h1> </h1>
<p className="text-xs text-slate-500 mt-0.5"> <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> </p>
</div> </div>
@@ -74,7 +74,7 @@ export default function Backtest() {
<div className="section-title">Configuration</div> <div className="section-title">Configuration</div>
<div className="space-y-3"> <div className="space-y-3">
<div> <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"> <div className="flex flex-wrap gap-1 mb-1">
{SYMBOLS.slice(0, 7).map(s => ( {SYMBOLS.slice(0, 7).map(s => (
<button <button
@@ -98,7 +98,7 @@ export default function Backtest() {
</div> </div>
<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 => ( {STRATEGIES.map(s => (
<button <button
key={s.key} key={s.key}
@@ -114,7 +114,7 @@ export default function Backtest() {
</div> </div>
<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 <input
type="date" type="date"
value={form.start_date} value={form.start_date}
@@ -142,7 +142,7 @@ export default function Backtest() {
</div> </div>
<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"> <div className="flex gap-1 mb-1">
{[30, 60, 90, 180].map(d => ( {[30, 60, 90, 180].map(d => (
<button <button
@@ -153,14 +153,14 @@ export default function Backtest() {
'border-slate-700 text-slate-500': form.expiry_days !== d, 'border-slate-700 text-slate-500': form.expiry_days !== d,
})} })}
> >
{d}j {d}d
</button> </button>
))} ))}
</div> </div>
</div> </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 <input
type="number" type="number"
value={form.capital} 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" 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" /> <Play className="w-4 h-4" />
{isPending ? 'Calcul...' : 'Lancer le backtest'} {isPending ? 'Computing...' : 'Run backtest'}
</button> </button>
</div> </div>
</div> </div>
@@ -196,12 +196,12 @@ export default function Backtest() {
{/* KPIs */} {/* KPIs */}
<div className="grid grid-cols-4 gap-3"> <div className="grid grid-cols-4 gap-3">
<StatCard <StatCard
label="Retour total" label="Total return"
value={`${typed.total_return_pct >= 0 ? '+' : ''}${typed.total_return_pct.toFixed(2)}%`} value={`${typed.total_return_pct >= 0 ? '+' : ''}${typed.total_return_pct.toFixed(2)}%`}
positive={typed.total_return_pct >= 0} positive={typed.total_return_pct >= 0}
/> />
<StatCard <StatCard
label="Taux de succès" label="Win rate"
value={`${typed.win_rate.toFixed(1)}%`} value={`${typed.win_rate.toFixed(1)}%`}
sub={`${typed.wins}W / ${typed.losses}L`} sub={`${typed.wins}W / ${typed.losses}L`}
positive={typed.win_rate >= 50} positive={typed.win_rate >= 50}
@@ -221,9 +221,9 @@ export default function Backtest() {
{/* Equity curve */} {/* Equity curve */}
<div className="card"> <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"> <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)}) {' '}(initial: {form.capital} · P&L: {typed.total_pnl >= 0 ? '+' : ''}{typed.total_pnl.toFixed(2)})
</div> </div>
<ResponsiveContainer width="100%" height={220}> <ResponsiveContainer width="100%" height={220}>
@@ -254,17 +254,17 @@ export default function Backtest() {
{/* Last trades */} {/* Last trades */}
<div className="card"> <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"> <div className="overflow-x-auto">
<table className="w-full text-xs"> <table className="w-full text-xs">
<thead> <thead>
<tr className="text-slate-600 border-b border-slate-700/40"> <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">Entry</th>
<th className="text-left pb-2">Sortie</th> <th className="text-left pb-2">Exit</th>
<th className="text-right pb-2">Prix entrée</th> <th className="text-right pb-2">Entry price</th>
<th className="text-right pb-2">Strike</th> <th className="text-right pb-2">Strike</th>
<th className="text-right pb-2">Prime</th> <th className="text-right pb-2">Premium</th>
<th className="text-right pb-2">Prix sortie</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">P&L</th>
<th className="text-right pb-2">Capital</th> <th className="text-right pb-2">Capital</th>
</tr> </tr>
@@ -298,8 +298,8 @@ export default function Backtest() {
<div className="card h-80 flex items-center justify-center text-slate-600"> <div className="card h-80 flex items-center justify-center text-slate-600">
<div className="text-center"> <div className="text-center">
<History className="w-10 h-10 mx-auto mb-3 opacity-20" /> <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-sm">Configure and run a backtest</div>
<div className="text-xs mt-1">Données yfinance historique complet disponible</div> <div className="text-xs mt-1">yfinance data full history available</div>
</div> </div>
</div> </div>
)} )}

View File

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

View File

@@ -7,39 +7,39 @@ import clsx from 'clsx'
const API = '' const API = ''
const SOURCE_CATEGORIES = { const SOURCE_CATEGORIES = {
'Flux RSS actifs': ['reuters_world', 'reuters_business', 'reuters_energy', 'ap_top', 'aljazeera', 'ft', 'bloomberg'], 'Active RSS Feeds': ['reuters_world', 'reuters_business', 'reuters_energy', 'ap_top', 'aljazeera', 'ft', 'bloomberg'],
'Données économiques': ['newsapi', 'gdelt', 'eia', 'fred', 'usda'], 'Economic Data': ['newsapi', 'gdelt', 'eia', 'fred', 'usda'],
'Santé & Catastrophes': ['who', 'emdat'], 'Health & Disasters': ['who', 'emdat'],
'Réseaux sociaux': ['twitter_trump'], 'Social Media': ['twitter_trump'],
} }
const SOURCE_DOCS: Record<string, { description: string; link?: string; cost: string }> = { const SOURCE_DOCS: Record<string, { description: string; link?: string; cost: string }> = {
reuters_world: { description: 'Actualités mondiales Reuters', cost: 'Gratuit' }, reuters_world: { description: 'Reuters World News', cost: 'Free' },
reuters_business: { description: 'Business et marchés Reuters', cost: 'Gratuit' }, reuters_business: { description: 'Reuters Business & Markets', cost: 'Free' },
reuters_energy: { description: 'Énergie et commodités Reuters', cost: 'Gratuit' }, reuters_energy: { description: 'Reuters Energy & Commodities', cost: 'Free' },
ap_top: { description: 'Associated Press — Top Stories', cost: 'Gratuit' }, ap_top: { description: 'Associated Press — Top Stories', cost: 'Free' },
aljazeera: { description: 'Al Jazeera — couverture Moyen-Orient/Monde', cost: 'Gratuit' }, aljazeera: { description: 'Al Jazeera — Middle East/World coverage', cost: 'Free' },
ft: { description: 'Financial Times — finance internationale', cost: 'Gratuit' }, ft: { description: 'Financial Times — international finance', cost: 'Free' },
bloomberg: { description: 'Bloomberg Markets RSS', cost: 'Gratuit' }, bloomberg: { description: 'Bloomberg Markets RSS', cost: 'Free' },
newsapi: { description: '100 req/jour gratuit — actualités mondiales multi-sources', link: 'https://newsapi.org', cost: '100 req/j gratuit' }, newsapi: { description: '100 req/day free — multi-source world news', link: 'https://newsapi.org', cost: '100 req/d free' },
gdelt: { description: 'Base de données géopolitique mondiale — 300K events/jour', link: 'https://gdeltproject.org', cost: 'Gratuit total' }, gdelt: { description: 'Global geopolitical database — 300K events/day', link: 'https://gdeltproject.org', cost: 'Totally free' },
eia: { description: 'US Energy Information Administration — données pétrole/gaz hebdo', link: 'https://www.eia.gov/opendata', cost: 'Gratuit avec clé' }, 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 — macro US (PIB, inflation, emploi)', link: 'https://fred.stlouisfed.org/docs/api/fred/', cost: 'Gratuit avec clé' }, 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 — rapports agricoles officiels US', cost: 'Gratuit' }, usda: { description: 'USDA — official US agricultural reports', cost: 'Free' },
who: { description: 'OMS — alertes sanitaires mondiales RSS', cost: 'Gratuit' }, who: { description: 'WHO — global health alerts RSS', cost: 'Free' },
emdat: { description: 'EM-DAT — base de données catastrophes naturelles', link: 'https://www.emdat.be', cost: 'Inscription gratuite' }, emdat: { description: 'EM-DAT — natural disasters database', link: 'https://www.emdat.be', cost: 'Free registration' },
twitter_trump: { description: 'Flux X/Twitter Trump — discours, annonces tarifs', cost: 'API payante ($100/mois+)' }, twitter_trump: { description: 'X/Twitter Trump feed — speeches, tariff announcements', cost: 'Paid API ($100/mo+)' },
} }
// ── Risk Profiles Component ─────────────────────────────────────────────────── // ── Risk Profiles Component ───────────────────────────────────────────────────
const PROFILE_COLORS = [ const PROFILE_COLORS = [
{ value: '#22c55e', label: 'Vert' }, { value: '#22c55e', label: 'Green' },
{ value: '#3b82f6', label: 'Bleu' }, { value: '#3b82f6', label: 'Blue' },
{ value: '#f97316', label: 'Orange' }, { value: '#f97316', label: 'Orange' },
{ value: '#ef4444', label: 'Rouge' }, { value: '#ef4444', label: 'Red' },
{ value: '#8b5cf6', label: 'Violet' }, { value: '#8b5cf6', label: 'Purple' },
{ value: '#eab308', label: 'Jaune' }, { value: '#eab308', label: 'Yellow' },
] ]
function NextRunCountdown({ nextRunAt }: { nextRunAt: string }) { function NextRunCountdown({ nextRunAt }: { nextRunAt: string }) {
@@ -64,16 +64,16 @@ function NextRunCountdown({ nextRunAt }: { nextRunAt: string }) {
return ( return (
<div className="flex items-center gap-2 text-emerald-400/80"> <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 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>Next cycle in <span className="font-mono font-semibold text-emerald-300">{remaining}</span></span>
<span className="text-slate-600">({absTime} heure locale)</span> <span className="text-slate-600">({absTime} local time)</span>
</div> </div>
) )
} }
function evNetLabel(evNet: number): { text: string; cls: string } { 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.05) return { text: `Net EV +${(evNet * 100).toFixed(0)}%`, cls: 'text-emerald-400' }
if (evNet >= -0.01) return { text: 'EV nette ≈ 0', cls: 'text-yellow-400' } if (evNet >= -0.01) return { text: 'Net EV ≈ 0', cls: 'text-yellow-400' }
return { text: `EV nette ${(evNet * 100).toFixed(0)}%`, cls: 'text-slate-600' } return { text: `Net EV ${(evNet * 100).toFixed(0)}%`, cls: 'text-slate-600' }
} }
function ProfileRow({ function ProfileRow({
@@ -113,7 +113,7 @@ function ProfileRow({
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap"> <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> <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">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="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> <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="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 className="grid grid-cols-3 gap-3">
<div> <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)} <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" /> className="w-full bg-dark-800 border border-slate-700 rounded px-2 py-1 text-sm text-white" />
</div> </div>
@@ -158,7 +158,7 @@ function ProfileRow({
{/* Live EV preview */} {/* Live EV preview */}
<div className="bg-dark-800 rounded px-3 py-2 text-xs flex items-center gap-4 flex-wrap"> <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={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">Trade Score = <span className="text-slate-400">{trade_score.toFixed(1)}</span>/100</span>
<span className="text-slate-600"> <span className="text-slate-600">
@@ -179,14 +179,14 @@ function ProfileRow({
<button onClick={() => setEnabled(!enabled)} <button onClick={() => setEnabled(!enabled)}
className={clsx('text-xs px-2 py-0.5 rounded border transition-all', 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 ? '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> </button>
<div className="ml-auto flex gap-2"> <div className="ml-auto flex gap-2">
<button onClick={() => setEditing(false)} <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} <button onClick={save}
className="text-xs bg-blue-600 hover:bg-blue-500 text-white px-3 py-1 rounded font-semibold"> className="text-xs bg-blue-600 hover:bg-blue-500 text-white px-3 py-1 rounded font-semibold">
Sauvegarder Save
</button> </button>
</div> </div>
</div> </div>
@@ -227,22 +227,22 @@ function RiskProfilesCard() {
<div className="card bg-dark-700/20 mb-4"> <div className="card bg-dark-700/20 mb-4">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<div> <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"> <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é A trade is logged in the journal if it passes <span className="text-slate-500">at least one</span> enabled profile
· Formule : EV nette = (score/100 × gain/100) (1 score/100) · Formula: Net EV = (score/100 × gain/100) (1 score/100)
</div> </div>
</div> </div>
<button onClick={() => setShowNew(true)} <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"> 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> </button>
</div> </div>
{isLoading ? ( {isLoading ? (
<div className="h-16 animate-pulse bg-dark-700 rounded" /> <div className="h-16 animate-pulse bg-dark-700 rounded" />
) : profiles.length === 0 ? ( ) : 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"> <div className="space-y-1">
{profiles.map((p: any) => ( {profiles.map((p: any) => (
@@ -257,13 +257,13 @@ function RiskProfilesCard() {
{showNew && ( {showNew && (
<div className="bg-dark-700/60 rounded-lg p-3 border border-blue-700/30 space-y-3 mt-3"> <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"> <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> <button onClick={() => setShowNew(false)} className="text-slate-600 hover:text-slate-400"><X className="w-3.5 h-3.5" /></button>
</div> </div>
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-3 gap-3">
<div> <div>
<label className="text-[10px] text-slate-500 mb-1 block">Nom</label> <label className="text-[10px] text-slate-500 mb-1 block">Name</label>
<input value={newName} onChange={e => setNewName(e.target.value)} placeholder="ex: Ultra-risqué" <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" /> 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>
<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"> <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={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-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>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@@ -295,10 +295,10 @@ function RiskProfilesCard() {
))} ))}
</div> </div>
<div className="ml-auto flex gap-2"> <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()} <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"> 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> </button>
</div> </div>
</div> </div>
@@ -308,7 +308,7 @@ function RiskProfilesCard() {
{/* Frontier visualization */} {/* Frontier visualization */}
{profiles.length > 0 && ( {profiles.length > 0 && (
<div className="mt-3 pt-3 border-t border-slate-700/30"> <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"> <div className="flex flex-wrap gap-2">
{profiles.filter((p: any) => p.enabled).map((p: any) => { {profiles.filter((p: any) => p.enabled).map((p: any) => {
const { text: ev, cls } = evNetLabel(p.ev_net_at_frontier ?? 0) 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`, { mutationFn: (cfg: any) => fetch(`${API}/api/var/scheduler/config`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(cfg), method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(cfg),
}).then(r => r.json()), }).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({ const { mutate: triggerVarNow, isPending: triggeringVar } = useMutation({
mutationFn: () => fetch(`${API}/api/var/run-now`, { method: 'POST' }).then(r => r.json()), 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({ const { mutate: triggerPnlNow, isPending: triggeringPnl } = useMutation({
mutationFn: () => fetch(`${API}/api/var/pnl/run-now`, { method: 'POST' }).then(r => r.json()), 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 // Auto-cycle local state
@@ -486,7 +486,7 @@ export default function Config() {
const saveSources = () => { const saveSources = () => {
updateSources(displaySources, { 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 if (fredKey) keys.fred_api_key = fredKey
updateApiKeys(keys, { updateApiKeys(keys, {
onSuccess: () => { onSuccess: () => {
setSavedMsg('Clés API sauvegardées') setSavedMsg('API keys saved')
setTimeout(() => setSavedMsg(''), 2000) setTimeout(() => setSavedMsg(''), 2000)
setOpenaiKey(''); setNewsapiKey(''); setEiaKey(''); setFredKey('') setOpenaiKey(''); setNewsapiKey(''); setEiaKey(''); setFredKey('')
} }
@@ -506,11 +506,11 @@ export default function Config() {
} }
const CONFIG_TABS = [ 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: 'cycle', label: 'Auto-Cycle & Logging', icon: Zap },
{ key: 'sources', label: 'Sources', icon: Globe }, { key: 'sources', label: 'Sources', icon: Globe },
{ key: 'options', label: 'Options — Paramètres', icon: TrendingUp }, { key: 'options', label: 'Options — Settings', icon: TrendingUp },
{ key: 'profiles', label: 'Profils de risque', icon: SlidersHorizontal }, { key: 'profiles', label: 'Risk Profiles', icon: SlidersHorizontal },
] as const ] as const
return ( return (
@@ -520,7 +520,7 @@ export default function Config() {
<h1 className="text-xl font-bold text-white flex items-center gap-2"> <h1 className="text-xl font-bold text-white flex items-center gap-2">
<Settings className="w-5 h-5 text-blue-400" /> Configuration <Settings className="w-5 h-5 text-blue-400" /> Configuration
</h1> </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> </div>
{savedMsg && ( {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"> <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"> <div className="space-y-4">
{/* AI Status */} {/* AI Status */}
<div className={clsx('card', aiStatus?.enabled ? 'border-emerald-700/40' : 'border-slate-700/40')}> <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"> <div className="flex items-center gap-3 mb-3">
{aiStatus?.enabled ? ( {aiStatus?.enabled ? (
<><CheckCircle className="w-5 h-5 text-emerald-400" /> <><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></> <div className="text-xs text-slate-500">GPT-4o + GPT-4o-mini</div></div></>
) : ( ) : (
<><XCircle className="w-5 h-5 text-red-400" /> <><XCircle className="w-5 h-5 text-red-400" />
<div><div className="text-sm text-red-400 font-semibold">OpenAI Non configuré</div> <div><div className="text-sm text-red-400 font-semibold">OpenAI Not configured</div>
<div className="text-xs text-slate-500">Entrer la clé ci-dessous</div></div></> <div className="text-xs text-slate-500">Enter key below</div></div></>
)} )}
</div> </div>
<div className="text-xs text-slate-600 space-y-1"> <div className="text-xs text-slate-600 space-y-1">
<div> Analyse de discours (Trump, Powell...)</div> <div> Speech analysis (Trump, Powell...)</div>
<div> Classification IA des actualités</div> <div> AI news classification</div>
<div> Évaluation de patterns</div> <div> Pattern evaluation</div>
<div> Top 10 idées contextualisées</div> <div> Top 10 contextualized ideas</div>
</div> </div>
</div> </div>
{/* OpenAI API Key */} {/* OpenAI API Key */}
<div className="card"> <div className="card">
<div className="flex items-center justify-between mb-3"> <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"> <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" />} {showKeys ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
</button> </button>
@@ -582,7 +582,7 @@ export default function Config() {
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<label className="text-xs text-slate-500">OpenAI API Key</label> <label className="text-xs text-slate-500">OpenAI API Key</label>
<span className={clsx('text-xs', aiStatus?.enabled ? 'text-emerald-400' : 'text-slate-600')}> <span className={clsx('text-xs', aiStatus?.enabled ? 'text-emerald-400' : 'text-slate-600')}>
{aiStatus?.enabled ? '✓ Actif' : '○ Inactif'} {aiStatus?.enabled ? '✓ Active' : '○ Inactive'}
</span> </span>
</div> </div>
<input <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" 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" /> <Save className="w-3.5 h-3.5" />
{savingKeys ? 'Sauvegarde...' : 'Sauvegarder la clé'} {savingKeys ? 'Saving...' : 'Save key'}
</button> </button>
</div> </div>
</div> </div>
@@ -608,11 +608,11 @@ export default function Config() {
{/* Right: Analysis params */} {/* Right: Analysis params */}
<div className="card"> <div className="card">
<h2 className="text-base font-bold text-white flex items-center gap-2 mb-4"> <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> </h2>
<div className="grid grid-cols-2 gap-4 mb-4"> <div className="grid grid-cols-2 gap-4 mb-4">
<div> <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"> <div className="flex gap-1">
{[5, 10, 15, 20].map(n => ( {[5, 10, 15, 20].map(n => (
<button key={n} onClick={() => setAnalysisTopN(n)} <button key={n} onClick={() => setAnalysisTopN(n)}
@@ -626,46 +626,46 @@ export default function Config() {
</div> </div>
</div> </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)} <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"> 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="all">All categories</option>
<option value="energy">Énergie</option> <option value="energy">Energy</option>
<option value="metals">Métaux</option> <option value="metals">Metals</option>
<option value="agriculture">Agriculture</option> <option value="agriculture">Agriculture</option>
<option value="indices">Indices</option> <option value="indices">Indices</option>
<option value="equities">Actions</option> <option value="equities">Equities</option>
<option value="forex">Forex</option> <option value="forex">Forex</option>
</select> </select>
</div> </div>
</div> </div>
<div className="mb-4"> <div className="mb-4">
<div className="flex items-center justify-between mb-1"> <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"> <button onClick={() => setAnalysisTemplate('')} className="text-xs text-slate-600 hover:text-slate-400">
Par faut Default
</button> </button>
</div> </div>
<textarea <textarea
value={analysisTemplate} value={analysisTemplate}
onChange={e => setAnalysisTemplate(e.target.value)} onChange={e => setAnalysisTemplate(e.target.value)}
rows={8} 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" 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"> <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>
</div> </div>
<button <button
onClick={() => saveAnalysis( onClick={() => saveAnalysis(
{ top_n: analysisTopN, category_filter: analysisCategoryDefault, template: analysisTemplate || undefined }, { 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} 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"> 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" /> <Save className="w-4 h-4" />
{savingAnalysis ? 'Sauvegarde...' : 'Sauvegarder'} {savingAnalysis ? 'Saving...' : 'Save'}
</button> </button>
</div> </div>
</div> </div>
@@ -677,11 +677,11 @@ export default function Config() {
{/* Score min threshold — prominent */} {/* Score min threshold — prominent */}
<div className="card border-blue-700/40 bg-blue-900/5"> <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"> <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> </h2>
<p className="text-xs text-slate-500 mb-4"> <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. Cycle pre-filter patterns with a score below this are not submitted for analysis.
Les profils de risque déterminent ensuite quels trades sont loggés. Risk profiles then determine which trades get logged.
</p> </p>
<div className="grid grid-cols-2 gap-6"> <div className="grid grid-cols-2 gap-6">
<div> <div>
@@ -694,12 +694,12 @@ export default function Config() {
onChange={e => setMinScore(parseInt(e.target.value))} onChange={e => setMinScore(parseInt(e.target.value))}
className="w-full accent-blue-500" /> className="w-full accent-blue-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1"> <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>
<div className="text-xs text-slate-500 mt-2"> <div className="text-xs text-slate-500 mt-2">
{minScore === 0 {minScore === 0
? 'Tous les patterns analysés par le cycle' ? 'All patterns processed by the cycle'
: `Seuls les patterns avec score ≥ ${minScore} sont soumis au cycle`} : `Only patterns with score ≥ ${minScore} are submitted to the cycle`}
</div> </div>
</div> </div>
<div> <div>
@@ -716,10 +716,10 @@ export default function Config() {
</div> </div>
<div className="text-xs text-slate-500 mt-2"> <div className="text-xs text-slate-500 mt-2">
{minEv === 0 {minEv === 0
? 'Toute EV nette acceptée' ? 'Any net EV accepted'
: minEv > 0 : minEv > 0
? `EV nette ≥ +${(minEv * 100).toFixed(0)}% requise` ? `Net EV ≥ +${(minEv * 100).toFixed(0)}% required`
: `Filtre désactivé (EV < ${(minEv * 100).toFixed(0)}%)`} : `Filter disabled (EV < ${(minEv * 100).toFixed(0)}%)`}
</div> </div>
</div> </div>
</div> </div>
@@ -735,38 +735,38 @@ export default function Config() {
<div className="flex items-center gap-2 text-xs"> <div className="flex items-center gap-2 text-xs">
{cs.running ? ( {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"> <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> </span>
) : cs.scheduler_alive ? ( ) : cs.scheduler_alive ? (
<span className="text-emerald-400 bg-emerald-900/30 border border-emerald-700/30 px-2 py-0.5 rounded"> <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>
) : ( ) : (
<span className="text-slate-600 bg-dark-700 border border-slate-700/30 px-2 py-0.5 rounded"> <span className="text-slate-600 bg-dark-700 border border-slate-700/30 px-2 py-0.5 rounded">
Scheduler inactif Scheduler inactive
</span> </span>
)} )}
</div> </div>
)} )}
</div> </div>
<p className="text-xs text-slate-500 mb-4"> <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> </p>
<div className="grid grid-cols-3 gap-4 mb-4"> <div className="grid grid-cols-3 gap-4 mb-4">
<div> <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 <button
onClick={() => setCycleEnabled(!cycleEnabled)} onClick={() => setCycleEnabled(!cycleEnabled)}
className={clsx('w-full py-2 rounded border text-sm font-semibold transition-all', { className={clsx('w-full py-2 rounded border text-sm font-semibold transition-all', {
'bg-blue-600 border-blue-500 text-white': cycleEnabled, 'bg-blue-600 border-blue-500 text-white': cycleEnabled,
'bg-dark-700 border-slate-700 text-slate-400 hover:border-slate-500': !cycleEnabled, 'bg-dark-700 border-slate-700 text-slate-400 hover:border-slate-500': !cycleEnabled,
})}> })}>
{cycleEnabled ? '✓ Activé' : '○ Désactivé'} {cycleEnabled ? '✓ Enabled' : '○ Disabled'}
</button> </button>
</div> </div>
<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"> <div className="flex gap-1">
{[1, 3, 6, 12].map(h => ( {[1, 3, 6, 12].map(h => (
<button key={h} onClick={() => setCycleHours(h)} <button key={h} onClick={() => setCycleHours(h)}
@@ -781,7 +781,7 @@ export default function Config() {
</div> </div>
<div> <div>
<label className="text-xs text-slate-500 mb-2 block"> <label className="text-xs text-slate-500 mb-2 block">
Similarité max ({Math.round(cycleSimilarity * 100)}%) Max similarity ({Math.round(cycleSimilarity * 100)}%)
</label> </label>
<input type="range" min="0.1" max="0.8" step="0.05" <input type="range" min="0.1" max="0.8" step="0.05"
value={cycleSimilarity} value={cycleSimilarity}
@@ -796,7 +796,7 @@ export default function Config() {
<div className="grid grid-cols-2 gap-4 mb-4"> <div className="grid grid-cols-2 gap-4 mb-4">
<div> <div>
<label className="text-xs text-slate-500 mb-2 block"> <label className="text-xs text-slate-500 mb-2 block">
Rétention Journal ({retentionDays}j) Journal retention ({retentionDays}d)
</label> </label>
<div className="flex gap-1"> <div className="flex gap-1">
{[30, 60, 90, 180].map(d => ( {[30, 60, 90, 180].map(d => (
@@ -812,7 +812,7 @@ export default function Config() {
</div> </div>
<div> <div>
<label className="text-xs text-slate-500 mb-2 block"> <label className="text-xs text-slate-500 mb-2 block">
Seuil maturité ({maturityThreshold}%) Maturity threshold ({maturityThreshold}%)
</label> </label>
<div className="flex gap-1"> <div className="flex gap-1">
{[20, 30, 35, 50].map(p => ( {[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="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 className="flex items-center justify-between mb-3">
<div> <div>
<span className="text-xs font-semibold text-slate-300">Cycles weekend</span> <span className="text-xs font-semibold text-slate-300">Weekend cycles</span>
<p className="text-[10px] text-slate-500 mt-0.5">Sam/Dim Globex ouvre dim ~18h UTC. Heures en UTC.</p> <p className="text-[10px] text-slate-500 mt-0.5">Sat/Sun Globex opens Sun ~18h UTC. Times in UTC.</p>
</div> </div>
<button <button
onClick={() => setWeekendEnabled(!weekendEnabled)} onClick={() => setWeekendEnabled(!weekendEnabled)}
@@ -841,12 +841,12 @@ export default function Config() {
'bg-amber-600/20 border-amber-500/50 text-amber-300': weekendEnabled, '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, 'bg-dark-700 border-slate-700 text-slate-500 hover:border-slate-500': !weekendEnabled,
})}> })}>
{weekendEnabled ? '✓ Activé' : '○ Désactivé'} {weekendEnabled ? '✓ Enabled' : '○ Disabled'}
</button> </button>
</div> </div>
{weekendEnabled && ( {weekendEnabled && (
<div> <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"> <div className="flex gap-1.5 flex-wrap">
{['06:00', '08:00', '12:00', '18:00', '22:00', '00:00'].map(t => { {['06:00', '08:00', '12:00', '18:00', '22:00', '00:00'].map(t => {
const active = weekendTimes.includes(t) const active = weekendTimes.includes(t)
@@ -865,7 +865,7 @@ export default function Config() {
})} })}
</div> </div>
{weekendTimes.length === 0 && ( {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> </div>
)} )}
@@ -874,14 +874,14 @@ export default function Config() {
{cs?.last_cycle && ( {cs?.last_cycle && (
<div className="card bg-dark-700/50 mb-2 text-xs space-y-2"> <div className="card bg-dark-700/50 mb-2 text-xs space-y-2">
<div className="flex items-center gap-4 flex-wrap"> <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="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')}> <span className={clsx('badge', cs.last_cycle.status === 'completed' ? 'badge-green' : 'badge-red')}>
{cs.last_cycle.status} {cs.last_cycle.status}
</span> </span>
<span className="text-slate-500">+{cs.last_cycle.patterns_added} patterns</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">{cs.last_cycle.patterns_scored} scored</span>
<span className="text-slate-500">Géo: {cs.last_cycle.geo_score}</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> <span className="text-slate-500 capitalize">{cs.last_cycle.dominant_regime}</span>
</div> </div>
</div> </div>
@@ -906,19 +906,19 @@ export default function Config() {
weekend_cycle_enabled: weekendEnabled, weekend_cycle_enabled: weekendEnabled,
weekend_cycle_times: weekendTimes.length > 0 ? weekendTimes.join(',') : '08:00,22:00', 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} 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"> 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" /> <Save className="w-4 h-4" />
{savingCycle ? 'Sauvegarde...' : 'Appliquer'} {savingCycle ? 'Saving...' : 'Apply'}
</button> </button>
<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} 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"> 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')} /> <RefreshCw className={clsx('w-4 h-4', triggeringCycle && 'animate-spin')} />
Lancer maintenant Run now
</button> </button>
</div> </div>
</div> </div>
@@ -929,8 +929,8 @@ export default function Config() {
<Gauge className="w-4 h-4 text-red-400" /> Schedulers VaR &amp; PnL <Gauge className="w-4 h-4 text-red-400" /> Schedulers VaR &amp; PnL
</h2> </h2>
<p className="text-xs text-slate-500 mb-4"> <p className="text-xs text-slate-500 mb-4">
Calcule et sauvegarde automatiquement les snapshots VaR (Black-Scholes delta) et PnL à intervalles définis. Automatically computes and saves VaR (Black-Scholes delta) and PnL snapshots at defined intervals.
Les données sont accessibles depuis la page VaR Analyse avec un historique complet. Data is accessible from the VaR Analysis page with full history.
</p> </p>
<div className="grid grid-cols-2 gap-6 mb-4"> <div className="grid grid-cols-2 gap-6 mb-4">
{/* VaR scheduler */} {/* VaR scheduler */}
@@ -939,21 +939,21 @@ export default function Config() {
<Gauge className="w-3.5 h-3.5 text-red-400" /> <Gauge className="w-3.5 h-3.5 text-red-400" />
<span className="text-sm font-semibold text-slate-300">Scheduler VaR</span> <span className="text-sm font-semibold text-slate-300">Scheduler VaR</span>
{schedStatus?.var?.alive {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-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"> Inactif</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>
<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)} <button onClick={() => setVarSchedEnabled(!varSchedEnabled)}
className={clsx('w-full py-2 rounded border text-sm font-semibold transition-all', { className={clsx('w-full py-2 rounded border text-sm font-semibold transition-all', {
'bg-red-700 border-red-600 text-white': varSchedEnabled, 'bg-red-700 border-red-600 text-white': varSchedEnabled,
'bg-dark-700 border-slate-700 text-slate-400': !varSchedEnabled, 'bg-dark-700 border-slate-700 text-slate-400': !varSchedEnabled,
})}> })}>
{varSchedEnabled ? '✓ Activé' : '○ Désactivé'} {varSchedEnabled ? '✓ Enabled' : '○ Disabled'}
</button> </button>
</div> </div>
<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"> <div className="flex gap-1">
{[1, 3, 6, 12, 24].map(h => ( {[1, 3, 6, 12, 24].map(h => (
<button key={h} onClick={() => setVarSchedHours(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" /> <DollarSign className="w-3.5 h-3.5 text-emerald-400" />
<span className="text-sm font-semibold text-slate-300">Scheduler PnL</span> <span className="text-sm font-semibold text-slate-300">Scheduler PnL</span>
{schedStatus?.pnl?.alive {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-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"> Inactif</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>
<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)} <button onClick={() => setPnlSchedEnabled(!pnlSchedEnabled)}
className={clsx('w-full py-2 rounded border text-sm font-semibold transition-all', { className={clsx('w-full py-2 rounded border text-sm font-semibold transition-all', {
'bg-emerald-700 border-emerald-600 text-white': pnlSchedEnabled, 'bg-emerald-700 border-emerald-600 text-white': pnlSchedEnabled,
'bg-dark-700 border-slate-700 text-slate-400': !pnlSchedEnabled, 'bg-dark-700 border-slate-700 text-slate-400': !pnlSchedEnabled,
})}> })}>
{pnlSchedEnabled ? '✓ Activé' : '○ Désactivé'} {pnlSchedEnabled ? '✓ Enabled' : '○ Disabled'}
</button> </button>
</div> </div>
<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"> <div className="flex gap-1">
{[1, 2, 4, 8, 24].map(h => ( {[1, 2, 4, 8, 24].map(h => (
<button key={h} onClick={() => setPnlSchedHours(h)} <button key={h} onClick={() => setPnlSchedHours(h)}
@@ -1010,21 +1010,21 @@ export default function Config() {
disabled={savingSched} 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"> 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" /> <Save className="w-4 h-4" />
{savingSched ? 'Sauvegarde…' : 'Appliquer'} {savingSched ? 'Saving…' : 'Apply'}
</button> </button>
<button <button
onClick={() => triggerVarNow()} onClick={() => triggerVarNow()}
disabled={triggeringVar} 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"> 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')} /> <Gauge className={clsx('w-4 h-4', triggeringVar && 'animate-spin')} />
Snapshot VaR maintenant VaR snapshot now
</button> </button>
<button <button
onClick={() => triggerPnlNow()} onClick={() => triggerPnlNow()}
disabled={triggeringPnl} 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"> 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')} /> <DollarSign className={clsx('w-4 h-4', triggeringPnl && 'animate-spin')} />
Snapshot PnL maintenant PnL snapshot now
</button> </button>
</div> </div>
</div> </div>
@@ -1037,16 +1037,16 @@ export default function Config() {
{/* Data API Keys */} {/* Data API Keys */}
<div className="card"> <div className="card">
<div className="flex items-center justify-between mb-3"> <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"> <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" />} {showKeys ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
</button> </button>
</div> </div>
<div className="grid grid-cols-3 gap-4"> <div className="grid grid-cols-3 gap-4">
{[ {[
{ label: 'NewsAPI Key', value: newsapiKey, setter: setNewsapiKey, placeholder: 'Obtenir sur newsapi.org' }, { label: 'NewsAPI Key', value: newsapiKey, setter: setNewsapiKey, placeholder: 'Get at newsapi.org' },
{ label: 'EIA API Key', value: eiaKey, setter: setEiaKey, placeholder: 'Obtenir sur eia.gov' }, { label: 'EIA API Key', value: eiaKey, setter: setEiaKey, placeholder: 'Get at eia.gov' },
{ label: 'FRED API Key', value: fredKey, setter: setFredKey, placeholder: 'Obtenir sur fred.stlouisfed.org' }, { label: 'FRED API Key', value: fredKey, setter: setFredKey, placeholder: 'Get at fred.stlouisfed.org' },
].map(({ label, value, setter, placeholder }) => ( ].map(({ label, value, setter, placeholder }) => (
<div key={label}> <div key={label}>
<label className="text-xs text-slate-500 mb-1 block">{label}</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)} 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"> 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" /> <Save className="w-4 h-4" />
{savingKeys ? 'Sauvegarde...' : 'Sauvegarder les clés'} {savingKeys ? 'Saving...' : 'Save keys'}
</button> </button>
</div> </div>
@@ -1093,14 +1093,14 @@ export default function Config() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-xs text-white font-semibold">{src.name || key}</span> <span className="text-xs text-white font-semibold">{src.name || key}</span>
<span className={clsx('badge text-xs', { <span className={clsx('badge text-xs', {
'badge-green': doc?.cost === 'Gratuit' || doc?.cost === 'Gratuit total', 'badge-green': doc?.cost === 'Free' || doc?.cost === 'Totally free',
'badge-blue': doc?.cost?.includes('gratuit') || doc?.cost?.includes('Inscription'), 'badge-blue': doc?.cost?.includes('free') || doc?.cost?.includes('registration'),
'badge-yellow': doc?.cost?.includes('clé'), 'badge-yellow': doc?.cost?.includes('key') || doc?.cost?.includes('key'),
'badge-red': doc?.cost?.includes('payante'), 'badge-red': doc?.cost?.includes('Paid'),
})}> })}>
{doc?.cost} {doc?.cost}
</span> </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>
<div className="text-xs text-slate-500 mt-0.5">{doc?.description}</div> <div className="text-xs text-slate-500 mt-0.5">{doc?.description}</div>
</div> </div>
@@ -1121,7 +1121,7 @@ export default function Config() {
<button onClick={saveSources} disabled={savingSources || !localSources} <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"> 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" /> <Save className="w-4 h-4" />
{savingSources ? 'Sauvegarde...' : 'Appliquer les changements'} {savingSources ? 'Saving...' : 'Apply changes'}
</button> </button>
</div> </div>
</> </>
@@ -1137,7 +1137,7 @@ export default function Config() {
<div className="card border-blue-700/30 bg-blue-900/5"> <div className="card border-blue-700/30 bg-blue-900/5">
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<h2 className="text-base font-bold text-white flex items-center gap-2"> <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> </h2>
<button <button
onClick={() => setIvGateEnabled(!ivGateEnabled)} onClick={() => setIvGateEnabled(!ivGateEnabled)}
@@ -1145,13 +1145,13 @@ export default function Config() {
'bg-blue-600 border-blue-500 text-white': ivGateEnabled, 'bg-blue-600 border-blue-500 text-white': ivGateEnabled,
'bg-dark-700 border-slate-700 text-slate-400': !ivGateEnabled, 'bg-dark-700 border-slate-700 text-slate-400': !ivGateEnabled,
})}> })}>
{ivGateEnabled ? ' Gate actif' : ' Gate désactivé'} {ivGateEnabled ? '✓ Gate active' : '○ Gate disabled'}
</button> </button>
</div> </div>
<p className="text-xs text-slate-500 mb-5"> <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 Automatically blocks trades with verdict <span className="text-red-400 font-semibold">ALERT</span> before
qu'ils soient loggés dans le journal. Un trade ALERT est une stratégie incompatible avec le régime de volatilité actuel they are logged in the journal. An ALERT trade is a strategy incompatible with the current volatility regime
(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>. (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> </p>
<div className={clsx('space-y-5 transition-opacity', !ivGateEnabled && 'opacity-40 pointer-events-none')}> <div className={clsx('space-y-5 transition-opacity', !ivGateEnabled && 'opacity-40 pointer-events-none')}>
@@ -1160,7 +1160,7 @@ export default function Config() {
<div> <div>
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<label className="text-xs text-slate-400 font-medium"> <label className="text-xs text-slate-400 font-medium">
Seuil vol chère (IVR High) High vol threshold (IVR High)
</label> </label>
<span className={clsx('font-mono font-bold text-base', ivGateHigh >= 70 ? 'text-orange-400' : 'text-yellow-400')}> <span className={clsx('font-mono font-bold text-base', ivGateHigh >= 70 ? 'text-orange-400' : 'text-yellow-400')}>
{ivGateHigh}% {ivGateHigh}%
@@ -1186,7 +1186,7 @@ export default function Config() {
<div> <div>
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<label className="text-xs text-slate-400 font-medium"> <label className="text-xs text-slate-400 font-medium">
Seuil vol extrême (IVR Extreme) Extreme vol threshold (IVR Extreme)
</label> </label>
<span className="font-mono font-bold text-base text-red-400">{ivGateExtreme}%</span> <span className="font-mono font-bold text-base text-red-400">{ivGateExtreme}%</span>
</div> </div>
@@ -1202,8 +1202,8 @@ export default function Config() {
<span>50%</span><span>100%</span> <span>50%</span><span>100%</span>
</div> </div>
<div className="text-[10px] text-slate-500 mt-1.5 space-y-0.5"> <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>IVR &gt; {ivGateExtreme}% <span className="text-red-400 font-semibold">BLOCKED</span> naked long vol ALERT</div>
<div>Straddle / Strangle ALERT dès IVR &gt; {ivGateExtreme}% (pénalité ×1.3)</div> <div>Straddle / Strangle ALERT when IVR &gt; {ivGateExtreme}% (penalty ×1.3)</div>
</div> </div>
</div> </div>
</div> </div>
@@ -1213,7 +1213,7 @@ export default function Config() {
<div> <div>
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<label className="text-xs text-slate-400 font-medium"> <label className="text-xs text-slate-400 font-medium">
Seuil skew put (protection chère) Put skew threshold (expensive protection)
</label> </label>
<span className="font-mono font-bold text-base text-orange-400">{ivGateSkew} pts</span> <span className="font-mono font-bold text-base text-orange-400">{ivGateSkew} pts</span>
</div> </div>
@@ -1222,31 +1222,31 @@ export default function Config() {
onChange={e => setIvGateSkew(parseInt(e.target.value))} onChange={e => setIvGateSkew(parseInt(e.target.value))}
className="w-full accent-orange-500" /> className="w-full accent-orange-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1"> <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>
<div className="text-[10px] text-slate-500 mt-1.5"> <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>
</div> </div>
{/* Visual summary */} {/* Visual summary */}
<div className="bg-dark-700/60 rounded-lg px-4 py-3 text-xs space-y-2"> <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="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-emerald-500" /> <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>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-yellow-500" /> <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>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-red-500 animate-pulse" /> <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>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-orange-500" /> <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> </div>
</div> </div>
@@ -1257,12 +1257,12 @@ export default function Config() {
onClick={() => saveOptionsGate( onClick={() => saveOptionsGate(
{ iv_gate_enabled: ivGateEnabled, iv_gate_ivr_high: ivGateHigh, { iv_gate_enabled: ivGateEnabled, iv_gate_ivr_high: ivGateHigh,
iv_gate_ivr_extreme: ivGateExtreme, iv_gate_skew_threshold: ivGateSkew }, 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} 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"> 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" /> <Save className="w-4 h-4" />
{savingOptionsGate ? 'Sauvegarde...' : 'Sauvegarder le gate'} {savingOptionsGate ? 'Saving...' : 'Save gate'}
</button> </button>
</div> </div>
</div> </div>
@@ -1270,16 +1270,16 @@ export default function Config() {
{/* ── Paramètres de sortie ── */} {/* ── Paramètres de sortie ── */}
<div className="card"> <div className="card">
<h2 className="text-base font-bold text-white flex items-center gap-2 mb-1"> <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> </h2>
<p className="text-xs text-slate-500 mb-5"> <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> </p>
<div className="grid grid-cols-2 gap-6 mb-4"> <div className="grid grid-cols-2 gap-6 mb-4">
<div> <div>
<label className="text-xs text-slate-500 mb-2 block"> <label className="text-xs text-slate-500 mb-2 block">
Objectif de gain : <span className="text-emerald-400 font-mono font-bold">+{exitTarget}%</span> Profit target: <span className="text-emerald-400 font-mono font-bold">+{exitTarget}%</span>
<span className="text-slate-600 ml-1">(P&L sous-jacent)</span> <span className="text-slate-600 ml-1">(underlying P&L)</span>
</label> </label>
<input type="range" min="5" max="150" step="5" <input type="range" min="5" max="150" step="5"
value={exitTarget} value={exitTarget}
@@ -1291,8 +1291,8 @@ export default function Config() {
</div> </div>
<div> <div>
<label className="text-xs text-slate-500 mb-2 block"> <label className="text-xs text-slate-500 mb-2 block">
Stop-loss : <span className="text-red-400 font-mono font-bold">{exitStop}%</span> 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> <span className="text-slate-600 ml-1">(underlying P&L)</span>
</label> </label>
<input type="range" min="-90" max="-5" step="5" <input type="range" min="-90" max="-5" step="5"
value={exitStop} value={exitStop}
@@ -1305,11 +1305,11 @@ export default function Config() {
</div> </div>
<div className="grid grid-cols-2 gap-6 mb-5"> <div className="grid grid-cols-2 gap-6 mb-5">
<div> <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"> <div className="flex gap-2">
{[ {[
{ value: 'badge_only', label: '🔔 Badge uniquement' }, { value: 'badge_only', label: '🔔 Badge only' },
{ value: 'auto', label: '⚡ Auto (futur)' }, { value: 'auto', label: '⚡ Auto (future)' },
].map(opt => ( ].map(opt => (
<button key={opt.value} onClick={() => setExitSignalMode(opt.value)} <button key={opt.value} onClick={() => setExitSignalMode(opt.value)}
className={clsx('flex-1 py-2 rounded text-xs border transition-all', { className={clsx('flex-1 py-2 rounded text-xs border transition-all', {
@@ -1321,19 +1321,19 @@ export default function Config() {
))} ))}
</div> </div>
<div className="text-[10px] text-slate-600 mt-1"> <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> </div>
<div> <div>
<label className="text-xs text-slate-500 mb-2 block"> <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> </label>
<input type="range" min="10" max="60" step="5" <input type="range" min="10" max="60" step="5"
value={exitSignalThreshold} value={exitSignalThreshold}
onChange={e => setExitSignalThreshold(parseFloat(e.target.value))} onChange={e => setExitSignalThreshold(parseFloat(e.target.value))}
className="w-full accent-blue-500" /> className="w-full accent-blue-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1"> <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> </div>
</div> </div>
@@ -1341,12 +1341,12 @@ export default function Config() {
onClick={() => saveExitDefaults( onClick={() => saveExitDefaults(
{ target_pct: exitTarget, stop_loss_pct: exitStop, { target_pct: exitTarget, stop_loss_pct: exitStop,
signal_reversal_mode: exitSignalMode, signal_reversal_threshold: exitSignalThreshold }, 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} 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"> 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" /> <Save className="w-4 h-4" />
{savingExitDefaults ? 'Sauvegarde...' : 'Sauvegarder les seuils'} {savingExitDefaults ? 'Saving...' : 'Save thresholds'}
</button> </button>
</div> </div>
@@ -1354,7 +1354,7 @@ export default function Config() {
<div className="card border-purple-700/30 bg-purple-900/5"> <div className="card border-purple-700/30 bg-purple-900/5">
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<h2 className="text-base font-bold text-white flex items-center gap-2"> <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> </h2>
<button <button
onClick={() => setTechEnabled(!techEnabled)} onClick={() => setTechEnabled(!techEnabled)}
@@ -1362,23 +1362,23 @@ export default function Config() {
'bg-purple-600 border-purple-500 text-white': techEnabled, 'bg-purple-600 border-purple-500 text-white': techEnabled,
'bg-dark-700 border-slate-700 text-slate-400': !techEnabled, 'bg-dark-700 border-slate-700 text-slate-400': !techEnabled,
})}> })}>
{techEnabled ? '✓ Activé' : '○ Désactivé'} {techEnabled ? '✓ Enabled' : '○ Disabled'}
</button> </button>
</div> </div>
<p className="text-xs text-slate-500 mb-4"> <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> </p>
<div className={clsx('space-y-4 transition-opacity', !techEnabled && 'opacity-40 pointer-events-none')}> <div className={clsx('space-y-4 transition-opacity', !techEnabled && 'opacity-40 pointer-events-none')}>
{/* Indicateurs à activer */} {/* Indicateurs à activer */}
<div> <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"> <div className="flex flex-wrap gap-2">
{[ {[
{ key: 'rsi', label: 'RSI', desc: 'Momentum / surachat-survente' }, { key: 'rsi', label: 'RSI', desc: 'Momentum / overbought-oversold' },
{ key: 'ma', label: 'MA fast/slow', desc: 'Tendance & golden/death cross' }, { key: 'ma', label: 'MA fast/slow', desc: 'Trend & golden/death cross' },
{ key: 'bollinger', label: 'Bollinger', desc: 'Position dans les bandes de vol' }, { key: 'bollinger', label: 'Bollinger', desc: 'Position in vol bands' },
{ key: 'atr', label: 'ATR', desc: 'Volatilité réalisée récente' }, { key: 'atr', label: 'ATR', desc: 'Recent realized volatility' },
].map(({ key, label, desc }) => { ].map(({ key, label, desc }) => {
const active = techList.includes(key) const active = techList.includes(key)
return ( return (
@@ -1397,13 +1397,13 @@ export default function Config() {
) )
})} })}
</div> </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> </div>
{/* Calibration auto */} {/* Calibration auto */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <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> <p className="text-[10px] text-slate-500">30j RSI14, MA20/50 | 90j RSI21, MA50/100 | &gt;90j RSI28, MA100/200</p>
</div> </div>
<button <button
@@ -1412,7 +1412,7 @@ export default function Config() {
'bg-purple-600 border-purple-500 text-white': techAutoCalibrate, 'bg-purple-600 border-purple-500 text-white': techAutoCalibrate,
'bg-dark-700 border-slate-700 text-slate-400': !techAutoCalibrate, 'bg-dark-700 border-slate-700 text-slate-400': !techAutoCalibrate,
})}> })}>
{techAutoCalibrate ? '✓ Auto' : '○ Fixe'} {techAutoCalibrate ? '✓ Auto' : '○ Fixed'}
</button> </button>
</div> </div>
</div> </div>
@@ -1420,12 +1420,12 @@ export default function Config() {
<button <button
onClick={() => saveTechIndicators( onClick={() => saveTechIndicators(
{ tech_indicators_enabled: techEnabled, tech_indicators_list: techList.join(','), tech_indicators_auto_calibrate: techAutoCalibrate }, { 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} 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"> 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" /> <Save className="w-4 h-4" />
{savingTechIndicators ? 'Sauvegarde...' : 'Sauvegarder les indicateurs'} {savingTechIndicators ? 'Saving...' : 'Save indicators'}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -19,10 +19,10 @@ import {
import { scoreColor } from '../components/TradeIdeas' import { scoreColor } from '../components/TradeIdeas'
const riskGauge = (score: number) => { const riskGauge = (score: number) => {
if (score < 25) return { color: 'text-emerald-400', bg: 'bg-emerald-500', label: 'FAIBLE' } 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: 'MODÉRÉ' } 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: 'ÉLEVÉ' } if (score < 75) return { color: 'text-orange-400', bg: 'bg-orange-500', label: 'HIGH' }
return { color: 'text-red-400', bg: 'bg-red-500', label: 'EXTRÊME' } return { color: 'text-red-400', bg: 'bg-red-500', label: 'EXTREME' }
} }
const assetEmoji: Record<string, string> = { const assetEmoji: Record<string, string> = {
@@ -37,9 +37,9 @@ const ASSET_COLORS: Record<string, string> = {
} }
const REGIME_LABELS: Record<string, string> = { const REGIME_LABELS: Record<string, string> = {
goldilocks: 'Goldilocks', desinflation: 'Désinflation', stagflation: 'Stagflation', goldilocks: 'Goldilocks', desinflation: 'Disinflation', stagflation: 'Stagflation',
recession: 'Récession', crise_liquidite: 'Crise liq.', reflation: 'Reflation', recession: 'Recession', crise_liquidite: 'Liq. Crisis', reflation: 'Reflation',
soft_landing: 'Soft Landing', inflation_shock: 'Infl. Choc', incertain: 'Incertain', soft_landing: 'Soft Landing', inflation_shock: 'Infl. Shock', incertain: 'Uncertain',
} }
function ViewToggle({ value, onChange }: { value: 'simulated' | 'portfolio'; onChange: (v: 'simulated' | 'portfolio') => void }) { 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')} <button onClick={click('simulated')}
className={clsx('px-1.5 py-0.5 rounded transition-colors', 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')}> value === 'simulated' ? 'bg-blue-600 text-white' : 'text-slate-600 hover:text-slate-400')}>
Simulé Simulated
</button> </button>
<button onClick={click('portfolio')} <button onClick={click('portfolio')}
className={clsx('px-1.5 py-0.5 rounded transition-colors', className={clsx('px-1.5 py-0.5 rounded transition-colors',
@@ -195,7 +195,7 @@ export default function Dashboard() {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <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"> <p className="text-xs text-slate-500 mt-0.5">
{format(new Date(), "EEEE d MMMM yyyy · HH:mm", { locale: fr })} {format(new Date(), "EEEE d MMMM yyyy · HH:mm", { locale: fr })}
</p> </p>
@@ -235,10 +235,10 @@ export default function Dashboard() {
<div className="card col-span-1"> <div className="card col-span-1">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<div className="section-title flex items-center gap-1 mb-0"> <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> </div>
<Link to="/geo" className="flex items-center gap-0.5 text-[10px] text-slate-600 hover:text-slate-300 transition-colors"> <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> </Link>
</div> </div>
{riskLoading ? ( {riskLoading ? (
@@ -259,12 +259,12 @@ export default function Dashboard() {
))} ))}
</div> </div>
</> </>
) : <div className="text-slate-500 text-xs">Backend requis</div>} ) : <div className="text-slate-500 text-xs">Backend required</div>}
</div> </div>
{/* Radar */} {/* Radar */}
<div className="card col-span-1"> <div className="card col-span-1">
<div className="section-title">Cartographie risques</div> <div className="section-title">Risk Map</div>
{radarData.length > 0 ? ( {radarData.length > 0 ? (
<ResponsiveContainer width="100%" height={160}> <ResponsiveContainer width="100%" height={160}>
<RadarChart data={radarData}> <RadarChart data={radarData}>
@@ -273,18 +273,18 @@ export default function Dashboard() {
<Radar dataKey="score" stroke="#3b82f6" fill="#3b82f6" fillOpacity={0.25} /> <Radar dataKey="score" stroke="#3b82f6" fill="#3b82f6" fillOpacity={0.25} />
</RadarChart> </RadarChart>
</ResponsiveContainer> </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> </div>
{/* Patterns — AI scores */} {/* Patterns — AI scores */}
<div className="card col-span-1 overflow-y-auto max-h-64"> <div className="card col-span-1 overflow-y-auto max-h-64">
<div className="section-title flex items-center gap-1"> <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>} {allPatterns.length > 0 && <span className="text-slate-600 text-xs ml-auto font-normal">{allPatterns.length} patterns</span>}
</div> </div>
<div className="space-y-1.5 mt-1"> <div className="space-y-1.5 mt-1">
{allPatterns.length === 0 ? ( {allPatterns.length === 0 ? (
<div className="text-slate-600 text-xs">Backend requis</div> <div className="text-slate-600 text-xs">Backend required</div>
) : ( ) : (
[...allPatterns] [...allPatterns]
.sort((a, b) => { .sort((a, b) => {
@@ -321,7 +321,7 @@ export default function Dashboard() {
{/* Calendrier */} {/* Calendrier */}
<div className="card col-span-1 space-y-3"> <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"> <div className="space-y-1.5">
{calendar?.slice(0, 4).map((ev, i) => ( {calendar?.slice(0, 4).map((ev, i) => (
<div key={i} className="flex items-center gap-2 text-xs"> <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"> <div className="grid grid-cols-2 gap-2 flex-shrink-0">
{/* Left: Unrealized */} {/* Left: Unrealized */}
<div className="bg-dark-700/40 rounded px-2.5 py-2 border border-slate-700/30"> <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' ? ( {pnlView === 'simulated' ? (
<> <>
<div className={clsx('text-xl font-bold font-mono leading-none', <div className={clsx('text-xl font-bold font-mono leading-none',
@@ -437,7 +437,7 @@ export default function Dashboard() {
{/* Right: Realized */} {/* Right: Realized */}
<div className="bg-dark-700/40 rounded px-2.5 py-2 border border-slate-700/30"> <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' ? ( {pnlView === 'simulated' ? (
<> <>
<div className={clsx('text-xl font-bold font-mono leading-none', <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')}> closedProfitEur >= 0 ? 'text-emerald-500' : 'text-red-400')}>
{closedCapital > 0 {closedCapital > 0
? `${closedProfitEur >= 0 ? '+' : ''}${closedProfitEur.toFixed(0)}` ? `${closedProfitEur >= 0 ? '+' : ''}${closedProfitEur.toFixed(0)}`
: closedTrades.length > 0 ? 'sans capital' : ''} : closedTrades.length > 0 ? 'no capital' : ''}
</div> </div>
<div className="text-[10px] text-slate-500 mt-1"> <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> <span className="text-red-400">{closedLosers}</span>
</div> </div>
</> </>
@@ -473,7 +473,7 @@ export default function Dashboard() {
</div> </div>
)} )}
<div className="text-[10px] text-slate-500 mt-1"> <div className="text-[10px] text-slate-500 mt-1">
{pf?.closed_positions ?? 0} fermées {pf?.closed_positions ?? 0} closed
</div> </div>
</> </>
)} )}
@@ -483,7 +483,7 @@ export default function Dashboard() {
{/* Total line */} {/* Total line */}
<div className="mt-1.5 flex items-center justify-between text-[10px] flex-shrink-0"> <div className="mt-1.5 flex items-center justify-between text-[10px] flex-shrink-0">
<span className="text-slate-600"> <span className="text-slate-600">
Investi{isEstimated ? ' ~' : ''} {pnlView === 'simulated' Invested{isEstimated ? ' ~' : ''} {pnlView === 'simulated'
? (totalCapital > 0 ? `${totalCapital.toFixed(0)}` : '—') ? (totalCapital > 0 ? `${totalCapital.toFixed(0)}` : '—')
: (pfInvest != null ? `${pfInvest.toFixed(0)}` : '—')} : (pfInvest != null ? `${pfInvest.toFixed(0)}` : '—')}
</span> </span>
@@ -509,7 +509,7 @@ export default function Dashboard() {
const gradId = 'pnlGrad' const gradId = 'pnlGrad'
return ( return (
<div className="mt-1.5 pt-1.5 border-t border-slate-700/30"> <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}> <ResponsiveContainer width="100%" height={58}>
<AreaChart data={chartData} margin={{ top: 2, right: 0, left: -38, bottom: 0 }}> <AreaChart data={chartData} margin={{ top: 2, right: 0, left: -38, bottom: 0 }}>
<defs> <defs>
@@ -552,14 +552,14 @@ export default function Dashboard() {
return ( return (
<Link to="/risk" className="card flex flex-col h-[288px] overflow-hidden hover:border-slate-600/60 transition-all cursor-pointer"> <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"> <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"> <div className="flex items-center gap-1.5">
<ViewToggle value={riskView} onChange={setRiskViewPersist} /> <ViewToggle value={riskView} onChange={setRiskViewPersist} />
<ArrowUpRight className="w-3 h-3 text-slate-600" /> <ArrowUpRight className="w-3 h-3 text-slate-600" />
</div> </div>
</div> </div>
<div className={clsx('text-lg font-bold mt-0.5', alertCount > 0 ? 'text-red-400' : 'text-emerald-400')}> <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> </div>
{sortedClasses.length > 0 && ( {sortedClasses.length > 0 && (
<div className="mt-2 space-y-1"> <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> <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"> <div className="flex-1 h-2 bg-dark-700 rounded-full overflow-hidden flex">
{data.bullish > 0 && ( {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 }} /> style={{ width: `${(data.bullish / openCount) * 100}%`, background: ASSET_COLORS[cls] ?? '#3b82f6', opacity: 0.9 }} />
)} )}
{data.bearish > 0 && ( {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 }} /> style={{ width: `${(data.bearish / openCount) * 100}%`, background: '#ef4444', opacity: 0.7 }} />
)} )}
</div> </div>
@@ -580,15 +580,15 @@ export default function Dashboard() {
</div> </div>
))} ))}
<div className="flex items-center gap-1 text-[9px] text-slate-700 pt-0.5"> <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-blue-500/60"></span>bullish
<span className="inline-block w-2 h-1.5 rounded-sm bg-red-500/60 ml-1"></span>baissier <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} conflit{conflictCount > 1 ? 's' : ''}</span>} {conflictCount > 0 && <span className="ml-auto text-red-400">{conflictCount} conflict{conflictCount > 1 ? 's' : ''}</span>}
</div> </div>
</div> </div>
)} )}
{sortedClasses.length === 0 && ( {sortedClasses.length === 0 && (
<div className="text-[10px] text-slate-600 mt-1"> <div className="text-[10px] text-slate-600 mt-1">
{openCount > 0 ? `${openCount} positions` : 'Aucune position'} {openCount > 0 ? `${openCount} positions` : 'No positions'}
</div> </div>
)} )}
{/* Latest VaR snapshot */} {/* Latest VaR snapshot */}
@@ -629,7 +629,7 @@ export default function Dashboard() {
return ( return (
<Link to="/journal" className="card flex flex-col h-[288px] overflow-hidden hover:border-slate-600/60 transition-all cursor-pointer"> <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"> <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" /> <ArrowUpRight className="w-3 h-3 text-slate-600" />
</div> </div>
{/* Column headers */} {/* 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">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">Log.</span>
<span className="text-[8px] text-slate-700 w-8 shrink-0 text-right">Clos</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> </div>
{runs.length > 0 ? ( {runs.length > 0 ? (
<div className="space-y-2.5 flex-1 overflow-hidden"> <div className="space-y-2.5 flex-1 overflow-hidden">
@@ -668,7 +668,7 @@ export default function Dashboard() {
})} })}
</div> </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> </Link>
) )
@@ -694,38 +694,38 @@ export default function Dashboard() {
key: 'vix', label: 'VIX', key: 'vix', label: 'VIX',
color: (v: number) => v < 18 ? 'text-emerald-400' : v < 25 ? 'text-yellow-400' : 'text-red-400', color: (v: number) => v < 18 ? 'text-emerald-400' : v < 25 ? 'text-yellow-400' : 'text-red-400',
fmt: (v: number) => v.toFixed(1), 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', color: (v: number) => v >= 0 ? 'text-emerald-400' : 'text-red-400',
fmt: (v: number) => `${v >= 0 ? '+' : ''}${v.toFixed(1)}%`, 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', 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)}`, 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', color: (v: number, chg: number) => chg >= 0 ? 'text-emerald-400' : 'text-red-400',
fmt: (v: number, chg: number) => `${chg >= 0 ? '+' : ''}${chg.toFixed(2)}%`, 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', 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)}%`, 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 ( return (
<Link to="/macro" className="card flex flex-col h-[288px] overflow-hidden hover:border-slate-600/60 transition-all cursor-pointer"> <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"> <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" /> <ArrowUpRight className="w-3 h-3 text-slate-600" />
</div> </div>
<div className={clsx('text-base font-bold mt-1', colorClass)}> <div className={clsx('text-base font-bold mt-1', colorClass)}>
@@ -834,7 +834,7 @@ export default function Dashboard() {
})} })}
</div> </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> </Link>
) )
@@ -849,7 +849,7 @@ export default function Dashboard() {
return ( return (
<Link to="/journal" className="card block hover:border-slate-600/60 transition-all cursor-pointer"> <Link to="/journal" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-1"> <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" /> <ArrowUpRight className="w-3 h-3 text-slate-600" />
</div> </div>
{recent.length > 0 ? ( {recent.length > 0 ? (
@@ -873,7 +873,7 @@ export default function Dashboard() {
{pnl >= 0 ? '+' : ''}{pnl.toFixed(1)}% {pnl >= 0 ? '+' : ''}{pnl.toFixed(1)}%
</span> </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> </div>
{entryDate && ( {entryDate && (
@@ -884,7 +884,7 @@ export default function Dashboard() {
})} })}
</div> </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> </Link>
) )
@@ -899,7 +899,7 @@ export default function Dashboard() {
return ( return (
<Link to="/patterns" className="card block hover:border-slate-600/60 transition-all cursor-pointer"> <Link to="/patterns" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-1"> <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" /> <ArrowUpRight className="w-3 h-3 text-slate-600" />
</div> </div>
{recent.length > 0 ? ( {recent.length > 0 ? (
@@ -932,7 +932,7 @@ export default function Dashboard() {
})} })}
</div> </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> </Link>
) )
@@ -969,7 +969,7 @@ export default function Dashboard() {
})} })}
</div> </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> </Link>
) )
@@ -982,7 +982,7 @@ export default function Dashboard() {
<div key={cls} className="card"> <div key={cls} className="card">
<div className="section-title">{assetEmoji[cls]} {cls}</div> <div className="section-title">{assetEmoji[cls]} {cls}</div>
{allQuotes?.[cls]?.map(q => <QuoteRow key={q.symbol} q={q} />) ?? ( {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> </div>
))} ))}

View File

@@ -13,16 +13,16 @@ const CATEGORY_COLORS: Record<string, string> = {
} }
const CATEGORY_LABELS: Record<string, string> = { const CATEGORY_LABELS: Record<string, string> = {
military: '⚔️ Militaire', sanctions: '🚫 Sanctions', elections: '🗳️ Élections', military: '⚔️ Military', sanctions: '🚫 Sanctions', elections: '🗳️ Elections',
natural_disaster: '🌪️ Catastrophe', health_crisis: '🏥 Santé', natural_disaster: '🌪️ Disaster', health_crisis: '🏥 Health',
resource_scarcity: '⚠️ Ressources', trade_war: '🤝 Commerce', resource_scarcity: '⚠️ Resources', trade_war: '🤝 Trade',
energy: '⚡ Énergie', political_speech: '🎙️ Discours', energy: '⚡ Energy', political_speech: '🎙️ Speech',
financial_crisis: '💸 Finance', general: '📰 Général', financial_crisis: '💸 Finance', general: '📰 General',
} }
const ASSET_LABELS: Record<string, string> = { const ASSET_LABELS: Record<string, string> = {
energy: '⛽ Énergie', metals: '🥇 Métaux', agriculture: '🌾 Agri', energy: '⛽ Energy', metals: '🥇 Metals', agriculture: '🌾 Agri',
equities: '📈 Actions', indices: '📊 Indices', forex: '💱 Forex', equities: '📈 Equities', indices: '📊 Indices', forex: '💱 Forex',
} }
function ImpactBar({ value, label }: { value: number; label: string }) { function ImpactBar({ value, label }: { value: number; label: string }) {
@@ -90,14 +90,14 @@ function NewsCard({ news }: { news: GeoNews }) {
) )
})} })}
{(news as any).ai_resolution && ( {(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> </div>
)} )}
<p className="text-xs text-slate-400 leading-relaxed mb-3">{news.summary}</p> <p className="text-xs text-slate-400 leading-relaxed mb-3">{news.summary}</p>
{Object.keys(news.asset_impacts).length > 0 && ( {Object.keys(news.asset_impacts).length > 0 && (
<div className="mb-3"> <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]) => ( {Object.entries(news.asset_impacts).map(([cls, val]) => (
<ImpactBar key={cls} label={ASSET_LABELS[cls] ?? cls} value={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" <a href={news.url} target="_blank" rel="noopener noreferrer"
className="text-xs text-blue-400 hover:text-blue-300 flex items-center gap-1" className="text-xs text-blue-400 hover:text-blue-300 flex items-center gap-1"
onClick={e => e.stopPropagation()}> onClick={e => e.stopPropagation()}>
<ExternalLink className="w-3 h-3" /> Lire l'article <ExternalLink className="w-3 h-3" /> Read article
</a> </a>
)} )}
</> </>
)} )}
<div className="flex items-center justify-between mt-2 text-xs text-slate-600"> <div className="flex items-center justify-between mt-2 text-xs text-slate-600">
<span>{news.date?.slice(0, 16)}</span> <span>{news.date?.slice(0, 16)}</span>
<span>{expanded ? '▲ Réduire' : '▼ Détail'}</span> <span>{expanded ? '▲ Collapse' : '▼ Details'}</span>
</div> </div>
</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="text-sm font-semibold text-white line-clamp-1">{p.name}</div>
<div className="flex items-center gap-1 mt-0.5 flex-wrap"> <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="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 && ( {hasNews && (
<span className="text-xs text-emerald-400 flex items-center gap-0.5"> <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 <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' : 'bg-emerald-900/30 text-emerald-400 border border-emerald-700/30'
)}> )}>
<Brain className="w-2.5 h-2.5" /> <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> </span>
)} )}
</div> </div>
@@ -211,7 +211,7 @@ function PatternRelevanceCard({ p }: { p: any }) {
<> <>
<button onClick={() => setExpanded(!expanded)} <button onClick={() => setExpanded(!expanded)}
className="text-xs text-slate-600 hover:text-slate-400 mb-2"> 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> </button>
{expanded && ( {expanded && (
<div className="space-y-1.5"> <div className="space-y-1.5">
@@ -235,7 +235,7 @@ function PatternRelevanceCard({ p }: { p: any }) {
<a href={n.url} target="_blank" rel="noopener noreferrer" <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" className="text-xs text-blue-400 hover:text-blue-300 flex items-center gap-1 mt-1"
onClick={e => e.stopPropagation()}> onClick={e => e.stopPropagation()}>
<ExternalLink className="w-2.5 h-2.5" /> Lire <ExternalLink className="w-2.5 h-2.5" /> Read
</a> </a>
)} )}
</div> </div>
@@ -246,7 +246,7 @@ function PatternRelevanceCard({ p }: { p: any }) {
)} )}
{!hasNews && p.relevance === 0 && ( {!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> </div>
) )
@@ -256,10 +256,10 @@ type SortMode = 'impact_desc' | 'impact_asc' | 'date_desc' | 'date_asc'
type DateRange = 'all' | 'today' | '7d' | '30d' type DateRange = 'all' | 'today' | '7d' | '30d'
const DATE_RANGE_LABELS: Record<DateRange, string> = { 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> = { 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 { function isWithinRange(dateStr: string, range: DateRange): boolean {
@@ -304,10 +304,10 @@ export default function GeoRadar() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-xl font-bold text-white flex items-center gap-2"> <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> </h1>
<p className="text-xs text-slate-500 mt-0.5"> <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> </p>
</div> </div>
{riskScore && ( {riskScore && (
@@ -337,7 +337,7 @@ export default function GeoRadar() {
'bg-blue-600 text-white': tab === t, 'bg-blue-600 text-white': tab === t,
'text-slate-400 hover:text-slate-200': 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> </button>
))} ))}
</div> </div>
@@ -345,7 +345,7 @@ export default function GeoRadar() {
{/* Day selector — only shown on patterns tab */} {/* Day selector — only shown on patterns tab */}
{tab === 'patterns' && ( {tab === 'patterns' && (
<div className="flex items-center gap-2"> <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"> <div className="flex gap-0.5 bg-dark-700 p-0.5 rounded">
{[1, 2, 7, 30].map(d => ( {[1, 2, 7, 30].map(d => (
<button key={d} onClick={() => setDays(d)} <button key={d} onClick={() => setDays(d)}
@@ -353,11 +353,11 @@ export default function GeoRadar() {
'bg-slate-600 text-white': days === d, 'bg-slate-600 text-white': days === d,
'text-slate-400 hover:text-slate-200': days !== d, 'text-slate-400 hover:text-slate-200': days !== d,
})}> })}>
{d}j {d}d
</button> </button>
))} ))}
</div> </div>
<span className="text-xs text-slate-600">derniers jours</span> <span className="text-xs text-slate-600">last days</span>
</div> </div>
)} )}
</div> </div>
@@ -375,7 +375,7 @@ export default function GeoRadar() {
'bg-blue-600 border-blue-500 text-white': filterCat === cat, 'bg-blue-600 border-blue-500 text-white': filterCat === cat,
'border-slate-700 text-slate-400 hover:border-slate-500': 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> </button>
))} ))}
</div> </div>
@@ -384,7 +384,7 @@ export default function GeoRadar() {
{/* Sort + date range row */} {/* Sort + date range row */}
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="flex items-center gap-2"> <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"> <div className="flex gap-0.5 bg-dark-700 p-0.5 rounded">
{(['today', '7d', '30d', 'all'] as DateRange[]).map(r => ( {(['today', '7d', '30d', 'all'] as DateRange[]).map(r => (
<button key={r} onClick={() => setDateRange(r)} <button key={r} onClick={() => setDateRange(r)}
@@ -398,7 +398,7 @@ export default function GeoRadar() {
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <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"> <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 => ( {(['impact_desc', 'impact_asc', 'date_desc', 'date_asc'] as SortMode[]).map(s => (
<button key={s} onClick={() => setSortMode(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.map((n, i) => <NewsCard key={n.id || i} news={n} />)}
{filtered.length === 0 && ( {filtered.length === 0 && (
<div className="card col-span-2 text-center py-8 text-slate-500"> <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>
)} )}
</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"> <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" /> <Search className="w-3.5 h-3.5 shrink-0 text-blue-400" />
<span> <span>
Correspondance entre les mots-clés de chaque pattern et les news des {days} derniers jours. Matching each pattern's keywords against news from the last {days} days.
Ce signal sert à enrichir le contexte donné à l'IA lors du scoring. This signal enriches the context sent to the AI during scoring.
{patternsWithNews > 0 && ( {patternsWithNews > 0 && (
<span className="text-emerald-400 ml-1"> <span className="text-emerald-400 ml-1">
{patternsWithNews} patterns ont des news correspondantes. {patternsWithNews} patterns have matching news.
</span> </span>
)} )}
</span> </span>
@@ -458,7 +458,7 @@ export default function GeoRadar() {
))} ))}
{(!patternRelevance || patternRelevance.length === 0) && ( {(!patternRelevance || patternRelevance.length === 0) && (
<div className="card text-center py-8 text-slate-500"> <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>
)} )}
</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 }> = { const SCENARIO_META: Record<string, { label: string; color: string; emoji: string }> = {
goldilocks: { label: 'Goldilocks', color: '#22c55e', emoji: '🌟' }, 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: '🛬' }, soft_landing: { label: 'Soft Landing', color: '#3b82f6', emoji: '🛬' },
reflation: { label: 'Reflation', color: '#f97316', emoji: '🔥' }, reflation: { label: 'Reflation', color: '#f97316', emoji: '🔥' },
stagflation: { label: 'Stagflation', color: '#eab308', emoji: '⚡' }, stagflation: { label: 'Stagflation', color: '#eab308', emoji: '⚡' },
inflation_shock: { label: 'Inflation Shock', color: '#ef4444', emoji: '💥' }, inflation_shock: { label: 'Inflation Shock', color: '#ef4444', emoji: '💥' },
recession: { label: 'Récession', color: '#8b5cf6', emoji: '📉' }, recession: { label: 'Recession', color: '#8b5cf6', emoji: '📉' },
crise_liquidite: { label: 'Crise Liquidité', color: '#ec4899', emoji: '🚨' }, crise_liquidite: { label: 'Liquidity Crisis', color: '#ec4899', emoji: '🚨' },
incertain: { label: 'Incertain', color: '#64748b', emoji: '❓' }, incertain: { label: 'Uncertain', color: '#64748b', emoji: '❓' },
} }
function ScenarioBadge({ dominant, size = 'sm' }: { dominant: string; size?: 'sm' | 'lg' }) { 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"> <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={clsx('text-[10px] font-bold font-mono', color)}>{pct?.toFixed(1)}%</span>
<span className="text-[8px] text-slate-600">{eur != null ? `${eur}` : ''}</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> </div>
) )
} }
@@ -105,10 +105,10 @@ function KellyCell({ patternId, capital = 10000 }: { patternId: string; capital?
// ── Close Trade Modal ───────────────────────────────────────────────────────── // ── Close Trade Modal ─────────────────────────────────────────────────────────
const CLOSE_REASONS = [ const CLOSE_REASONS = [
{ value: 'target', label: '🎯 Objectif atteint' }, { value: 'target', label: '🎯 Target reached' },
{ value: 'stop_loss', label: '🛑 Stop-loss déclenché' }, { value: 'stop_loss', label: '🛑 Stop-loss triggered' },
{ value: 'signal_reversal', label: '🔄 Signal IA retourné' }, { value: 'signal_reversal', label: '🔄 AI signal reversed' },
{ value: 'manual', label: '✍️ Fermeture manuelle' }, { value: 'manual', label: '✍️ Manual close' },
] ]
function CloseTradeModal({ trade, onClose }: { trade: any; onClose: () => void }) { 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 className="flex items-center justify-between">
<div> <div>
<div className="text-sm font-bold text-white flex items-center gap-2"> <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>
<div className="text-xs text-slate-500 mt-0.5"> <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>
</div> </div>
<button onClick={onClose} className="text-slate-600 hover:text-slate-400"><X className="w-4 h-4" /></button> <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' trade.alert_type === 'target_reached'
? 'bg-emerald-900/30 border-emerald-700/40 text-emerald-300' ? 'bg-emerald-900/30 border-emerald-700/40 text-emerald-300'
: 'bg-red-900/30 border-red-700/40 text-red-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 '} {' — P&L actuel '}
<span className="font-mono">{trade.pnl_pct >= 0 ? '+' : ''}{trade.pnl_pct?.toFixed(2)}%</span> <span className="font-mono">{trade.pnl_pct >= 0 ? '+' : ''}{trade.pnl_pct?.toFixed(2)}%</span>
</div> </div>
@@ -164,24 +164,24 @@ function CloseTradeModal({ trade, onClose }: { trade: any; onClose: () => void }
<div className="space-y-3"> <div className="space-y-3">
<div> <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 <input
type="number" step="0.01" type="number" step="0.01"
value={closePrice} value={closePrice}
onChange={e => setClosePrice(e.target.value)} 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" 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 && ( {pnlPreview !== null && (
<div className={clsx('text-xs mt-1 font-mono font-bold', <div className={clsx('text-xs mt-1 font-mono font-bold',
pnlPreview >= 0 ? 'text-emerald-400' : 'text-red-400')}> pnlPreview >= 0 ? 'text-emerald-400' : 'text-red-400')}>
P&L réalisé : {pnlPreview >= 0 ? '+' : ''}{pnlPreview}% Realized P&L: {pnlPreview >= 0 ? '+' : ''}{pnlPreview}%
</div> </div>
)} )}
</div> </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 <select
value={reason} value={reason}
onChange={e => setReason(e.target.value)} onChange={e => setReason(e.target.value)}
@@ -194,24 +194,24 @@ function CloseTradeModal({ trade, onClose }: { trade: any; onClose: () => void }
</div> </div>
<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 <input
type="text" type="text"
value={note} value={note}
onChange={e => setNote(e.target.value)} 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" 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} maxLength={200}
/> />
</div> </div>
</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"> <div className="flex gap-3 pt-1">
<button onClick={onClose} <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"> 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>
<button <button
onClick={handleSubmit} 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" 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" /> <Lock className="w-3.5 h-3.5" />
{isPending ? 'Fermeture…' : 'Confirmer la fermeture'} {isPending ? 'Closing…' : 'Confirm close'}
</button> </button>
</div> </div>
</div> </div>
@@ -230,9 +230,9 @@ function CloseTradeModal({ trade, onClose }: { trade: any; onClose: () => void }
// ── Shared filter bar ──────────────────────────────────────────────────────── // ── Shared filter bar ────────────────────────────────────────────────────────
const DIRECTIONS = [ const DIRECTIONS = [
{ key: 'all', label: 'Tous' }, { key: 'all', label: 'All' },
{ key: 'bullish', label: '🐂 Haussier' }, { key: 'bullish', label: '🐂 Bullish' },
{ key: 'bearish', label: '🐻 Baissier' }, { key: 'bearish', label: '🐻 Bearish' },
] ]
function _isBearishStr(strategy: string) { function _isBearishStr(strategy: string) {
@@ -314,7 +314,7 @@ function FilterBar({ search, onSearch, assetClass, onAssetClass, direction, onDi
type="text" type="text"
value={search} value={search}
onChange={e => onSearch(e.target.value)} 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" 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 && ( {search && (
@@ -353,9 +353,9 @@ function FilterBar({ search, onSearch, assetClass, onAssetClass, direction, onDi
{onPnlFilter && ( {onPnlFilter && (
<div className="flex items-center gap-0.5 bg-dark-700 rounded p-0.5"> <div className="flex items-center gap-0.5 bg-dark-700 rounded p-0.5">
{[ {[
{ key: 'all', label: 'Tous' }, { key: 'all', label: 'All' },
{ key: 'winners', label: '✓ Gagnants' }, { key: 'winners', label: '✓ Winners' },
{ key: 'losers', label: '✗ Perdants' }, { key: 'losers', label: '✗ Losers' },
].map(p => ( ].map(p => (
<button key={p.key} onClick={() => onPnlFilter(p.key)} <button key={p.key} onClick={() => onPnlFilter(p.key)}
className={clsx('px-2 py-1 rounded text-xs transition-colors', { 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 }> = { 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' }, stop_loss: { label: '🛑 Stop-loss', color: 'text-red-400' },
signal_reversal: { label: '🔄 Signal IA', color: 'text-blue-400' }, signal_reversal: { label: '🔄 AI Signal', color: 'text-blue-400' },
manual: { label: '✍️ Manuel', color: 'text-slate-400' }, manual: { label: '✍️ Manual', color: 'text-slate-400' },
} }
return ( return (
@@ -410,10 +410,10 @@ function ClosedTradesSection({ days }: { days: number }) {
{allTrades.length > 0 && ( {allTrades.length > 0 && (
<div className="grid grid-cols-4 gap-3"> <div className="grid grid-cols-4 gap-3">
{[ {[
{ label: 'Trades fermés', value: stats.total ?? 0, sub: '', color: 'text-white' }, { label: 'Closed trades', 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: 'Win rate', 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: '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: '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: '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 => ( ].map(s => (
<div key={s.label} className="card text-center py-3"> <div key={s.label} className="card text-center py-3">
<div className={clsx('text-lg font-bold font-mono', s.color)}>{s.value}</div> <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="flex items-center justify-between flex-wrap gap-2">
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide"> <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> </div>
<button onClick={() => refetch()} disabled={isFetching} <button onClick={() => refetch()} disabled={isFetching}
className="text-slate-600 hover:text-slate-400 disabled:opacity-40"> className="text-slate-600 hover:text-slate-400 disabled:opacity-40">
@@ -446,11 +446,11 @@ function ClosedTradesSection({ days }: { days: number }) {
) : allTrades.length === 0 ? ( ) : allTrades.length === 0 ? (
<div className="card text-center py-12 text-slate-600 text-sm"> <div className="card text-center py-12 text-slate-600 text-sm">
<Lock className="w-8 h-8 mx-auto mb-2 opacity-20" /> <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> </div>
) : trades.length === 0 ? ( ) : trades.length === 0 ? (
<div className="card text-center py-8 text-slate-600 text-sm"> <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>
) : ( ) : (
<div className="overflow-x-auto rounded-lg border border-slate-700/40"> <div className="overflow-x-auto rounded-lg border border-slate-700/40">
@@ -458,15 +458,15 @@ function ClosedTradesSection({ days }: { days: number }) {
<thead> <thead>
<tr className="text-slate-600 border-b border-slate-700/40"> <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">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-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">Entry</th>
<th className="text-right px-3 py-2 font-medium">Sortie</th> <th className="text-right px-3 py-2 font-medium">Exit</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">Entry date</th>
<th className="text-right px-3 py-2 font-medium">Date sortie</th> <th className="text-right px-3 py-2 font-medium">Close date</th>
<th className="text-right px-3 py-2 font-medium">Durée</th> <th className="text-right px-3 py-2 font-medium">Duration</th>
<th className="text-right px-3 py-2 font-medium">P&L réalisé</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">Motif</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="text-left px-3 py-2 font-medium">Note</th>
<th className="px-2 py-2 w-8"></th> <th className="px-2 py-2 w-8"></th>
</tr> </tr>
@@ -523,7 +523,7 @@ function ClosedTradesSection({ days }: { days: number }) {
disabled={deleting} 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" 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>
<button onClick={() => setConfirmDeleteId(null)} className="text-slate-600 hover:text-slate-400"> <button onClick={() => setConfirmDeleteId(null)} className="text-slate-600 hover:text-slate-400">
<X className="w-3 h-3" /> <X className="w-3 h-3" />
@@ -533,7 +533,7 @@ function ClosedTradesSection({ days }: { days: number }) {
<button <button
onClick={() => setConfirmDeleteId(t.id)} onClick={() => setConfirmDeleteId(t.id)}
className="p-1 rounded text-slate-700 hover:text-red-400 hover:bg-red-900/20 transition-colors" 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" /> <Trash2 className="w-3.5 h-3.5" />
</button> </button>
@@ -618,27 +618,27 @@ function IBKRTicket({ strategy, underlying, strikeGuidance, expiryDays, entryPri
return ( return (
<div className="bg-blue-950/20 border border-blue-700/30 rounded p-3 space-y-2.5"> <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"> <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 <Terminal className="w-3 h-3" /> IBKR Ticket
{!entryPrice && <span className="ml-auto text-slate-600 font-normal normal-case">Prix non disponible vérifier dans IBKR</span>} {!entryPrice && <span className="ml-auto text-slate-600 font-normal normal-case">Price unavailable check in IBKR</span>}
</div> </div>
<div className="grid grid-cols-3 gap-3 text-[10px]"> <div className="grid grid-cols-3 gap-3 text-[10px]">
<div> <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-white font-mono font-bold text-sm">{underlying}</div>
<div className="text-slate-600 text-[9px]">Security Type: OPT</div> <div className="text-slate-600 text-[9px]">Security Type: OPT</div>
</div> </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> <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> <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"> <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>
<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> </div>
@@ -646,7 +646,7 @@ function IBKRTicket({ strategy, underlying, strikeGuidance, expiryDays, entryPri
{legs.map((leg, i) => ( {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]"> <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={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={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-slate-600">Strike</span>
<span className="text-yellow-300 font-bold">{fmtStrike(leg.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 className="grid grid-cols-3 gap-3 text-[10px]">
<div> <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-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> <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-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> <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 className="text-emerald-400 font-mono">{targetGain ? `+${targetGain.toFixed(0)}` : '—'}</div>
</div> </div>
</div> </div>
@@ -685,7 +685,7 @@ function MacroHistorySection({ days }: { days: number }) {
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide"> <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> </div>
<button onClick={() => refetch()} disabled={isFetching} <button onClick={() => refetch()} disabled={isFetching}
className="text-slate-600 hover:text-slate-400 disabled:opacity-40"> className="text-slate-600 hover:text-slate-400 disabled:opacity-40">
@@ -698,7 +698,7 @@ function MacroHistorySection({ days }: { days: number }) {
) : history.length === 0 ? ( ) : history.length === 0 ? (
<div className="card text-center py-10 text-slate-600 text-sm"> <div className="card text-center py-10 text-slate-600 text-sm">
<Activity className="w-8 h-8 mx-auto mb-2 opacity-20" /> <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>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
@@ -714,7 +714,7 @@ function MacroHistorySection({ days }: { days: number }) {
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
{isTransition && ( {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"> <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> </span>
)} )}
<ScenarioBadge dominant={entry.dominant} size="lg" /> <ScenarioBadge dominant={entry.dominant} size="lg" />
@@ -800,7 +800,7 @@ function PostmortemPanel({ tradeId, onClose }: { tradeId: number; onClose: () =>
{/* Score history sparkline */} {/* Score history sparkline */}
{score_history?.length > 0 && ( {score_history?.length > 0 && (
<div className="flex items-center gap-3"> <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"> <div className="flex items-center gap-1.5 flex-wrap">
{[...score_history].reverse().map((h: any, i: number) => ( {[...score_history].reverse().map((h: any, i: number) => (
<div key={i} className="flex flex-col items-center gap-0.5"> <div key={i} className="flex flex-col items-center gap-0.5">
@@ -817,7 +817,7 @@ function PostmortemPanel({ tradeId, onClose }: { tradeId: number; onClose: () =>
</div> </div>
{score_history.length > 0 && ( {score_history.length > 0 && (
<span className="text-slate-600 ml-auto shrink-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> </span>
)} )}
</div> </div>
@@ -830,7 +830,7 @@ function PostmortemPanel({ tradeId, onClose }: { tradeId: number; onClose: () =>
onClick={() => setShowScoring(v => !v)} onClick={() => setShowScoring(v => !v)}
> >
{showScoring ? <ChevronUp className="w-3.5 h-3.5" /> : <ChevronDown className="w-3.5 h-3.5" />} {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>} {scOut.key_catalyst && <span className="text-slate-600 font-normal ml-1">· {scOut.key_catalyst}</span>}
</button> </button>
{showScoring && ( {showScoring && (
@@ -838,13 +838,13 @@ function PostmortemPanel({ tradeId, onClose }: { tradeId: number; onClose: () =>
{/* Regime / bias */} {/* Regime / bias */}
<div className="flex flex-wrap gap-3 text-[11px] text-slate-500"> <div className="flex flex-wrap gap-3 text-[11px] text-slate-500">
{sc?.macro_dominant && ( {sc?.macro_dominant && (
<span>Régime : <ScenarioBadge dominant={sc.macro_dominant} /></span> <span>Regime: <ScenarioBadge dominant={sc.macro_dominant} /></span>
)} )}
{scCtx.asset_bias && ( {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 && ( {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> </div>
{/* Buckets */} {/* Buckets */}
@@ -885,7 +885,7 @@ function PostmortemPanel({ tradeId, onClose }: { tradeId: number; onClose: () =>
onClick={() => setShowSuggestion(v => !v)} onClick={() => setShowSuggestion(v => !v)}
> >
{showSuggestion ? <ChevronUp className="w-3.5 h-3.5" /> : <ChevronDown className="w-3.5 h-3.5" />} {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> </button>
{showSuggestion && ( {showSuggestion && (
<div className="pl-5 space-y-1.5 text-[11px] text-slate-400"> <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>} {sgOut.description && <p className="text-slate-500">{sgOut.description}</p>}
{sg.macro_dominant && ( {sg.macro_dominant && (
<div className="flex gap-3 mt-1"> <div className="flex gap-3 mt-1">
<span>Créé sous : <ScenarioBadge dominant={sg.macro_dominant} /></span> <span>Created under: <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>} {sg.geo_score != null && <span>Geo: <span className="font-mono text-slate-300">{sg.geo_score}/100</span></span>}
</div> </div>
)} )}
</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" 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')} /> <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> </button>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center gap-2 text-blue-400 font-semibold"> <div className="flex items-center gap-2 text-blue-400 font-semibold">
<Brain className="w-3.5 h-3.5" /> <Brain className="w-3.5 h-3.5" />
Analyse GPT-4o GPT-4o Analysis
</div> </div>
{[ {[
{ key: 'diagnostic', label: 'Diagnostic', color: 'text-slate-200' }, { key: 'diagnostic', label: 'Diagnostic', color: 'text-slate-200' },
{ key: 'what_worked', label: 'Ce qui a marché', color: 'text-emerald-400' }, { key: 'what_worked', label: 'What worked', color: 'text-emerald-400' },
{ key: 'what_missed', label: 'Ce qui a manqué', color: 'text-red-400' }, { key: 'what_missed', label: 'What was missed', color: 'text-red-400' },
{ key: 'regime_alignment', label: 'Alignement régime', color: 'text-yellow-400' }, { key: 'regime_alignment', label: 'Regime alignment', color: 'text-yellow-400' },
{ key: 'contra_assessment', label: 'Contra-signals', color: 'text-orange-400' }, { key: 'contra_assessment', label: 'Contra-signals', color: 'text-orange-400' },
{ key: 'lesson', label: 'Leçon', color: 'text-blue-300' }, { key: 'lesson', label: 'Lesson', color: 'text-blue-300' },
{ key: 'next_cycle', label: 'Prochain cycle', color: 'text-purple-400' }, { key: 'next_cycle', label: 'Next cycle', color: 'text-purple-400' },
].map(({ key, label, color }) => ].map(({ key, label, color }) =>
analysis[key] ? ( analysis[key] ? (
<div key={key}> <div key={key}>
@@ -974,10 +974,10 @@ function TradeMtmSection({ days }: { days: number }) {
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center justify-between gap-4 flex-wrap"> <div className="flex items-center justify-between gap-4 flex-wrap">
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide"> <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 && ( {avgPnl != null && (
<span className={clsx('ml-2 font-bold', avgPnl >= 0 ? 'text-emerald-400' : 'text-red-400')}> <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> </span>
)} )}
</div> </div>
@@ -1015,8 +1015,8 @@ function TradeMtmSection({ days }: { days: number }) {
<div className="card text-center py-10 text-slate-600 text-sm"> <div className="card text-center py-10 text-slate-600 text-sm">
<TrendingUp className="w-8 h-8 mx-auto mb-2 opacity-20" /> <TrendingUp className="w-8 h-8 mx-auto mb-2 opacity-20" />
{allTrades.length === 0 {allTrades.length === 0
? 'Aucun trade ouvert scorer des patterns pour commencer le suivi' ? 'No open trades — score patterns to start tracking'
: 'Aucun résultat pour ces filtres'} : 'No results for these filters'}
</div> </div>
) : ( ) : (
<div className="overflow-x-auto rounded-lg border border-slate-700/40"> <div className="overflow-x-auto rounded-lg border border-slate-700/40">
@@ -1024,20 +1024,20 @@ function TradeMtmSection({ days }: { days: number }) {
<thead> <thead>
<tr className="text-slate-600 border-b border-slate-700/40"> <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">Pattern</th>
<th className="text-left px-3 py-2 font-medium">Profil</th> <th className="text-left px-3 py-2 font-medium">Profile</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-right px-3 py-2 font-medium">Strike</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-right px-3 py-2 font-medium">DTE</th>
<th className="text-left px-3 py-2 font-medium">Ticker</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">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">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">Net EV</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">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">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">Entry price</th>
<th className="text-right px-3 py-2 font-medium">Prix actuel</th> <th className="text-right px-3 py-2 font-medium">Current price</th>
<th className="text-right px-3 py-2 font-medium">Cible/Stop</th> <th className="text-right px-3 py-2 font-medium">Target/Stop</th>
<th className="text-right px-3 py-2 font-medium">Maturité</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">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">Kelly</th>
<th className="text-right px-3 py-2 font-medium">P&L</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 ?? '—'} {t.strike_guidance ?? '—'}
</td> </td>
<td className="px-3 py-2 text-right font-mono text-[11px] text-slate-400"> <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>
<td className="px-3 py-2 font-mono"> <td className="px-3 py-2 font-mono">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
@@ -1082,7 +1082,7 @@ function TradeMtmSection({ days }: { days: number }) {
{t.price_warning && ( {t.price_warning && (
<span <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" 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'} {t.price_warning === 'no_price_data' ? 'NO DATA' : t.price_warning === 'no_entry_price' ? 'NO ENTRY' : 'NO LIVE'}
</span> </span>
@@ -1167,7 +1167,7 @@ function TradeMtmSection({ days }: { days: number }) {
{t.maturity.emoji} {t.maturity.label} {t.maturity.emoji} {t.maturity.label}
</span> </span>
<span className="text-[9px] text-slate-600 font-mono"> <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> </span>
<div className="w-16 h-1 bg-slate-700 rounded-full overflow-hidden"> <div className="w-16 h-1 bg-slate-700 rounded-full overflow-hidden">
<div <div
@@ -1183,7 +1183,7 @@ function TradeMtmSection({ days }: { days: number }) {
</div> </div>
) : ( ) : (
<span className="text-slate-700 text-[11px]"> <span className="text-slate-700 text-[11px]">
{t.days_held != null ? `${t.days_held}j` : '—'} {t.days_held != null ? `${t.days_held}d` : '—'}
</span> </span>
)} )}
</td> </td>
@@ -1219,7 +1219,7 @@ function TradeMtmSection({ days }: { days: number }) {
? 'text-amber-400 bg-amber-900/30 hover:bg-amber-900/50' ? 'text-amber-400 bg-amber-900/30 hover:bg-amber-900/50'
: 'text-slate-700 hover:text-amber-400 hover:bg-amber-900/20' : '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" /> <Lock className="w-3.5 h-3.5" />
</button> </button>
@@ -1265,7 +1265,7 @@ function GeoHistorySection({ days }: { days: number }) {
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide"> <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> </div>
<button onClick={() => refetch()} disabled={isFetching} <button onClick={() => refetch()} disabled={isFetching}
className="text-slate-600 hover:text-slate-400 disabled:opacity-40"> className="text-slate-600 hover:text-slate-400 disabled:opacity-40">
@@ -1278,7 +1278,7 @@ function GeoHistorySection({ days }: { days: number }) {
) : history.length === 0 ? ( ) : history.length === 0 ? (
<div className="card text-center py-10 text-slate-600 text-sm"> <div className="card text-center py-10 text-slate-600 text-sm">
<AlertTriangle className="w-8 h-8 mx-auto mb-2 opacity-20" /> <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>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
@@ -1348,7 +1348,7 @@ function CyclesSection() {
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide"> <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>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
@@ -1356,7 +1356,7 @@ function CyclesSection() {
disabled={triggering} 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"> 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')} /> <Zap className={clsx('w-3 h-3', triggering && 'animate-pulse')} />
{triggering ? 'Lancement...' : 'Lancer un cycle'} {triggering ? 'Launching...' : 'Run a cycle'}
</button> </button>
<button onClick={() => refetch()} disabled={isFetching} <button onClick={() => refetch()} disabled={isFetching}
className="text-slate-600 hover:text-slate-400 disabled:opacity-40"> className="text-slate-600 hover:text-slate-400 disabled:opacity-40">
@@ -1370,7 +1370,7 @@ function CyclesSection() {
) : runs.length === 0 ? ( ) : runs.length === 0 ? (
<div className="card text-center py-10 text-slate-600 text-sm"> <div className="card text-center py-10 text-slate-600 text-sm">
<Zap className="w-8 h-8 mx-auto mb-2 opacity-20" /> <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>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
@@ -1392,16 +1392,16 @@ function CyclesSection() {
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<span className="text-xs text-slate-300 font-semibold">{ts} UTC</span> <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')}> <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> </span>
{run.dominant_regime && <ScenarioBadge dominant={run.dominant_regime} />} {run.dominant_regime && <ScenarioBadge dominant={run.dominant_regime} />}
{duration != null && <span className="text-[10px] text-slate-600">{duration}s</span>} {duration != null && <span className="text-[10px] text-slate-600">{duration}s</span>}
</div> </div>
<div className="flex gap-3 mt-1 text-[11px] text-slate-500 flex-wrap"> <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_suggested} suggested</span>
<span>✚ {run.patterns_added} ajoutés</span> <span> {run.patterns_added} added</span>
<span>🎯 {run.patterns_scored} scorés</span> <span>🎯 {run.patterns_scored} scored</span>
{run.geo_score != null && <span>🌐 Géo {run.geo_score}/100</span>} {run.geo_score != null && <span>🌐 Geo {run.geo_score}/100</span>}
</div> </div>
</div> </div>
</div> </div>
@@ -1409,7 +1409,7 @@ function CyclesSection() {
{commentary && ( {commentary && (
<div className="bg-blue-900/10 border border-blue-700/20 rounded p-3 space-y-2"> <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"> <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> </div>
<p className="text-xs text-slate-300 leading-relaxed">{commentary.commentary}</p> <p className="text-xs text-slate-300 leading-relaxed">{commentary.commentary}</p>
{commentary.key_risk && ( {commentary.key_risk && (
@@ -1419,13 +1419,13 @@ function CyclesSection() {
)} )}
{commentary.top_pattern && ( {commentary.top_pattern && (
<div className="text-[11px] text-emerald-400/80"> <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> </div>
)} )}
{commentary.lessons_from_report ? ( {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"> <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" /> <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 && ( {commentary.lessons_headline && (
<span className="text-slate-600 ml-1">· {commentary.lessons_headline}</span> <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"> <div className="flex items-center gap-1.5 text-[10px] text-slate-700 italic">
<BookOpen className="w-3 h-3 shrink-0" /> <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>
)} )}
</div> </div>
)} )}
{!commentary && ok && ( {!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> </div>
) )
@@ -1476,8 +1476,8 @@ function SimPortfolioRiskPanel() {
return ( return (
<div className="card text-center py-8 text-slate-500"> <div className="card text-center py-8 text-slate-500">
<PieChart className="w-8 h-8 mx-auto mb-2 opacity-20" /> <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-sm">No open positions in the simulated portfolio</div>
<div className="text-xs mt-1">Les trades logués apparaîtront ici après le prochain cycle</div> <div className="text-xs mt-1">Logged trades will appear here after the next cycle</div>
</div> </div>
) )
} }
@@ -1497,11 +1497,11 @@ function SimPortfolioRiskPanel() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<PieChart className="w-4 h-4 text-blue-400" /> <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-sm font-semibold text-white">Simulated Portfolio Risk Analysis</span>
<span className="text-xs text-slate-500">{risk.open_count} positions ouvertes</span> <span className="text-xs text-slate-500">{risk.open_count} open positions</span>
{dangers.length > 0 && ( {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"> <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> </span>
)} )}
</div> </div>
@@ -1512,7 +1512,7 @@ function SimPortfolioRiskPanel() {
{/* Asset class concentration bars */} {/* Asset class concentration bars */}
<div className="card"> <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"> <div className="space-y-2">
{Object.entries(concentration) {Object.entries(concentration)
.sort(([, a]: any, [, b]: any) => b.pct - a.pct) .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" /> <ShieldAlert className="w-4 h-4 text-red-400 shrink-0 mt-0.5" />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="text-xs font-semibold text-red-300 mb-1"> <div className="text-xs font-semibold text-red-300 mb-1">
Conflit directionnel {c.underlying} Directional conflict {c.underlying}
</div> </div>
<div className="space-y-0.5"> <div className="space-y-0.5">
{c.trades.map((t: any) => ( {c.trades.map((t: any) => (
@@ -1589,7 +1589,7 @@ function SimPortfolioRiskPanel() {
{warnings.length > 0 && ( {warnings.length > 0 && (
<div className="card border-amber-700/30 bg-amber-900/10"> <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"> <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>
<div className="space-y-1"> <div className="space-y-1">
{warnings.map((a: any, i: number) => ( {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="card border-blue-700/30 bg-blue-900/10">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<div className="text-xs font-semibold text-blue-300 flex items-center gap-1.5"> <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> </div>
{aiTs && ( {aiTs && (
<span className="text-[10px] text-slate-600">{aiTs.slice(0, 16).replace('T', ' ')}</span> <span className="text-[10px] text-slate-600">{aiTs.slice(0, 16).replace('T', ' ')}</span>
@@ -1650,7 +1650,7 @@ function SimPortfolioRiskPanel() {
{alerts.length === 0 && ( {alerts.length === 0 && (
<div className="card border-emerald-700/30 bg-emerald-900/10 text-center py-3"> <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" /> <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>
)} )}
</div> </div>
@@ -1658,13 +1658,13 @@ function SimPortfolioRiskPanel() {
} }
const TABS = [ const TABS = [
{ key: 'cycles', label: 'Cycles IA', icon: Zap }, { key: 'cycles', label: 'AI Cycles', icon: Zap },
{ key: 'macro', label: 'Régimes Macro', icon: Activity }, { key: 'macro', label: 'Macro Regimes', icon: Activity },
{ key: 'ideas', label: 'Idées de trade', icon: Target }, { key: 'ideas', label: 'Trade Ideas', icon: Target },
{ key: 'mtm', label: 'Ouverts', icon: TrendingUp }, { key: 'mtm', label: 'Open', icon: TrendingUp },
{ key: 'closed', label: 'Fermés', icon: Lock }, { key: 'closed', label: 'Closed', icon: Lock },
{ key: 'geo', label: 'Alertes Géo', icon: AlertTriangle }, { key: 'geo', label: 'Geo Alerts', icon: AlertTriangle },
{ key: 'skipped', label: 'Non loggés', icon: XCircle }, { key: 'skipped', label: 'Not logged', icon: XCircle },
] as const ] as const
function SkippedTradesSection({ days }: { days: number }) { function SkippedTradesSection({ days }: { days: number }) {
@@ -1706,7 +1706,7 @@ function SkippedTradesSection({ days }: { days: number }) {
return ( return (
<div className="card text-center py-8 text-slate-500 text-sm"> <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" /> <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> </div>
) )
} }
@@ -1716,9 +1716,9 @@ function SkippedTradesSection({ days }: { days: number }) {
<div className="card"> <div className="card">
<div className="section-title flex items-center gap-1 mb-3"> <div className="section-title flex items-center gap-1 mb-3">
<XCircle className="w-3 h-3 text-amber-400" /> <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"> <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> </span>
</div> </div>
@@ -1743,7 +1743,7 @@ function SkippedTradesSection({ days }: { days: number }) {
<button onClick={() => setReasonFilter('all')} <button onClick={() => setReasonFilter('all')}
className={clsx('px-2 py-1 rounded text-xs transition-colors', 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')}> reasonFilter === 'all' ? 'bg-slate-600 text-white' : 'text-slate-400 hover:text-slate-200')}>
Tous All
</button> </button>
{allReasons.map(r => ( {allReasons.map(r => (
<button key={r} onClick={() => setReasonFilter(r)} <button key={r} onClick={() => setReasonFilter(r)}
@@ -1757,7 +1757,7 @@ function SkippedTradesSection({ days }: { days: number }) {
</div> </div>
{trades.length === 0 ? ( {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"> <div className="overflow-x-auto rounded-lg border border-slate-700/40">
<table className="w-full text-xs"> <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"> <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="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">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">Score</th>
<th className="px-2 py-2 text-right">Gain attendu</th> <th className="px-2 py-2 text-right">Expected gain</th>
<th className="px-2 py-2 text-left">Classe</th> <th className="px-2 py-2 text-left">Class</th>
<th className="pr-3 py-2 text-left">Raison du skip</th> <th className="pr-3 py-2 text-left">Skip reason</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-slate-800/40"> <tbody className="divide-y divide-slate-800/40">
@@ -1827,7 +1827,7 @@ export default function JournalDeBord() {
setResetting(true) setResetting(true)
try { try {
await api.delete('/journal/reset') await api.delete('/journal/reset')
setResetMsg('Journal réinitialisé') setResetMsg('Journal reset')
setConfirmReset(false) setConfirmReset(false)
// Invalidate all journal queries // Invalidate all journal queries
await qc.invalidateQueries({ queryKey: ['journal-summary'] }) await qc.invalidateQueries({ queryKey: ['journal-summary'] })
@@ -1837,7 +1837,7 @@ export default function JournalDeBord() {
await qc.invalidateQueries({ queryKey: ['cycle-history'] }) await qc.invalidateQueries({ queryKey: ['cycle-history'] })
setTimeout(() => setResetMsg(''), 3000) setTimeout(() => setResetMsg(''), 3000)
} catch { } catch {
setResetMsg('Erreur lors du reset') setResetMsg('Reset error')
setTimeout(() => setResetMsg(''), 3000) setTimeout(() => setResetMsg(''), 3000)
} finally { } finally {
setResetting(false) setResetting(false)
@@ -1850,10 +1850,10 @@ export default function JournalDeBord() {
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div> <div>
<h1 className="text-xl font-bold text-white flex items-center gap-2"> <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> </h1>
<p className="text-xs text-slate-500 mt-0.5"> <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> </p>
</div> </div>
@@ -1866,7 +1866,7 @@ export default function JournalDeBord() {
</span> </span>
)} )}
{confirmReset && !resetting && ( {confirmReset && !resetting && (
<span className="text-xs text-amber-400">Confirmer ?</span> <span className="text-xs text-amber-400">Confirm?</span>
)} )}
<button <button
onClick={confirmReset ? handleReset : () => setConfirmReset(true)} 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' : 'border-slate-700/40 text-slate-600 hover:text-slate-400 hover:border-slate-600'
)}> )}>
<Trash2 className="w-3.5 h-3.5" /> <Trash2 className="w-3.5 h-3.5" />
{resetting ? 'Réinit...' : confirmReset ? 'Effacer tout' : 'Reset journal'} {resetting ? 'Resetting...' : confirmReset ? 'Clear all' : 'Reset journal'}
</button> </button>
</div> </div>
@@ -1891,7 +1891,7 @@ export default function JournalDeBord() {
'bg-blue-600 text-white': days === d, 'bg-blue-600 text-white': days === d,
'text-slate-400 hover:text-slate-200': days !== d, 'text-slate-400 hover:text-slate-200': days !== d,
})}> })}>
{d}j {d}d
</button> </button>
))} ))}
</div> </div>
@@ -1903,14 +1903,14 @@ export default function JournalDeBord() {
<div className="grid grid-cols-4 gap-3"> <div className="grid grid-cols-4 gap-3">
<div className="card text-center"> <div className="card text-center">
<div className="text-2xl font-bold text-white">{s.macro_snapshots ?? 0}</div> <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 && ( {s.current_dominant && (
<div className="mt-1"><ScenarioBadge dominant={s.current_dominant} /></div> <div className="mt-1"><ScenarioBadge dominant={s.current_dominant} /></div>
)} )}
</div> </div>
<div className="card text-center"> <div className="card text-center">
<div className="text-2xl font-bold text-white">{s.regime_transitions?.length ?? 0}</div> <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 && ( {s.regime_transitions?.length > 0 && (
<div className="text-[10px] text-amber-400 mt-1"> <div className="text-[10px] text-amber-400 mt-1">
{s.regime_transitions[0].from} {s.regime_transitions[0].to} {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 ?? 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) : '—'} {s.avg_geo_score != null ? s.avg_geo_score.toFixed(0) : '—'}
</div> </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 && ( {s.max_geo_score != null && (
<div className="text-[10px] text-slate-600 mt-1">max {s.max_geo_score}</div> <div className="text-[10px] text-slate-600 mt-1">max {s.max_geo_score}</div>
)} )}
</div> </div>
<div className="card text-center"> <div className="card text-center">
<div className="text-2xl font-bold text-white">{s.trade_entries_logged ?? 0}</div> <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> </div>
{riskAlertCount > 0 && ( {riskAlertCount > 0 && (
<div className="card text-center border-red-700/40 bg-red-900/10 col-span-full"> <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"> <div className="flex items-center justify-center gap-2">
<ShieldAlert className="w-4 h-4 text-red-400" /> <ShieldAlert className="w-4 h-4 text-red-400" />
<span className="text-sm font-semibold text-red-300"> <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>
<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>
</div> </div>
)} )}

View File

@@ -18,12 +18,12 @@ const SCENARIO_ORDER = [
] as const ] as const
const BLOC_INFO: Record<string, { label: string; emoji: string; description: string }> = { 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' }, liquidite: { label: 'Global Liquidity', emoji: '💧', description: 'Market fuel — DXY↓ = loose global conditions; yield curve = recession/recovery signal' },
credit: { label: 'Crédit & Stress', emoji: '⚡', description: 'Thermomètre systémique — HYG (HY, obligataires détectent avant les actions), LQD (IG), VIX' }, credit: { label: 'Credit & Stress', emoji: '⚡', description: 'Systemic thermometer — HYG (HY, bonds lead equities), LQD (IG), VIX' },
energie: { label: 'Énergie', emoji: '⛽', description: 'Premier moteur d\'inflation — Brent↑↑ = stagflation/choc ; Brent↓ = désinflationniste' }, energie: { label: 'Energy', emoji: '⛽', description: 'Primary inflation driver — Brent↑↑ = stagflation/shock; Brent↓ = disinflationary' },
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' }, metaux: { label: 'Strategic Metals', emoji: '🥇', description: 'Copper = "Dr Copper" (global growth); Gold = haven/real rates; Gold/Cu ratio = fear vs expansion bias' },
croissance: { label: 'Croissance mondiale', emoji: '📊', description: 'Cycle réel — S&P, Russell 2000 (breadth risk-on), XLI (proxy ISM mfg), Baltic Dry (secondaire)' }, croissance: { label: 'Global Growth', emoji: '📊', description: 'Real cycle — S&P, Russell 2000 (breadth risk-on), XLI (ISM mfg proxy), Baltic Dry (secondary)' },
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)' }, 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> = { const ASSET_BIAS_COLORS: Record<string, string> = {
@@ -31,12 +31,12 @@ const ASSET_BIAS_COLORS: Record<string, string> = {
'bearish': '#f97316', 'bearish+': '#ef4444', 'defensive': '#f59e0b', 'bearish': '#f97316', 'bearish+': '#ef4444', 'defensive': '#f59e0b',
} }
const ASSET_BIAS_LABELS: Record<string, string> = { const ASSET_BIAS_LABELS: Record<string, string> = {
'bullish+': '★★ Très favorable', 'bullish': '★ Favorable', 'neutral': '→ Neutre', 'bullish+': '★★ Very Favorable', 'bullish': '★ Favorable', 'neutral': '→ Neutral',
'bearish': '✗ favorable', 'bearish+': '✗✗ Contre', 'defensive': '⚠ Défensif', 'bearish': '✗ Unfavorable', 'bearish+': '✗✗ Against', 'defensive': '⚠ Defensive',
} }
const ASSET_CLASS_LABELS: Record<string, string> = { const ASSET_CLASS_LABELS: Record<string, string> = {
energy: 'Énergie ⛽', metals: 'Métaux 🥇', agriculture: 'Agriculture 🌾', energy: 'Energy ⛽', metals: 'Metals 🥇', agriculture: 'Agriculture 🌾',
indices: 'Indices 📊', equities: 'Actions 📈', forex: 'Forex 💱', indices: 'Indices 📊', equities: 'Equities 📈', forex: 'Forex 💱',
} }
function GaugeRow({ g }: { g: MacroGauge }) { function GaugeRow({ g }: { g: MacroGauge }) {
@@ -153,7 +153,7 @@ export default function MacroRegime() {
setSavedNarration(entry) setSavedNarration(entry)
localStorage.setItem('geo-options:macro-narration', JSON.stringify(entry)) localStorage.setItem('geo-options:macro-narration', JSON.stringify(entry))
} catch { } 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) setSavedNarration(entry)
} finally { } finally {
setLoadingNarration(false) setLoadingNarration(false)
@@ -166,10 +166,10 @@ export default function MacroRegime() {
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div> <div>
<h1 className="text-xl font-bold text-white flex items-center gap-2"> <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> </h1>
<p className="text-xs text-slate-500 mt-0.5"> <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> </p>
</div> </div>
<div className="flex items-center gap-3"> <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" 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')} /> <RefreshCw className={clsx('w-3.5 h-3.5', isFetching && 'animate-spin')} />
Actualiser Refresh
</button> </button>
</div> </div>
</div> </div>
{isLoading ? ( {isLoading ? (
<div className="card flex items-center justify-center py-12 text-slate-600 text-sm"> <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> </div>
) : ( ) : (
<> <>
@@ -201,7 +201,7 @@ export default function MacroRegime() {
style={{ borderColor: `${dominantMeta.color}44`, background: `${dominantMeta.color}0d` }}> style={{ borderColor: `${dominantMeta.color}44`, background: `${dominantMeta.color}0d` }}>
<div className="text-4xl">{dominantMeta.emoji}</div> <div className="text-4xl">{dominantMeta.emoji}</div>
<div className="flex-1"> <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-xl font-bold" style={{ color: dominantMeta.color }}>{dominantMeta.label}</div>
<div className="text-xs text-slate-500 mt-1 flex flex-wrap gap-1"> <div className="text-xs text-slate-500 mt-1 flex flex-wrap gap-1">
{(reasons[dominant] ?? []).map((r, i) => ( {(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 }}> <div className="text-3xl font-bold font-mono" style={{ color: dominantMeta.color }}>
{scores[dominant]}% {scores[dominant]}%
</div> </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 && ( {regimeStability > 0 && (
<div className="text-[10px] mt-1 px-1.5 py-0.5 rounded-full text-center" <div className="text-[10px] mt-1 px-1.5 py-0.5 rounded-full text-center"
style={{ background: `${dominantMeta.color}22`, color: dominantMeta.color }}> style={{ background: `${dominantMeta.color}22`, color: dominantMeta.color }}>
stable {regimeStability}j stable {regimeStability}d
</div> </div>
)} )}
{historyDays > 0 && ( {historyDays > 0 && (
<div className="text-[10px] text-slate-700 mt-0.5 text-center"> <div className="text-[10px] text-slate-700 mt-0.5 text-center">
{historyDays}j d'historique {historyDays}d history
</div> </div>
)} )}
</div> </div>
@@ -236,7 +236,7 @@ export default function MacroRegime() {
{/* Left: Gauge blocs */} {/* Left: Gauge blocs */}
<div className="col-span-2 space-y-3"> <div className="col-span-2 space-y-3">
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide"> <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> </div>
{['liquidite', 'credit', 'energie', 'metaux', 'croissance', 'derive'].map(bloc => ( {['liquidite', 'credit', 'energie', 'metaux', 'croissance', 'derive'].map(bloc => (
<BlocCard key={bloc} id={bloc} gauges={byBloc[bloc] ?? []} /> <BlocCard key={bloc} id={bloc} gauges={byBloc[bloc] ?? []} />
@@ -248,7 +248,7 @@ export default function MacroRegime() {
{/* Scenario ranking */} {/* Scenario ranking */}
<div className="card"> <div className="card">
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-3"> <div className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-3">
Classement des scénarios Scenario Ranking
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
{ranked.map(([key, score]) => { {ranked.map(([key, score]) => {
@@ -291,7 +291,7 @@ export default function MacroRegime() {
{dominant !== 'incertain' && Object.keys(dominantBias).length > 0 && ( {dominant !== 'incertain' && Object.keys(dominantBias).length > 0 && (
<div className="card"> <div className="card">
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-3"> <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>
<div className="space-y-1.5"> <div className="space-y-1.5">
{Object.entries(dominantBias).map(([cls, bias]) => { {Object.entries(dominantBias).map(([cls, bias]) => {
@@ -312,7 +312,7 @@ export default function MacroRegime() {
<div className="card"> <div className="card">
<div className="flex items-center justify-between mb-2"> <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"> <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> </div>
{savedNarration?.at && ( {savedNarration?.at && (
<span className="text-[10px] text-slate-700"> <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-300 leading-relaxed mb-3">{savedNarration.text}</p>
) : ( ) : (
<p className="text-xs text-slate-600 leading-relaxed mb-3"> <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> </p>
)} )}
{aiStatus?.enabled ? ( {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" 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 ? ( {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> </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> </div>
{/* Reading order reminder */} {/* Reading order reminder */}
<div className="card bg-dark-700/30"> <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 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>1. <span className="text-slate-500">Liquidity</span> Fed balance sheet, DXY, real rates</div>
<div>2. <span className="text-slate-500">Crédit</span> HY spreads, VIX, MOVE</div> <div>2. <span className="text-slate-500">Credit</span> HY spreads, VIX, MOVE</div>
<div>3. <span className="text-slate-500">Énergie</span> Brent, gaz naturel</div> <div>3. <span className="text-slate-500">Energy</span> Brent, natural gas</div>
<div>4. <span className="text-slate-500">Métaux</span> Or/Cuivre ratio</div> <div>4. <span className="text-slate-500">Metals</span> Gold/Copper ratio</div>
<div>5. <span className="text-slate-500">Synthèse</span> Scénario dominant</div> <div>5. <span className="text-slate-500">Synthesis</span> Dominant scenario</div>
</div> </div>
</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 }[] = [ const ASSET_CLASSES: { key: AssetClass; label: string; emoji: string }[] = [
{ key: 'energy', label: 'Énergie', emoji: '⛽' }, { key: 'energy', label: 'Energy', emoji: '⛽' },
{ key: 'metals', label: 'Métaux', emoji: '🥇' }, { key: 'metals', label: 'Metals', emoji: '🥇' },
{ key: 'agriculture', label: 'Agriculture', emoji: '🌾' }, { key: 'agriculture', label: 'Agriculture', emoji: '🌾' },
{ key: 'indices', label: 'Indices', emoji: '📊' }, { key: 'indices', label: 'Indices', emoji: '📊' },
{ key: 'equities', label: 'Actions', emoji: '📈' }, { key: 'equities', label: 'Equities', emoji: '📈' },
{ key: 'forex', label: 'Forex', emoji: '💱' }, { key: 'forex', label: 'Forex', emoji: '💱' },
] ]
@@ -56,8 +56,8 @@ const PERIOD_OPTIONS = [
{ label: '1M', value: '1mo' }, { label: '1M', value: '1mo' },
{ label: '3M', value: '3mo' }, { label: '3M', value: '3mo' },
{ label: '6M', value: '6mo' }, { label: '6M', value: '6mo' },
{ label: '1A', value: '1y' }, { label: '1Y', value: '1y' },
{ label: '2A', value: '2y' }, { label: '2Y', value: '2y' },
] ]
function PriceChart({ symbol, name }: { symbol: string; name: string }) { function PriceChart({ symbol, name }: { symbol: string; name: string }) {
@@ -127,7 +127,7 @@ function PriceChart({ symbol, name }: { symbol: string; name: string }) {
<Tooltip <Tooltip
contentStyle={{ background: '#0f1623', border: '1px solid #1e2d4d', fontSize: 11 }} contentStyle={{ background: '#0f1623', border: '1px solid #1e2d4d', fontSize: 11 }}
labelStyle={{ color: '#94a3b8' }} labelStyle={{ color: '#94a3b8' }}
formatter={(v: number) => [v.toFixed(4), 'Prix']} formatter={(v: number) => [v.toFixed(4), 'Price']}
/> />
<Area <Area
type="monotone" dataKey="close" type="monotone" dataKey="close"
@@ -139,7 +139,7 @@ function PriceChart({ symbol, name }: { symbol: string; name: string }) {
</ResponsiveContainer> </ResponsiveContainer>
) : ( ) : (
<div className="h-48 flex items-center justify-center text-slate-600 text-xs"> <div className="h-48 flex items-center justify-center text-slate-600 text-xs">
Aucune donnée disponible No data available
</div> </div>
)} )}
</div> </div>
@@ -192,17 +192,17 @@ export default function Markets() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-xl font-bold text-white flex items-center gap-2"> <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> </h1>
<p className="text-xs text-slate-500 mt-0.5"> <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> </p>
</div> </div>
<button <button
onClick={() => refetch()} onClick={() => refetch()}
className="flex items-center gap-1.5 text-xs text-slate-400 hover:text-slate-200 transition-colors" 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> </button>
</div> </div>
@@ -227,7 +227,7 @@ export default function Markets() {
value={searchQuery} value={searchQuery}
onChange={e => setSearchQuery(e.target.value)} onChange={e => setSearchQuery(e.target.value)}
onKeyDown={e => { if (e.key === 'Escape') setSearchQuery('') }} 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" 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 && ( {searchResults.length > 0 && (
@@ -282,7 +282,7 @@ export default function Markets() {
)) ))
) : ( ) : (
<div className="card text-slate-500 text-xs text-center py-6"> <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>
)} )}
</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="card h-full flex items-center justify-center text-slate-600 text-sm">
<div className="text-center"> <div className="text-center">
<BarChart2 className="w-8 h-8 mx-auto mb-2 opacity-30" /> <BarChart2 className="w-8 h-8 mx-auto mb-2 opacity-30" />
<div>Sélectionner un instrument</div> <div>Select an instrument</div>
<div className="text-xs mt-1">pour voir le graphique de prix</div> <div className="text-xs mt-1">to view the price chart</div>
</div> </div>
</div> </div>
)} )}
@@ -305,16 +305,16 @@ export default function Markets() {
{/* Market overview table */} {/* Market overview table */}
<div className="card"> <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"> <div className="overflow-x-auto">
<table className="w-full text-xs"> <table className="w-full text-xs">
<thead> <thead>
<tr className="text-slate-600 border-b border-slate-700/40"> <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">Instrument</th>
<th className="text-left pb-2">Classe</th> <th className="text-left pb-2">Class</th>
<th className="text-right pb-2">Prix</th> <th className="text-right pb-2">Price</th>
<th className="text-right pb-2">Var. 1j</th> <th className="text-right pb-2">1d Chg</th>
<th className="text-right pb-2">Vol. IV (est.)</th> <th className="text-right pb-2">IV Vol (est.)</th>
<th className="text-right pb-2">Signal</th> <th className="text-right pb-2">Signal</th>
</tr> </tr>
</thead> </thead>
@@ -337,9 +337,9 @@ export default function Markets() {
{q.iv ? `${(q.iv * 100).toFixed(1)}%` : '—'} {q.iv ? `${(q.iv * 100).toFixed(1)}%` : '—'}
</td> </td>
<td className="py-1.5 text-right"> <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-green badge"> Bullish</span>
: q.change_pct < -1.5 ? <span className="badge-red badge"> Baissier</span> : q.change_pct < -1.5 ? <span className="badge-red badge"> Bearish</span>
: <span className="badge badge-blue"> Neutre</span>} : <span className="badge badge-blue"> Neutral</span>}
</td> </td>
</tr> </tr>
)) ))

View File

@@ -25,9 +25,9 @@ function ivRankBg(rank: number | null | undefined): string {
function ivSignalLabel(rank: number | null | undefined) { function ivSignalLabel(rank: number | null | undefined) {
if (rank == null) return null 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 >= 80) return { text: 'Sell 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' } if (rank < 20) return { text: 'Buy 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' } return { text: 'Neutral', icon: <Minus className="w-3 h-3" />, cls: 'text-slate-500' }
} }
function StructureBadge({ structure }: { structure: string | null | undefined }) { function StructureBadge({ structure }: { structure: string | null | undefined }) {
@@ -93,7 +93,7 @@ function TickerDetail({ ticker }: { ticker: string }) {
<div> <div>
<div className="text-[9px] text-slate-500 uppercase tracking-wide font-semibold mb-2">Term Structure</div> <div className="text-[9px] text-slate-500 uppercase tracking-wide font-semibold mb-2">Term Structure</div>
<div className="space-y-1.5"> <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 ? ( iv ? (
<div key={label} className="flex items-center gap-2"> <div key={label} className="flex items-center gap-2">
<span className="text-[9px] text-slate-600 w-7 shrink-0">{label}</span> <span className="text-[9px] text-slate-600 w-7 shrink-0">{label}</span>
@@ -130,7 +130,7 @@ function TickerDetail({ ticker }: { ticker: string }) {
)} )}
</div> </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> </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.gamma_bias && <div className="text-[9px] text-amber-500/70">{flow.gamma_bias}</div>}
{flow.unusual_strikes?.length > 0 && ( {flow.unusual_strikes?.length > 0 && (
<div className="mt-1 pt-1 border-t border-slate-700/30"> <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) => ( {flow.unusual_strikes.slice(0, 2).map((s: any) => (
<div key={s.strike} className="text-[8px] font-mono text-slate-500"> <div key={s.strike} className="text-[8px] font-mono text-slate-500">
{s.strike} {s.type.toUpperCase()} OI={s.total_oi.toLocaleString()} {s.strike} {s.type.toUpperCase()} OI={s.total_oi.toLocaleString()}
@@ -161,19 +161,19 @@ function TickerDetail({ ticker }: { ticker: string }) {
)} )}
</div> </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> </div>
{/* History sparkline */} {/* History sparkline */}
{history.length > 4 && ( {history.length > 4 && (
<div className="col-span-3 flex items-center gap-3 pt-1 border-t border-slate-700/20"> <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} /> <IvSparkline history={history} />
<div className="text-[9px] text-slate-600"> <div className="text-[9px] text-slate-600">
min <span className="text-slate-400">{snap.iv_min_52w_pct}%</span> min <span className="text-slate-400">{snap.iv_min_52w_pct}%</span>
{' · '}max <span className="text-slate-400">{snap.iv_max_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>
</div> </div>
)} )}
@@ -203,7 +203,7 @@ function WatchlistRow({ item }: { item: any }) {
<span className={clsx('text-sm font-bold font-mono', ivRankColor(item.iv_rank))}> <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}%` : '—'} {item.iv_current_pct != null ? `${item.iv_source === 'history' ? '~' : ''}${item.iv_current_pct}%` : '—'}
</span> </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>
<div className="flex-1 min-w-0 space-y-1"> <div className="flex-1 min-w-0 space-y-1">
@@ -230,7 +230,7 @@ function WatchlistRow({ item }: { item: any }) {
</div> </div>
)} )}
{item.history_days > 0 && ( {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> </div>
@@ -286,15 +286,15 @@ function WatchlistManager() {
<div className="card space-y-3"> <div className="card space-y-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<List className="w-4 h-4 text-slate-400" /> <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="text-sm font-semibold text-slate-300">IV Watchlist Manager</span>
<span className="ml-auto text-xs text-slate-600">{active.length} tickers actifs</span> <span className="ml-auto text-xs text-slate-600">{active.length} active tickers</span>
</div> </div>
{/* Add ticker */} {/* Add ticker */}
<div className="flex gap-2"> <div className="flex gap-2">
<input <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" 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} value={input}
onChange={e => setInput(e.target.value.toUpperCase())} onChange={e => setInput(e.target.value.toUpperCase())}
onKeyDown={e => e.key === 'Enter' && handleAdd()} 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" 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" /> <Plus className="w-3.5 h-3.5" />
{adding ? 'Ajout...' : 'Ajouter'} {adding ? 'Adding...' : 'Add'}
</button> </button>
</div> </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 */} {/* Active tickers */}
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
@@ -331,12 +331,12 @@ function WatchlistManager() {
<div className="text-[9px] text-slate-700 flex gap-3"> <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-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-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> </div>
{inactive.length > 0 && ( {inactive.length > 0 && (
<button onClick={() => setShowInactive(!showInactive)} className="text-[10px] text-slate-600 hover:text-slate-400"> <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> </button>
)} )}
{showInactive && inactive.map(e => ( {showInactive && inactive.map(e => (
@@ -365,10 +365,10 @@ export default function OptionsLab() {
setBootstrapMsg(null) setBootstrapMsg(null)
try { try {
await api.post('/options-vol/bootstrap-history') 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) setTimeout(() => refetch(), 70_000)
} catch { } catch {
setBootstrapMsg('Erreur lors du bootstrap.') setBootstrapMsg('Error during bootstrap.')
} finally { } finally {
setBootstrapping(false) setBootstrapping(false)
} }
@@ -381,14 +381,14 @@ export default function OptionsLab() {
<div className="p-6 space-y-5"> <div className="p-6 space-y-5">
{needsBootstrap && !bootstrapMsg && ( {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"> <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 <button
onClick={handleBootstrap} onClick={handleBootstrap}
disabled={bootstrapping} 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" 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')} /> <Database className={clsx('w-3.5 h-3.5', bootstrapping && 'animate-pulse')} />
{bootstrapping ? 'Chargement...' : 'Initialiser historique'} {bootstrapping ? 'Loading...' : 'Initialize history'}
</button> </button>
</div> </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" 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')} /> <RefreshCw className={clsx('w-3.5 h-3.5', isFetching && 'animate-spin')} />
{isFetching ? 'Chargement...' : 'Actualiser'} {isFetching ? 'Loading...' : 'Refresh'}
</button> </button>
</div> </div>
@@ -422,22 +422,22 @@ export default function OptionsLab() {
<div className="card bg-red-900/10 border-red-700/20 text-xs"> <div className="card bg-red-900/10 border-red-700/20 text-xs">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<TrendingDown className="w-3.5 h-3.5 text-red-400" /> <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>
<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>
<div className="card bg-blue-900/10 border-blue-700/20 text-xs"> <div className="card bg-blue-900/10 border-blue-700/20 text-xs">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<TrendingUp className="w-3.5 h-3.5 text-blue-400" /> <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>
<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> </div>
<div className="text-[10px] text-slate-600 -mt-2 px-1"> <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">Contango</strong> = short IV &lt; long IV (calm market, good for selling short term) ·
<strong className="text-slate-500"> Backwardation</strong> = IV courte &gt; longue (stress, bon pour acheter de la protection courte) · <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 plus chers que calls (biais baissier institutionnel) <strong className="text-slate-500"> Skew +</strong> = puts more expensive than calls (institutional bearish bias)
</div> </div>
{isLoading ? ( {isLoading ? (
@@ -447,14 +447,14 @@ export default function OptionsLab() {
) : items.length === 0 ? ( ) : items.length === 0 ? (
<div className="card text-center py-12 text-slate-600"> <div className="card text-center py-12 text-slate-600">
<Activity className="w-8 h-8 mx-auto mb-3 opacity-30" /> <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"> <div className="text-xs text-slate-600">
Les ETFs US sont requis (SPY, QQQ, GLD...). US ETFs are required (SPY, QQQ, GLD...).
<br />L'IV Rank nécessite de l'historique — les données s'accumulent à chaque actualisation. <br />IV Rank requires history data accumulates with each refresh.
</div> </div>
<button onClick={() => refetch()} disabled={isFetching} <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"> 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> </button>
</div> </div>
) : ( ) : (
@@ -462,7 +462,7 @@ export default function OptionsLab() {
{sellVol.length > 0 && ( {sellVol.length > 0 && (
<div> <div>
<div className="text-xs font-semibold text-red-400 uppercase tracking-wide mb-2 flex items-center gap-1.5"> <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>
<div className="space-y-1.5">{sellVol.map(item => <WatchlistRow key={item.ticker} item={item} />)}</div> <div className="space-y-1.5">{sellVol.map(item => <WatchlistRow key={item.ticker} item={item} />)}</div>
</div> </div>
@@ -470,7 +470,7 @@ export default function OptionsLab() {
{buyVol.length > 0 && ( {buyVol.length > 0 && (
<div> <div>
<div className="text-xs font-semibold text-blue-400 uppercase tracking-wide mb-2 flex items-center gap-1.5"> <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>
<div className="space-y-1.5">{buyVol.map(item => <WatchlistRow key={item.ticker} item={item} />)}</div> <div className="space-y-1.5">{buyVol.map(item => <WatchlistRow key={item.ticker} item={item} />)}</div>
</div> </div>
@@ -478,14 +478,14 @@ export default function OptionsLab() {
{neutral.length > 0 && ( {neutral.length > 0 && (
<div> <div>
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 flex items-center gap-1.5"> <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>
<div className="space-y-1.5">{neutral.map(item => <WatchlistRow key={item.ticker} item={item} />)}</div> <div className="space-y-1.5">{neutral.map(item => <WatchlistRow key={item.ticker} item={item} />)}</div>
</div> </div>
)} )}
{noData.length > 0 && ( {noData.length > 0 && (
<div> <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 className="space-y-1.5">{noData.map(item => <WatchlistRow key={item.ticker} item={item} />)}</div>
</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 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_CLASSES = ['energy', 'metals', 'agriculture', 'equities', 'indices', 'forex', 'rates']
const ASSET_CLASS_LABELS: Record<string, string> = { const ASSET_CLASS_LABELS: Record<string, string> = {
energy: '⚡ Énergie', metals: '🥇 Métaux', agriculture: '🌾 Agri', energy: '⚡ Energy', metals: '🥇 Metals', agriculture: '🌾 Agri',
equities: '📈 Actions', indices: '📊 Indices', forex: '💱 Forex', rates: '📉 Taux', equities: '📈 Equities', indices: '📊 Indices', forex: '💱 Forex', rates: '📉 Rates',
} }
const TRIGGER_LABELS: Record<string, string> = { const TRIGGER_LABELS: Record<string, string> = {
military: '⚔️ Militaire', sanctions: '🚫 Sanctions', elections: '🗳️ Élections', military: '⚔️ Military', sanctions: '🚫 Sanctions', elections: '🗳️ Elections',
natural_disaster: '🌪️ Catastrophe', health_crisis: '🏥 Santé', natural_disaster: '🌪️ Disaster', health_crisis: '🏥 Health',
resource_scarcity: '⚠️ Ressources', trade_war: '🤝 Commerce', resource_scarcity: '⚠️ Resources', trade_war: '🤝 Trade',
energy: '⚡ Énergie', political_speech: '🎙️ Discours', financial_crisis: '💸 Finance', energy: '⚡ Energy', political_speech: '🎙️ Speech', financial_crisis: '💸 Finance',
} }
const EMPTY_PATTERN = { const EMPTY_PATTERN = {
@@ -35,7 +35,7 @@ const EMPTY_PATTERN = {
function QualityBadge({ score }: { score: number }) { function QualityBadge({ score }: { score: number }) {
const color = score >= 75 ? 'badge-green' : score >= 50 ? 'badge-yellow' : 'badge-red' 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> 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="flex items-center gap-2 mb-0.5 flex-wrap">
<div className="text-sm font-semibold text-white">{p.name}</div> <div className="text-sm font-semibold text-white">{p.name}</div>
{isCustom && <span className="badge badge-purple text-xs">Custom</span>} {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} />} {p.ai_quality_score && <QualityBadge score={p.ai_quality_score} />}
<ReliabilityBadge rel={reliability} /> <ReliabilityBadge rel={reliability} />
</div> </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')}> <div className={clsx('text-sm font-bold', p.expected_move_pct > 0 ? 'positive' : 'negative')}>
{p.expected_move_pct > 0 ? '+' : ''}{p.expected_move_pct}% {p.expected_move_pct > 0 ? '+' : ''}{p.expected_move_pct}%
</div> </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> </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" /> <Edit3 className="w-4 h-4" />
</button> </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" />} {isActive ? <Eye className="w-4 h-4" /> : <EyeOff className="w-4 h-4" />}
</button> </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" /> <Trash2 className="w-4 h-4" />
</button> </button>
</div> </div>
@@ -101,7 +101,7 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
<div className="flex flex-wrap gap-1 mb-2"> <div className="flex flex-wrap gap-1 mb-2">
{similarTo.map((s, i) => ( {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"> <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 className="font-mono">{Math.round(s.similarity * 100)}%</span>
</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> <span className="badge badge-blue text-xs">{p.asset_class}</span>
{aiScore != null ? ( {aiScore != null ? (
<span className={clsx('badge text-xs font-bold', aiScore >= 50 ? 'badge-green' : aiScore >= 25 ? 'badge-yellow' : 'badge-red')}> <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>
) : ( ) : (
<span className="badge badge-purple text-xs">{Math.round(p.probability * 100)}% prob.</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> </div>
<button onClick={() => setExpanded(!expanded)} className="text-xs text-slate-600 hover:text-slate-400"> <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> </button>
{expanded && ( {expanded && (
<div className="mt-3 space-y-2"> <div className="mt-3 space-y-2">
{p.keywords?.length > 0 && ( {p.keywords?.length > 0 && (
<div> <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"> <div className="flex flex-wrap gap-1">
{p.keywords.map((kw: string) => ( {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> <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 && ( {p.historical_instances?.length > 0 && (
<div> <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) => ( {p.historical_instances.map((h: any, i: number) => (
<div key={i} className="card-sm mb-1"> <div key={i} className="card-sm mb-1">
<span className="text-xs text-slate-500">{h.date}</span> <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 && ( {p.suggested_trades?.length > 0 && (
<div> <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) => ( {p.suggested_trades.map((t: any, i: number) => (
<div key={i} className="flex items-center gap-2 text-xs mb-1 flex-wrap"> <div key={i} className="flex items-center gap-2 text-xs mb-1 flex-wrap">
<span className="badge badge-green">{t.strategy}</span> <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) && ( {(p.counter_thesis || p.invalidation_trigger) && (
<div className="card-sm border-red-700/30 bg-red-900/10"> <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"> <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> </div>
{p.counter_thesis && ( {p.counter_thesis && (
<div className="text-xs text-slate-300 mb-1"> <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> </div>
)} )}
{p.invalidation_trigger && ( {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 && ( {p.ai_evaluation && Object.keys(p.ai_evaluation).length > 0 && (
<div className="card-sm border-blue-700/30"> <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 && ( {p.ai_evaluation.strengths?.length > 0 && (
<div className="text-xs text-emerald-400 mb-1"> {p.ai_evaluation.strengths[0]}</div> <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 className="flex items-center justify-between p-4 border-b border-slate-700/30">
<div> <div>
<h2 className="text-base font-semibold text-white flex items-center gap-2"> <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> </h2>
<p className="text-xs text-slate-500 mt-0.5"> <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> </p>
</div> </div>
<button onClick={onClose} className="text-slate-500 hover:text-white"><X className="w-5 h-5" /></button> <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"> <div className="text-center py-12">
<Sparkles className="w-10 h-10 text-blue-400/40 mx-auto mb-3" /> <Sparkles className="w-10 h-10 text-blue-400/40 mx-auto mb-3" />
<p className="text-sm text-slate-400 mb-4"> <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> </p>
<button onClick={() => suggest()} <button onClick={() => suggest()}
className="bg-blue-600 hover:bg-blue-500 text-white px-6 py-2 rounded text-sm font-semibold"> 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> </button>
</div> </div>
)} )}
@@ -260,7 +260,7 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
{isPending && ( {isPending && (
<div className="text-center py-12"> <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> <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> </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')}> <span className={clsx('badge text-xs font-bold', score >= 50 ? 'badge-green' : score >= 25 ? 'badge-yellow' : 'badge-red')}>
{score}/100 IA {score}/100 IA
</span> </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>
</div> </div>
<button <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-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), '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> </button>
</div> </div>
@@ -295,7 +295,7 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
<div className="flex flex-wrap gap-1 mb-2"> <div className="flex flex-wrap gap-1 mb-2">
{similars.map((s, i) => ( {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"> <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 className="font-mono">{Math.round(s.similarity * 100)}%</span>
</span> </span>
))} ))}
@@ -312,7 +312,7 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
{(p.counter_thesis || p.invalidation_trigger) && ( {(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"> <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 && ( {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 && ( {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> <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 && ( {data && suggestions.length === 0 && (
<div className="card text-center py-8 text-slate-500 text-sm"> <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>
)} )}
</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"> <div className="p-4 border-t border-slate-700/30 flex justify-between items-center">
{suggestions.length > 0 && ( {suggestions.length > 0 && (
<span className="text-xs text-slate-500"> <span className="text-xs text-slate-500">
{saved.size}/{suggestions.length} sauvegardé{saved.size > 1 ? 's' : ''} {saved.size}/{suggestions.length} saved
</span> </span>
)} )}
<div className="flex gap-2 ml-auto"> <div className="flex gap-2 ml-auto">
{data && suggestions.length > 0 && ( {data && suggestions.length > 0 && (
<button onClick={() => suggest()} disabled={isPending} <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"> 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>
)} )}
<button onClick={onClose} className="border border-slate-700 text-slate-400 hover:text-slate-200 px-4 py-1.5 rounded text-xs"> <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> </button>
</div> </div>
</div> </div>
@@ -431,18 +431,18 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
{/* AI Suggest from context */} {/* AI Suggest from context */}
{aiStatus?.enabled && ( {aiStatus?.enabled && (
<div className="card border-blue-500/30"> <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"> <div className="flex gap-2">
<textarea <textarea
value={suggestInput} value={suggestInput}
onChange={e => setSuggestInput(e.target.value)} 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} 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" 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} <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"> 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> </button>
</div> </div>
</div> </div>
@@ -451,22 +451,22 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
{/* Form fields */} {/* Form fields */}
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div className="col-span-2"> <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)} <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" /> 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="col-span-2"> <div className="col-span-2">
<label className="text-xs text-slate-500 mb-1 block">Description</label> <label className="text-xs text-slate-500 mb-1 block">Description</label>
<textarea value={form.description} onChange={e => set('description', e.target.value)} <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" /> 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>
</div> </div>
{/* Triggers */} {/* Triggers */}
<div> <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"> <div className="flex flex-wrap gap-1.5">
{TRIGGERS.map(t => ( {TRIGGERS.map(t => (
<button key={t} onClick={() => toggleTrigger(t)} <button key={t} onClick={() => toggleTrigger(t)}
@@ -482,11 +482,11 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
{/* Keywords */} {/* Keywords */}
<div> <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"> <div className="flex gap-2 mb-2">
<input value={kwInput} onChange={e => setKwInput(e.target.value)} <input value={kwInput} onChange={e => setKwInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && addKeyword()} 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" /> 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> <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> </div>
@@ -504,48 +504,48 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
{/* Metrics */} {/* Metrics */}
<div className="grid grid-cols-4 gap-3"> <div className="grid grid-cols-4 gap-3">
<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)} <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"> 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>)} {ASSET_CLASSES.map(a => <option key={a}>{a}</option>)}
</select> </select>
</div> </div>
<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))} <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" /> 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> <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))} <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" /> 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> <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))} <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" /> 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> </div>
{/* Contre-thèse & Invalidation */} {/* Counter-thesis & Invalidation */}
<div className="space-y-2"> <div className="space-y-2">
<div> <div>
<label className="text-xs text-slate-500 mb-1 block flex items-center gap-1"> <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> </label>
<textarea value={form.counter_thesis || ''} onChange={e => set('counter_thesis', e.target.value)} <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" /> 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>
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-3 gap-3">
<div className="col-span-2"> <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)} <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" /> 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>
<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 ?? ''} <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)} onChange={e => set('invalidation_probability', e.target.value ? Number(e.target.value) : undefined)}
placeholder="0.20" 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="card border-blue-700/40">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<div className="text-sm font-semibold text-blue-400 flex items-center gap-1"> <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> </div>
<QualityBadge score={aiResult.quality_score || 0} /> <QualityBadge score={aiResult.quality_score || 0} />
</div> </div>
<div className="grid grid-cols-2 gap-3 text-xs"> <div className="grid grid-cols-2 gap-3 text-xs">
<div> <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) => ( {aiResult.strengths?.map((s: string, i: number) => (
<div key={i} className="text-emerald-400 mb-0.5"> {s}</div> <div key={i} className="text-emerald-400 mb-0.5"> {s}</div>
))} ))}
</div> </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) => ( {aiResult.weaknesses?.map((w: string, i: number) => (
<div key={i} className="text-orange-400 mb-0.5"> {w}</div> <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> </div>
{aiResult.suggested_improvements?.additional_keywords?.length > 0 && ( {aiResult.suggested_improvements?.additional_keywords?.length > 0 && (
<div className="mt-2"> <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"> <div className="flex flex-wrap gap-1">
{aiResult.suggested_improvements.additional_keywords.map((kw: string) => ( {aiResult.suggested_improvements.additional_keywords.map((kw: string) => (
<button key={kw} onClick={() => set('keywords', [...form.keywords, kw])} <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 && ( {aiResult.counter_scenarios?.length > 0 && (
<div className="mt-2"> <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) => ( {aiResult.counter_scenarios.map((s: string, i: number) => (
<div key={i} className="text-xs text-red-400 mb-0.5"> {s}</div> <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} <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"> 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" /> <Brain className="w-3.5 h-3.5" />
{aiLoading ? 'Évaluation IA...' : 'Évaluer avec IA'} {aiLoading ? 'AI Evaluation...' : 'Evaluate with AI'}
</button> </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"> <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>
<button onClick={() => onSave(form)} disabled={!form.name || form.triggers.length === 0} <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"> 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> </button>
</div> </div>
</div> </div>
@@ -725,23 +725,23 @@ export default function PatternEditor() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-xl font-bold text-white flex items-center gap-2"> <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> </h1>
<p className="text-xs text-slate-500 mt-0.5"> <p className="text-xs text-slate-500 mt-0.5">
{patterns?.length ?? 0} patterns ·{' '} {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> </p>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
{aiStatus?.enabled && ( {aiStatus?.enabled && (
<button onClick={() => setShowAiSuggest(true)} <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"> 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>
)} )}
<button onClick={() => { setCreating(true); setEditing(null) }} <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"> 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> </button>
</div> </div>
</div> </div>
@@ -749,7 +749,7 @@ export default function PatternEditor() {
{(creating || editing) && ( {(creating || editing) && (
<div className="card border-blue-500/30"> <div className="card border-blue-500/30">
<div className="text-sm font-semibold text-white mb-4"> <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> </div>
<PatternForm <PatternForm
initial={editing} initial={editing}
@@ -763,9 +763,9 @@ export default function PatternEditor() {
<div className="flex flex-wrap items-center justify-between gap-2"> <div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex gap-1 bg-dark-700 p-1 rounded"> <div className="flex gap-1 bg-dark-700 p-1 rounded">
{[ {[
{ key: 'all', label: `Tous (${patterns?.length ?? 0})` }, { key: 'all', label: `All (${patterns?.length ?? 0})` },
{ key: 'builtin', label: `Intégrés (${patterns?.filter((p: any) => p.source === 'builtin').length ?? 0})` }, { key: 'builtin', label: `Built-in (${patterns?.filter((p: any) => p.source === 'builtin').length ?? 0})` },
{ key: 'custom', label: `Mes patterns (${patterns?.filter((p: any) => p.source === 'custom').length ?? 0})` }, { key: 'custom', label: `My patterns (${patterns?.filter((p: any) => p.source === 'custom').length ?? 0})` },
].map(({ key, label }) => ( ].map(({ key, label }) => (
<button key={key} onClick={() => setTab(key as any)} <button key={key} onClick={() => setTab(key as any)}
className={clsx('px-3 py-1.5 rounded text-sm', { className={clsx('px-3 py-1.5 rounded text-sm', {
@@ -799,9 +799,9 @@ export default function PatternEditor() {
{/* Period filter */} {/* Period filter */}
<div className="flex gap-1 bg-dark-700 p-1 rounded"> <div className="flex gap-1 bg-dark-700 p-1 rounded">
{([ {([
{ key: null, label: 'Tout' }, { key: null, label: 'All' },
{ key: 7, label: '7j' }, { key: 7, label: '7d' },
{ key: 30, label: '30j' }, { key: 30, label: '30d' },
] as const).map(({ key, label }) => ( ] as const).map(({ key, label }) => (
<button key={String(key)} onClick={() => setFilterDays(key)} <button key={String(key)} onClick={() => setFilterDays(key)}
className={clsx('px-2 py-1 rounded text-xs transition-colors', { 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', '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', 'border-slate-700/40 text-slate-500 hover:border-slate-500 hover:text-slate-300': filterAsset !== 'all',
})}> })}>
Tous types All types
</button> </button>
{ASSET_CLASSES.map(ac => ( {ASSET_CLASSES.map(ac => (
<button key={ac} onClick={() => setFilterAsset(filterAsset === ac ? 'all' : ac)} <button key={ac} onClick={() => setFilterAsset(filterAsset === ac ? 'all' : ac)}
@@ -847,7 +847,7 @@ export default function PatternEditor() {
))} ))}
{(filterAsset !== 'all' || filterDays !== null) && ( {(filterAsset !== 'all' || filterDays !== null) && (
<span className="text-xs text-slate-600 ml-1"> <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> </span>
)} )}
</div> </div>
@@ -874,12 +874,12 @@ export default function PatternEditor() {
<Zap className="w-8 h-8 mx-auto mb-2 opacity-20" /> <Zap className="w-8 h-8 mx-auto mb-2 opacity-20" />
<div> <div>
{filterAsset !== 'all' {filterAsset !== 'all'
? `Aucun pattern dans "${ASSET_CLASS_LABELS[filterAsset]}"` ? `No patterns in "${ASSET_CLASS_LABELS[filterAsset]}"`
: filterDays !== null : filterDays !== null
? `Aucun pattern dans les ${filterDays} derniers jours` ? `No patterns in the last ${filterDays} days`
: 'Aucun pattern personnalisé'} : 'No custom patterns'}
</div> </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>
)} )}
</div> </div>

View File

@@ -86,7 +86,7 @@ function AddPositionModal({ prefill, onClose }: AddModalProps) {
onSuccess: onClose, onSuccess: onClose,
onError: (err: unknown) => { onError: (err: unknown) => {
const detail = (err as { response?: { data?: { detail?: string } } })?.response?.data?.detail 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="card w-full max-w-lg max-h-[90vh] overflow-y-auto">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h2 className="text-base font-bold text-white flex items-center gap-2"> <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> </h2>
<button onClick={onClose} className="text-slate-500 hover:text-slate-200"><X className="w-4 h-4" /></button> <button onClick={onClose} className="text-slate-500 hover:text-slate-200"><X className="w-4 h-4" /></button>
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div> <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)} <input value={form.title} onChange={e => set('title', e.target.value)}
placeholder="Ex: Long Call GLD Q3 2026" 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" /> 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> <div>
<label className="text-xs text-slate-500 mb-1 block"> <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> </label>
<input value={form.underlying} onChange={e => { set('underlying', e.target.value.toUpperCase()); setAddError(null) }} <input value={form.underlying} onChange={e => { set('underlying', e.target.value.toUpperCase()); setAddError(null) }}
placeholder="^GSPC, GLD, CL=F..." 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" /> 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> </div>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<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>
<select value={form.strategy} onChange={e => set('strategy', e.target.value)} <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"> 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>)} {STRATEGIES.map(s => <option key={s}>{s}</option>)}
</select> </select>
</div> </div>
<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)} <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"> 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>)} {ASSET_CLASSES.map(s => <option key={s}>{s}</option>)}
@@ -138,7 +138,7 @@ function AddPositionModal({ prefill, onClose }: AddModalProps) {
<div> <div>
<label className="text-xs text-slate-500 mb-1 block">Strike</label> <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)} <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" /> 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> <div>
@@ -150,45 +150,45 @@ function AddPositionModal({ prefill, onClose }: AddModalProps) {
</select> </select>
</div> </div>
<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)} <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" /> 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> </div>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div> <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))} <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" /> 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> <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))} <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" /> 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> </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)} <input value={form.geo_trigger} onChange={e => set('geo_trigger', e.target.value)}
placeholder="Ex: Middle East escalation → oil spike" 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" /> 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> <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)} <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" /> 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>
{isStraddle && ( {isStraddle && (
<div className="bg-blue-900/30 border border-blue-700/30 rounded p-2 text-xs text-blue-300"> <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>
)} )}
<div className="bg-dark-700 rounded p-2 text-xs text-slate-500"> <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 className="font-semibold text-slate-400 mb-1">Simulated IB fees</div>
<div>Options: $0.65/contrat (min $1.00) à l'entrée + sortie</div> <div>Options: $0.65/contract (min $1.00) at entry + exit</div>
<div>1 contrat = <span className="text-white">$0.65</span> · 5 contrats = <span className="text-white">$3.25</span></div> <div>1 contract = <span className="text-white">$0.65</span> · 5 contracts = <span className="text-white">$3.25</span></div>
</div> </div>
{addError && ( {addError && (
<div className="bg-red-900/40 border border-red-700/40 rounded p-2 text-xs text-red-300"> <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} <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"> 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> </button>
</div> </div>
</div> </div>
@@ -233,7 +233,7 @@ function PositionCard({ pos }: { pos: Record<string, any> }) {
<button <button
onClick={() => navigate(`/markets?symbol=${pos.underlying}`)} onClick={() => navigate(`/markets?symbol=${pos.underlying}`)}
className="text-xs text-blue-400 hover:text-blue-300 flex items-center gap-0.5 transition-colors" 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" /> {pos.underlying} <ExternalLink className="w-2.5 h-2.5" />
</button> </button>
@@ -253,7 +253,7 @@ function PositionCard({ pos }: { pos: Record<string, any> }) {
</div> </div>
</div> </div>
<button onClick={() => deletePos(pos.id)} disabled={deleting} <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" /> <Trash2 className="w-3.5 h-3.5" />
</button> </button>
</div> </div>
@@ -262,29 +262,29 @@ function PositionCard({ pos }: { pos: Record<string, any> }) {
{/* KPI grid */} {/* KPI grid */}
<div className="grid grid-cols-4 gap-2 mb-3"> <div className="grid grid-cols-4 gap-2 mb-3">
<div className="card-sm text-center"> <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 className="text-xs text-white font-mono mt-0.5">{entryRef.toFixed(2)}</div>
</div> </div>
<div className="card-sm text-center"> <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"> <div className="text-xs text-white font-mono mt-0.5">
{pos.current_value != null ? `${pos.current_value.toFixed(2)}` : '—'} {pos.current_value != null ? `${pos.current_value.toFixed(2)}` : '—'}
</div> </div>
</div> </div>
<div className="card-sm text-center"> <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"> <div className="text-xs text-white font-mono mt-0.5">
{pos.current_underlying != null ? `$${pos.current_underlying}` : '—'} {pos.current_underlying != null ? `$${pos.current_underlying}` : '—'}
</div> </div>
</div> </div>
<div className="card-sm text-center"> <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', { <div className={clsx('text-xs font-mono mt-0.5', {
'text-red-400': (pos.days_remaining ?? 99) < 14, 'text-red-400': (pos.days_remaining ?? 99) < 14,
'text-yellow-400': (pos.days_remaining ?? 99) < 30, 'text-yellow-400': (pos.days_remaining ?? 99) < 30,
'text-white': (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> </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"> <div className="flex items-center gap-4 text-xs mb-2">
<span className="text-slate-600">Greeks:</span> <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-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>ν <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> </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" 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" />} {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> </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="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"> <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 && ( {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 && ( {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> <span>r: <span className="text-slate-300">5%</span></span>
{pos.expiry_days != null && ( {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> </div>
<table className="w-full mb-2"> <table className="w-full mb-2">
@@ -331,9 +331,9 @@ function PositionCard({ pos }: { pos: Record<string, any> }) {
<tr className="text-slate-600"> <tr className="text-slate-600">
<th className="text-left pb-1 font-normal">Leg</th> <th className="text-left pb-1 font-normal">Leg</th>
<th className="text-left pb-1 font-normal">Strike</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">Qty</th>
<th className="text-right pb-1 font-normal">Prime/contrat</th> <th className="text-right pb-1 font-normal">Premium/contract</th>
<th className="text-right pb-1 font-normal">Coût total</th> <th className="text-right pb-1 font-normal">Total cost</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -370,7 +370,7 @@ function PositionCard({ pos }: { pos: Record<string, any> }) {
</tbody> </tbody>
</table> </table>
<div className="text-slate-600 italic"> <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>
</div> </div>
)} )}
@@ -403,29 +403,29 @@ function PositionCard({ pos }: { pos: Record<string, any> }) {
<div className="flex gap-2"> <div className="flex gap-2">
<button onClick={() => setShowClose(!showClose)} <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"> 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> </button>
</div> </div>
{showClose && ( {showClose && (
<div className="mt-2 space-y-1"> <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"> <div className="flex gap-2">
<input type="number" value={closeVal} onChange={e => setCloseVal(e.target.value)} <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" /> 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 <button
onClick={() => closePos({ id: pos.id, close_value: parseFloat(closeVal) || 0 }, onClick={() => closePos({ id: pos.id, close_value: parseFloat(closeVal) || 0 },
{ onSuccess: () => setShowClose(false) })} { onSuccess: () => setShowClose(false) })}
disabled={closing || !closeVal} disabled={closing || !closeVal}
className="bg-emerald-700 hover:bg-emerald-600 disabled:opacity-40 text-white px-3 rounded text-xs font-semibold"> 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> </button>
</div> </div>
{closeVal && ( {closeVal && (
<div className={clsx('text-xs', parseFloat(closeVal) - entryRef - (pos.ib_fees_entry||0) - 1 >= 0 ? 'positive' : 'negative')}> <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)}€ Net IB P&L: {(parseFloat(closeVal) - entryRef - (pos.ib_fees_entry||0) - 1).toFixed(2)}
(frais sortie: ~$1.00) (exit fees: ~$1.00)
</div> </div>
)} )}
</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 text-slate-500">{pos.close_date?.slice(0, 10)}</td>
<td className="py-1.5 pl-2"> <td className="py-1.5 pl-2">
<button onClick={() => deletePos(pos.id)} disabled={isPending} <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" /> <Trash2 className="w-3.5 h-3.5" />
</button> </button>
</td> </td>
@@ -470,19 +470,19 @@ export default function Portfolio() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-xl font-bold text-white flex items-center gap-2"> <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> </h1>
<p className="text-xs text-slate-500 mt-0.5"> <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> </p>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button onClick={() => refetch()} className="flex items-center gap-1 text-xs text-slate-400 hover:text-slate-200"> <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>
<button onClick={() => setShowModal(true)} <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"> 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> </button>
</div> </div>
</div> </div>
@@ -491,11 +491,11 @@ export default function Portfolio() {
{summary && ( {summary && (
<div className="grid grid-cols-5 gap-3"> <div className="grid grid-cols-5 gap-3">
{[ {[
{ label: 'Positions ouvertes', value: summary.open_positions, color: 'text-white' }, { label: 'Open positions', value: summary.open_positions, color: 'text-white' },
{ label: 'Capital engagé', value: `${summary.total_invested}€`, color: 'text-white' }, { label: 'Capital deployed', 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: 'Unrealized P&L', 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: 'Realized P&L', 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: 'Total IB fees', value: `${summary.total_fees_paid}`, color: 'text-orange-400' },
].map(({ label, value, color }) => ( ].map(({ label, value, color }) => (
<div key={label} className="card-sm text-center"> <div key={label} className="card-sm text-center">
<div className="stat-label">{label}</div> <div className="stat-label">{label}</div>
@@ -508,9 +508,9 @@ export default function Portfolio() {
{/* Tabs */} {/* Tabs */}
<div className="flex gap-1 bg-dark-700 p-1 rounded w-fit"> <div className="flex gap-1 bg-dark-700 p-1 rounded w-fit">
{[ {[
{ key: 'open', label: `Positions ouvertes (${positions?.length ?? 0})` }, { key: 'open', label: `Open positions (${positions?.length ?? 0})` },
{ key: 'closed', label: `Clôturées (${closed?.length ?? 0})` }, { key: 'closed', label: `Closed (${closed?.length ?? 0})` },
{ key: 'history', label: 'Courbe P&L' }, { key: 'history', label: 'P&L curve' },
].map(({ key, label }) => ( ].map(({ key, label }) => (
<button key={key} onClick={() => setTab(key as any)} <button key={key} onClick={() => setTab(key as any)}
className={clsx('px-3 py-1.5 rounded text-sm transition-colors', { 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"> <div className="card text-center py-12 text-slate-500">
<DollarSign className="w-10 h-10 mx-auto mb-3 opacity-20" /> <DollarSign className="w-10 h-10 mx-auto mb-3 opacity-20" />
<div>Aucune position ouverte</div> <div>No open positions</div>
<div className="text-xs mt-1">Ajouter un trade depuis les idées ou manuellement</div> <div className="text-xs mt-1">Add a trade from ideas or manually</div>
</div> </div>
)} )}
</> </>
@@ -552,12 +552,12 @@ export default function Portfolio() {
<thead> <thead>
<tr className="text-slate-600 border-b border-slate-700/40"> <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">Position</th>
<th className="text-left pb-2">Stratégie</th> <th className="text-left pb-2">Strategy</th>
<th className="text-right pb-2">Investi</th> <th className="text-right pb-2">Invested</th>
<th className="text-right pb-2">Clôture</th> <th className="text-right pb-2">Close</th>
<th className="text-right pb-2">Frais IB</th> <th className="text-right pb-2">IB fees</th>
<th className="text-right pb-2">P&L net</th> <th className="text-right pb-2">Net P&L</th>
<th className="text-left pb-2">Date clôture</th> <th className="text-left pb-2">Close date</th>
<th className="pb-2"></th> <th className="pb-2"></th>
</tr> </tr>
</thead> </thead>
@@ -570,7 +570,7 @@ export default function Portfolio() {
) )
})} })}
{(!closed || closed.length === 0) && ( {(!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> </tbody>
</table> </table>
@@ -580,7 +580,7 @@ export default function Portfolio() {
{/* P&L history */} {/* P&L history */}
{tab === 'history' && ( {tab === 'history' && (
<div className="card"> <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 ? ( {pnlHistory && pnlHistory.length > 0 ? (
<> <>
<ResponsiveContainer width="100%" height={250}> <ResponsiveContainer width="100%" height={250}>
@@ -596,7 +596,7 @@ export default function Portfolio() {
<YAxis tick={{ fill: '#475569', fontSize: 9 }} tickLine={false} axisLine={false} <YAxis tick={{ fill: '#475569', fontSize: 9 }} tickLine={false} axisLine={false}
tickFormatter={v => `${v.toFixed(0)}`} /> tickFormatter={v => `${v.toFixed(0)}`} />
<Tooltip contentStyle={{ background: '#0f1623', border: '1px solid #1e2d4d', fontSize: 11 }} <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" /> <ReferenceLine y={0} stroke="#475569" strokeDasharray="4 4" />
<Area type="monotone" dataKey="cumulative" stroke="#10b981" fill="url(#pnl-grad)" strokeWidth={2} dot={false} /> <Area type="monotone" dataKey="cumulative" stroke="#10b981" fill="url(#pnl-grad)" strokeWidth={2} dot={false} />
</AreaChart> </AreaChart>
@@ -615,7 +615,7 @@ export default function Portfolio() {
</> </>
) : ( ) : (
<div className="h-40 flex items-center justify-center text-slate-600 text-sm"> <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>
)} )}
</div> </div>

View File

@@ -140,13 +140,13 @@ function DiffView({ diffData }: { diffData: any }) {
{pd.positions_opened > 0 && ( {pd.positions_opened > 0 && (
<div className="text-center"> <div className="text-center">
<div className="text-emerald-400 font-bold text-lg">+{pd.positions_opened}</div> <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> </div>
)} )}
{pd.positions_closed > 0 && ( {pd.positions_closed > 0 && (
<div className="text-center"> <div className="text-center">
<div className="text-red-400 font-bold text-lg">-{pd.positions_closed}</div> <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>
)} )}
</div> </div>
@@ -159,7 +159,7 @@ function DiffView({ diffData }: { diffData: any }) {
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
<Plus className="w-4 h-4 text-emerald-400" /> <Plus className="w-4 h-4 text-emerald-400" />
<h3 className="text-sm font-semibold text-emerald-400"> <h3 className="text-sm font-semibold text-emerald-400">
Nouvelles positions ({new_positions.length}) New positions ({new_positions.length})
</h3> </h3>
</div> </div>
<div className="space-y-1.5"> <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> <span className="text-slate-400 ml-2">{p.strategy}</span>
</div> </div>
<div className="flex gap-4"> <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')}> <span className={clsx('font-semibold', (p.pnl_pct ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400')}>
{fmt(p.pnl_pct)} {fmt(p.pnl_pct)}
</span> </span>
@@ -187,7 +187,7 @@ function DiffView({ diffData }: { diffData: any }) {
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
<Minus className="w-4 h-4 text-red-400" /> <Minus className="w-4 h-4 text-red-400" />
<h3 className="text-sm font-semibold text-red-400"> <h3 className="text-sm font-semibold text-red-400">
Positions fermées ({closed_positions.length}) Closed positions ({closed_positions.length})
</h3> </h3>
</div> </div>
<div className="space-y-1.5"> <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> <span className="text-slate-500 ml-2">{p.strategy}</span>
</div> </div>
<div className="flex gap-4"> <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')}> <span className={clsx('font-semibold', (p.pnl_pct ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400')}>
{fmtEur(p.pnl_eur)} {fmtEur(p.pnl_eur)}
</span> </span>
@@ -213,7 +213,7 @@ function DiffView({ diffData }: { diffData: any }) {
{changed_positions.length > 0 && ( {changed_positions.length > 0 && (
<div className="bg-dark-800 border border-slate-700/40 rounded-xl p-4"> <div className="bg-dark-800 border border-slate-700/40 rounded-xl p-4">
<h3 className="text-sm font-semibold text-white mb-3"> <h3 className="text-sm font-semibold text-white mb-3">
Évolution des positions ({changed_positions.length}) Position changes ({changed_positions.length})
</h3> </h3>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-xs"> <table className="w-full text-xs">
@@ -265,7 +265,7 @@ function SnapshotDetail({ snapId }: { snapId: number }) {
staleTime: 300_000, 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 if (!data) return null
const trades: any[] = data.trades_snapshot ?? [] const trades: any[] = data.trades_snapshot ?? []
@@ -277,7 +277,7 @@ function SnapshotDetail({ snapId }: { snapId: number }) {
<div> <div>
<h3 className="text-sm font-semibold text-white">{fmtDate(data.snapped_at)} UTC</h3> <h3 className="text-sm font-semibold text-white">{fmtDate(data.snapped_at)} UTC</h3>
{macro?.dominant && ( {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>
<div className="flex gap-4 text-xs"> <div className="flex gap-4 text-xs">
@@ -294,7 +294,7 @@ function SnapshotDetail({ snapId }: { snapId: number }) {
</span> </span>
</div> </div>
<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> </div>
</div> </div>
@@ -304,9 +304,9 @@ function SnapshotDetail({ snapId }: { snapId: number }) {
<thead> <thead>
<tr className="text-slate-500 border-b border-slate-700/40"> <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">Ticker</th>
<th className="text-left pb-2 font-medium">Stratégie</th> <th className="text-left pb-2 font-medium">Strategy</th>
<th className="text-right pb-2 font-medium">Entrée</th> <th className="text-right pb-2 font-medium">Entry</th>
<th className="text-right pb-2 font-medium">Prix actuel</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>
<th className="text-right pb-2 font-medium">PnL </th> <th className="text-right pb-2 font-medium">PnL </th>
</tr> </tr>
@@ -329,7 +329,7 @@ function SnapshotDetail({ snapId }: { snapId: number }) {
</tbody> </tbody>
</table> </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> </div>
) )
@@ -397,11 +397,11 @@ export default function PositionHistory() {
<div> <div>
<div className="flex items-center gap-3 mb-1"> <div className="flex items-center gap-3 mb-1">
<History className="w-6 h-6 text-violet-400" /> <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> </div>
<p className="text-slate-400 text-sm"> <p className="text-slate-400 text-sm">
{snapshots.length} snapshot{snapshots.length !== 1 ? 's' : ''} PnL enregistré{snapshots.length !== 1 ? 's' : ''} {snapshots.length} PnL snapshot{snapshots.length !== 1 ? 's' : ''} recorded
{snapshots.length > 0 && ` · de ${fmtDate(snapshots[snapshots.length - 1]?.snapped_at)} à ${fmtDate(snapshots[0]?.snapped_at)}`} {snapshots.length > 0 && ` · from ${fmtDate(snapshots[snapshots.length - 1]?.snapped_at)} to ${fmtDate(snapshots[0]?.snapped_at)}`}
</p> </p>
</div> </div>
<div className="flex gap-2 flex-wrap"> <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', 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')} viewMode === 'detail' ? 'bg-slate-600 text-white' : 'bg-dark-700 text-slate-400 hover:text-white')}
> >
Détail Detail
</button> </button>
<button <button
onClick={() => setViewMode('diff')} onClick={() => setViewMode('diff')}
@@ -428,12 +428,12 @@ export default function PositionHistory() {
onClick={() => { setSelectedA(null); setSelectedB(null); setViewMode('detail') }} 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" 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>
)} )}
<button onClick={() => refetch()} <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"> 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> </button>
</div> </div>
</div> </div>
@@ -448,24 +448,24 @@ export default function PositionHistory() {
{/* Selection banner */} {/* Selection banner */}
{(selectedA || selectedB) && ( {(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"> <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 ? ( {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="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> <span className="font-bold">A</span>
{fmtDate(snapshots.find(s => s.id === selectedA)?.snapped_at ?? '')} {fmtDate(snapshots.find(s => s.id === selectedA)?.snapped_at ?? '')}
</span> </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" /> <ArrowRight className="w-3.5 h-3.5 text-slate-600" />
{selectedB ? ( {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="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> <span className="font-bold">B</span>
{fmtDate(snapshots.find(s => s.id === selectedB)?.snapped_at ?? '')} {fmtDate(snapshots.find(s => s.id === selectedB)?.snapped_at ?? '')}
</span> </span>
) : <span className="text-slate-600">B : non sélectionné</span>} ) : <span className="text-slate-600">B: not selected</span>}
{canDiff && ( {canDiff && (
<button onClick={() => setViewMode('diff')} <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"> 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> </button>
)} )}
</div> </div>
@@ -475,10 +475,10 @@ export default function PositionHistory() {
{!isLoading && snapshots.length === 0 && ( {!isLoading && snapshots.length === 0 && (
<div className="text-center py-20"> <div className="text-center py-20">
<History className="w-14 h-14 text-slate-700 mx-auto mb-4" /> <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"> <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 Enable the PnL Scheduler in Configuration or trigger a snapshot manually
depuis la page Configuration Auto-Cycle &amp; Logging. from the Configuration Auto-Cycle &amp; Logging page.
</p> </p>
</div> </div>
)} )}
@@ -491,7 +491,7 @@ export default function PositionHistory() {
{/* Mini sparkline */} {/* Mini sparkline */}
{sparkData.length > 2 && ( {sparkData.length > 2 && (
<div className="bg-dark-800 border border-slate-700/40 rounded-xl p-3"> <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}> <ResponsiveContainer width="100%" height={80}>
<LineChart data={sparkData} margin={{ top: 4, right: 4, left: -30, bottom: 0 }}> <LineChart data={sparkData} margin={{ top: 4, right: 4, left: -30, bottom: 0 }}>
<XAxis dataKey="date" hide /> <XAxis dataKey="date" hide />
@@ -507,7 +507,7 @@ export default function PositionHistory() {
{/* Snapshot list */} {/* 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="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"> <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> </div>
{snapshots.map(s => ( {snapshots.map(s => (
<div key={s.id} onClick={() => { setActiveDetail(s.id); setViewMode('detail') }}> <div key={s.id} onClick={() => { setActiveDetail(s.id); setViewMode('detail') }}>
@@ -527,7 +527,7 @@ export default function PositionHistory() {
<div className="lg:col-span-2"> <div className="lg:col-span-2">
{viewMode === 'diff' && canDiff && ( {viewMode === 'diff' && canDiff && (
diffLoading 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 : diffError
? <div className="text-red-400 text-sm p-4">{String(diffError)}</div> ? <div className="text-red-400 text-sm p-4">{String(diffError)}</div>
: diffData ? <DiffView diffData={diffData} /> : 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"> <div className="flex flex-col items-center justify-center py-20 text-center">
<TrendingUp className="w-12 h-12 text-slate-700 mb-4" /> <TrendingUp className="w-12 h-12 text-slate-700 mb-4" />
<div className="text-slate-500 text-sm"> <div className="text-slate-500 text-sm">
Cliquez sur un snapshot pour voir le détail<br /> Click a snapshot to see the detail<br />
ou sélectionnez A et B pour comparer deux valuations or select A and B to compare two valuations
</div> </div>
</div> </div>
)} )}
@@ -552,7 +552,7 @@ export default function PositionHistory() {
<div className="flex flex-col items-center justify-center py-20 text-center"> <div className="flex flex-col items-center justify-center py-20 text-center">
<TrendingDown className="w-12 h-12 text-slate-700 mb-4" /> <TrendingDown className="w-12 h-12 text-slate-700 mb-4" />
<div className="text-slate-500 text-sm"> <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>
</div> </div>
)} )}

View File

@@ -9,14 +9,14 @@ import {
const SCENARIO_META: Record<string, { label: string; color: string; emoji: string }> = { const SCENARIO_META: Record<string, { label: string; color: string; emoji: string }> = {
goldilocks: { label: 'Goldilocks', color: '#22c55e', emoji: '🌟' }, 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: '🛬' }, soft_landing: { label: 'Soft Landing', color: '#3b82f6', emoji: '🛬' },
reflation: { label: 'Reflation', color: '#f97316', emoji: '🔥' }, reflation: { label: 'Reflation', color: '#f97316', emoji: '🔥' },
stagflation: { label: 'Stagflation', color: '#eab308', emoji: '⚡' }, stagflation: { label: 'Stagflation', color: '#eab308', emoji: '⚡' },
inflation_shock: { label: 'Inflation Shock', color: '#ef4444', emoji: '💥' }, inflation_shock: { label: 'Inflation Shock', color: '#ef4444', emoji: '💥' },
recession: { label: 'Récession', color: '#8b5cf6', emoji: '📉' }, recession: { label: 'Recession', color: '#8b5cf6', emoji: '📉' },
crise_liquidite: { label: 'Crise Liquidité', color: '#ec4899', emoji: '🚨' }, crise_liquidite: { label: 'Liquidity Crisis', color: '#ec4899', emoji: '🚨' },
incertain: { label: 'Incertain', color: '#64748b', emoji: '❓' }, incertain: { label: 'Uncertain', color: '#64748b', emoji: '❓' },
} }
function RegimeBadge({ dominant }: { dominant?: string }) { 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> <div className="text-slate-600 text-[10px] mt-0.5 line-clamp-2">{item.description}</div>
)} )}
{item.triggers?.length > 0 && ( {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 && ( {item.pnl_pct != null && (
<span className={clsx('font-mono text-[10px] ml-1', item.pnl_pct >= 0 ? 'text-emerald-400' : 'text-red-400')}> <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 */} {/* Left panel — history list */}
<div className="w-56 shrink-0"> <div className="w-56 shrink-0">
<div className="section-title flex items-center gap-1 mb-3"> <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> </div>
{listLoading ? ( {listLoading ? (
<div className="text-xs text-slate-600">Chargement</div> <div className="text-xs text-slate-600">Loading</div>
) : reports.length === 0 ? ( ) : 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"> <div className="space-y-1">
{reports.map((r: any) => { {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 dt = r.generated_at ? new Date(r.generated_at.endsWith('Z') ? r.generated_at : r.generated_at + 'Z') : null
const dateStr = dt 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 isActive = selectedRunId === r.run_id || (!selectedRunId && r === reports[0])
const m = SCENARIO_META[r.macro_dominant] ?? SCENARIO_META.incertain 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"> <div className="flex-1 min-w-0">
{isLoading ? ( {isLoading ? (
<div className="flex items-center gap-2 text-slate-500 text-sm mt-12 justify-center"> <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> </div>
) : !report ? ( ) : !report ? (
<div className="card text-center py-12"> <div className="card text-center py-12">
<Brain className="w-10 h-10 text-slate-700 mx-auto mb-3" /> <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-slate-500 text-sm mb-1">No cycle report available</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-xs text-slate-600">Reports are generated automatically at the end of each cycle.</div>
</div> </div>
) : ( ) : (
<> <>
@@ -221,18 +221,18 @@ export default function RapportCycle() {
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<RegimeBadge dominant={report.macro_dominant} /> <RegimeBadge dominant={report.macro_dominant} />
<span className="text-xs text-slate-500 font-mono"> <span className="text-xs text-slate-500 font-mono">
Géo {report.geo_score ?? '—'}/100 Geo {report.geo_score ?? '—'}/100
</span> </span>
</div> </div>
<div className="text-[10px] text-slate-600 font-mono"> <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') ? new Date(report.generated_at.endsWith('Z') ? report.generated_at : report.generated_at + 'Z')
.toLocaleString('fr-FR') .toLocaleString('en-GB')
: '—'} : '—'}
{report.prev_cycle_at && ( {report.prev_cycle_at && (
<span className="ml-2 text-slate-700"> <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') · vs cycle of {new Date(report.prev_cycle_at.endsWith('Z') ? report.prev_cycle_at : report.prev_cycle_at + 'Z')
.toLocaleDateString('fr-FR')} .toLocaleDateString('en-GB')}
</span> </span>
)} )}
</div> </div>
@@ -260,7 +260,7 @@ export default function RapportCycle() {
<div className="grid grid-cols-2 gap-3 mt-2"> <div className="grid grid-cols-2 gap-3 mt-2">
{report.pnl_summary && Object.keys(report.pnl_summary).length > 0 && ( {report.pnl_summary && Object.keys(report.pnl_summary).length > 0 && (
<div className="bg-dark-700/50 rounded px-3 py-2"> <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"> <div className="flex items-baseline gap-2">
<span className={clsx('text-lg font-bold font-mono', <span className={clsx('text-lg font-bold font-mono',
(report.pnl_summary.total_pnl_pct ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400')}> (report.pnl_summary.total_pnl_pct ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400')}>
@@ -276,13 +276,13 @@ export default function RapportCycle() {
)} )}
</div> </div>
<div className="text-[9px] text-slate-600 mt-0.5"> <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>
</div> </div>
)} )}
{report.var_summary && Object.keys(report.var_summary).length > 0 && ( {report.var_summary && Object.keys(report.var_summary).length > 0 && (
<div className="bg-dark-700/50 rounded px-3 py-2"> <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"> <div className="grid grid-cols-3 gap-1">
{[ {[
{ label: 'Hist.', val: report.var_summary.hist_var_1d_pct, color: 'text-blue-400' }, { label: 'Hist.', val: report.var_summary.hist_var_1d_pct, color: 'text-blue-400' },
@@ -303,15 +303,15 @@ export default function RapportCycle() {
</div> </div>
{/* Delta section */} {/* 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 <DeltaRow
emoji="⭐" label="patterns ajoutés" emoji="⭐" label="patterns added"
count={report.patterns_added ?? 0} count={report.patterns_added ?? 0}
items={report.patterns_added_list ?? []} items={report.patterns_added_list ?? []}
colorClass="text-blue-400" colorClass="text-blue-400"
/> />
<DeltaRow <DeltaRow
emoji="📥" label="trades loggés" emoji="📥" label="trades logged"
count={report.trades_logged ?? 0} count={report.trades_logged ?? 0}
items={(report.trades_logged_list ?? []).map((t: any) => ({ items={(report.trades_logged_list ?? []).map((t: any) => ({
underlying: t.underlying, strategy: t.strategy, underlying: t.underlying, strategy: t.strategy,
@@ -320,7 +320,7 @@ export default function RapportCycle() {
colorClass="text-emerald-400" colorClass="text-emerald-400"
/> />
<DeltaRow <DeltaRow
emoji="✅" label="trades fermés depuis le cycle précédent" emoji="✅" label="trades closed since last cycle"
count={report.trades_closed ?? 0} count={report.trades_closed ?? 0}
items={(report.trades_closed_list ?? []).map((t: any) => ({ items={(report.trades_closed_list ?? []).map((t: any) => ({
underlying: t.underlying, strategy: t.strategy, underlying: t.underlying, strategy: t.strategy,
@@ -331,7 +331,7 @@ export default function RapportCycle() {
{/* Top scored */} {/* Top scored */}
{(report.top_scored ?? []).length > 0 && ( {(report.top_scored ?? []).length > 0 && (
<div className="mt-3 pt-3 border-t border-slate-700/30"> <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"> <div className="space-y-1.5">
{(report.top_scored as any[]).map((p: any, i: number) => ( {(report.top_scored as any[]).map((p: any, i: number) => (
<div key={i} className="flex items-start gap-2 text-xs"> <div key={i} className="flex items-start gap-2 text-xs">
@@ -355,7 +355,7 @@ export default function RapportCycle() {
{/* Context narrative */} {/* Context narrative */}
{report.context_narrative?.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"> <p className="text-sm text-slate-300 leading-relaxed mb-4">
{report.context_narrative.narrative} {report.context_narrative.narrative}
</p> </p>
@@ -363,7 +363,7 @@ export default function RapportCycle() {
{(report.context_narrative.context_driven ?? []).length > 0 && ( {(report.context_narrative.context_driven ?? []).length > 0 && (
<div> <div>
<div className="text-[10px] text-emerald-400 font-semibold uppercase tracking-wide mb-2"> <div className="text-[10px] text-emerald-400 font-semibold uppercase tracking-wide mb-2">
📡 Du contexte transmis 📡 From transmitted context
</div> </div>
<ul className="space-y-1"> <ul className="space-y-1">
{(report.context_narrative.context_driven as string[]).map((s, i) => ( {(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 && ( {(report.context_narrative.general_knowledge ?? []).length > 0 && (
<div> <div>
<div className="text-[10px] text-blue-400 font-semibold uppercase tracking-wide mb-2"> <div className="text-[10px] text-blue-400 font-semibold uppercase tracking-wide mb-2">
🧠 Connaissance générale 🧠 General knowledge
</div> </div>
<ul className="space-y-1"> <ul className="space-y-1">
{(report.context_narrative.general_knowledge as string[]).map((s, i) => ( {(report.context_narrative.general_knowledge as string[]).map((s, i) => (
@@ -391,7 +391,7 @@ export default function RapportCycle() {
</div> </div>
{(report.context_narrative.key_signals ?? []).length > 0 && ( {(report.context_narrative.key_signals ?? []).length > 0 && (
<div className="mt-3 pt-3 border-t border-slate-700/30"> <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"> <div className="flex flex-wrap gap-1.5">
{(report.context_narrative.key_signals as string[]).map((s, i) => ( {(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"> <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 */} {/* Context log */}
{report.context_narrative.context_log && ( {report.context_narrative.context_log && (
<div className="mt-3 pt-3 border-t border-slate-700/30"> <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]"> <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]) => ( {Object.entries(report.context_narrative.context_log.key_gauges ?? {}).map(([k, v]: [string, any]) => (
<div key={k} className="flex items-center gap-1.5"> <div key={k} className="flex items-center gap-1.5">
@@ -415,7 +415,7 @@ export default function RapportCycle() {
</div> </div>
{(report.context_narrative.context_log.dominant_news ?? []).length > 0 && ( {(report.context_narrative.context_log.dominant_news ?? []).length > 0 && (
<div className="mt-2"> <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) => ( {(report.context_narrative.context_log.dominant_news as string[]).map((n, i) => (
<div key={i} className="text-[9px] text-slate-600 truncate">· {n}</div> <div key={i} className="text-[9px] text-slate-600 truncate">· {n}</div>
))} ))}
@@ -428,7 +428,7 @@ export default function RapportCycle() {
{/* Cycle commentary */} {/* Cycle commentary */}
{report.commentary?.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> <p className="text-sm text-slate-300 leading-relaxed mb-3">{report.commentary.commentary}</p>
{report.commentary.key_risk && ( {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"> <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 && ( {report.commentary.top_pattern && (
<div className="mt-2 text-xs text-slate-500"> <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> </div>
)} )}
</Section> </Section>
@@ -446,7 +446,7 @@ export default function RapportCycle() {
{/* Options Technical Agent */} {/* Options Technical Agent */}
{report.options_technical && (report.options_technical.assessments?.length > 0 || report.options_technical.global_assessment) && ( {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 */} {/* Global score + summary */}
<div className="flex items-start gap-3 mb-4"> <div className="flex items-start gap-3 mb-4">
{report.options_technical.global_score != null && ( {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]"> <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 && ( {a.optimal_strategy && a.optimal_strategy !== a.strategy && (
<span className="text-slate-500"> <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> </span>
)} )}
{a.when_to_enter && ( {a.when_to_enter && (
@@ -563,7 +563,7 @@ export default function RapportCycle() {
{/* Risk / portfolio monitor */} {/* Risk / portfolio monitor */}
{report.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> <p className="text-sm text-slate-400 mb-3">{report.portfolio_monitor.assessment}</p>
{(report.portfolio_monitor.actions ?? []).length > 0 && ( {(report.portfolio_monitor.actions ?? []).length > 0 && (
<div className="space-y-2"> <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 className={clsx('h-full rounded-full transition-all', color)} style={{ width: `${Math.min(pct, 100)}%` }} />
</div> </div>
{pct > threshold && ( {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> </div>
) )
@@ -28,7 +28,7 @@ function EquityCurve({ timeline }: { timeline: any[] }) {
if (!timeline || timeline.length < 2) { if (!timeline || timeline.length < 2) {
return ( return (
<div className="h-24 flex items-center justify-center text-slate-600 text-xs"> <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> </div>
) )
} }
@@ -119,7 +119,7 @@ function RecommendationCard({ rec }: { rec: any }) {
'text-emerald-400': isOk, 'text-emerald-400': isOk,
})}> })}>
{isOk ? <CheckCircle className="w-4 h-4" /> : <AlertTriangle className="w-4 h-4" />} {isOk ? <CheckCircle className="w-4 h-4" /> : <AlertTriangle className="w-4 h-4" />}
Recommandation Risk Committee Risk Committee Recommendation
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
{(rec.messages ?? []).map((msg: string, i: number) => ( {(rec.messages ?? []).map((msg: string, i: number) => (
@@ -150,8 +150,8 @@ function SimRiskPanel() {
if (!risk || risk.open_count === 0) return ( if (!risk || risk.open_count === 0) return (
<div className="card text-center py-10 text-slate-500"> <div className="card text-center py-10 text-slate-500">
<PieChart className="w-8 h-8 mx-auto mb-2 opacity-20" /> <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-sm">No open positions in the simulated portfolio</div>
<div className="text-xs mt-1">Les trades logués apparaîtront ici après le prochain cycle IA</div> <div className="text-xs mt-1">Logged trades will appear here after the next AI cycle</div>
</div> </div>
) )
@@ -169,25 +169,25 @@ function SimRiskPanel() {
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3"> <div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div className="card text-center"> <div className="card text-center">
<div className="text-2xl font-bold text-white font-mono">{risk.open_count}</div> <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>
<div className="card text-center"> <div className="card text-center">
<div className={clsx('text-2xl font-bold font-mono', dangers.length > 0 ? 'text-red-400' : 'text-emerald-400')}> <div className={clsx('text-2xl font-bold font-mono', dangers.length > 0 ? 'text-red-400' : 'text-emerald-400')}>
{dangers.length} {dangers.length}
</div> </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>
<div className="card text-center"> <div className="card text-center">
<div className={clsx('text-2xl font-bold font-mono', warnings.length > 0 ? 'text-amber-400' : 'text-emerald-400')}> <div className={clsx('text-2xl font-bold font-mono', warnings.length > 0 ? 'text-amber-400' : 'text-emerald-400')}>
{warnings.length} {warnings.length}
</div> </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>
<div className="card text-center"> <div className="card text-center">
<div className="text-2xl font-bold font-mono text-slate-300"> <div className="text-2xl font-bold font-mono text-slate-300">
{Object.keys(concentration).filter(ac => ac !== 'unknown').length} {Object.keys(concentration).filter(ac => ac !== 'unknown').length}
</div> </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>
</div> </div>
@@ -197,7 +197,7 @@ function SimRiskPanel() {
<div className="card"> <div className="card">
<div className="text-sm font-semibold text-white mb-3 flex items-center justify-between"> <div className="text-sm font-semibold text-white mb-3 flex items-center justify-between">
<span className="flex items-center gap-2"> <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> </span>
<button onClick={() => refetch()} className="text-slate-600 hover:text-slate-400"> <button onClick={() => refetch()} className="text-slate-600 hover:text-slate-400">
<RefreshCw className="w-3.5 h-3.5" /> <RefreshCw className="w-3.5 h-3.5" />
@@ -243,14 +243,14 @@ function SimRiskPanel() {
{conflicts.length === 0 && warnings.length === 0 ? ( {conflicts.length === 0 && warnings.length === 0 ? (
<div className="card border-emerald-700/30 bg-emerald-900/10 flex items-center gap-3 py-4"> <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" /> <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> </div>
) : null} ) : null}
{conflicts.map((c: any, i: number) => ( {conflicts.map((c: any, i: number) => (
<div key={i} className="card border-red-700/40 bg-red-900/10"> <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"> <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>
<div className="space-y-1"> <div className="space-y-1">
{c.trades.map((t: any) => ( {c.trades.map((t: any) => (
@@ -271,7 +271,7 @@ function SimRiskPanel() {
{warnings.length > 0 && ( {warnings.length > 0 && (
<div className="card border-amber-700/30 bg-amber-900/10"> <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"> <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>
<div className="space-y-1"> <div className="space-y-1">
{warnings.map((a: any, i: number) => ( {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="card border-blue-700/30 bg-blue-900/10">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<div className="text-xs font-semibold text-blue-300 flex items-center gap-1.5"> <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> </div>
{aiTs && <span className="text-[10px] text-slate-600">{aiTs.slice(0, 16).replace('T', ' ')}</span>} {aiTs && <span className="text-[10px] text-slate-600">{aiTs.slice(0, 16).replace('T', ' ')}</span>}
</div> </div>
@@ -349,8 +349,8 @@ export default function RiskDashboard() {
</h1> </h1>
<p className="text-xs text-slate-500 mt-0.5"> <p className="text-xs text-slate-500 mt-0.5">
{mode === 'real' {mode === 'real'
? 'Concentration · Clusters · Corrélations · Position Sizing — Portefeuille IBKR' ? 'Concentration · Clusters · Correlations · Position Sizing — IBKR Portfolio'
: 'Conflits directionnels · Concentration · Recommandations IA — Trades loggés'} : 'Directional conflicts · Concentration · AI Recommendations — Logged trades'}
</p> </p>
</div> </div>
{/* Mode toggle */} {/* Mode toggle */}
@@ -360,14 +360,14 @@ export default function RiskDashboard() {
'bg-blue-600 text-white': mode === 'real', 'bg-blue-600 text-white': mode === 'real',
'text-slate-400 hover:text-slate-200': mode !== 'real', 'text-slate-400 hover:text-slate-200': mode !== 'real',
})}> })}>
Portefeuille Réel Real Portfolio
</button> </button>
<button onClick={() => setMode('sim')} <button onClick={() => setMode('sim')}
className={clsx('flex items-center gap-1.5 px-3 py-1.5 rounded text-sm font-semibold transition-colors', { 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', 'bg-blue-600 text-white': mode === 'sim',
'text-slate-400 hover:text-slate-200': mode !== 'sim', 'text-slate-400 hover:text-slate-200': mode !== 'sim',
})}> })}>
Simulé (loggés) Simulated (logged)
{simConflicts > 0 && ( {simConflicts > 0 && (
<span className="text-[10px] font-bold bg-red-500 text-white rounded-full w-4 h-4 flex items-center justify-center"> <span className="text-[10px] font-bold bg-red-500 text-white rounded-full w-4 h-4 flex items-center justify-center">
{simConflicts} {simConflicts}
@@ -387,14 +387,14 @@ export default function RiskDashboard() {
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4"> <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<div className="card text-center"> <div className="card text-center">
<div className="text-2xl font-bold text-white font-mono">{d.open_trades ?? 0}</div> <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>
<div className="card text-center"> <div className="card text-center">
<div className={clsx('text-2xl font-bold font-mono', <div className={clsx('text-2xl font-bold font-mono',
d.diversification_score >= 60 ? 'text-emerald-400' d.diversification_score >= 60 ? 'text-emerald-400'
: d.diversification_score >= 35 ? 'text-amber-400' : 'text-red-400' : d.diversification_score >= 35 ? 'text-amber-400' : 'text-red-400'
)}>{d.diversification_score ?? '—'}%</div> )}>{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 className="text-[10px] text-slate-600">N effectif = {d.effective_n_positions ?? '—'}</div>
</div> </div>
<div className="card text-center"> <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) < 15 ? 'text-emerald-400'
: (d.expected_drawdown_pct ?? 0) < 25 ? 'text-amber-400' : 'text-red-400' : (d.expected_drawdown_pct ?? 0) < 25 ? 'text-amber-400' : 'text-red-400'
)}>{d.expected_drawdown_pct ?? '—'}%</div> )}>{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>
<div className="card text-center"> <div className="card text-center">
<div className={clsx('text-2xl font-bold font-mono', <div className={clsx('text-2xl font-bold font-mono',
@@ -410,7 +410,7 @@ export default function RiskDashboard() {
)}> )}>
{d.saturated_factors?.length ?? 0} {d.saturated_factors?.length ?? 0}
</div> </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 && ( {d.saturated_factors?.length > 0 && (
<div className="text-[10px] text-red-400">{d.saturated_factors.join(', ')}</div> <div className="text-[10px] text-red-400">{d.saturated_factors.join(', ')}</div>
)} )}
@@ -439,10 +439,10 @@ export default function RiskDashboard() {
{/* Exposure by class */} {/* Exposure by class */}
<div className="card"> <div className="card">
<div className="text-sm font-semibold text-white mb-3 flex items-center gap-2"> <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> </div>
{Object.keys(d.exposure_by_class ?? {}).length === 0 ? ( {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"> <div className="space-y-3">
{Object.entries(d.exposure_by_class ?? {}) {Object.entries(d.exposure_by_class ?? {})
@@ -457,10 +457,10 @@ export default function RiskDashboard() {
{/* Risk factors */} {/* Risk factors */}
<div className="card"> <div className="card">
<div className="text-sm font-semibold text-white mb-3 flex items-center gap-2"> <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> </div>
{(d.risk_clusters ?? []).length === 0 ? ( {(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"> <div className="space-y-3">
{(d.risk_clusters ?? []).map((c: any) => ( {(d.risk_clusters ?? []).map((c: any) => (
@@ -475,14 +475,14 @@ export default function RiskDashboard() {
<div className="card"> <div className="card">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<div className="text-sm font-semibold text-white flex items-center gap-2"> <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> </div>
<select value={tlDays} onChange={e => setTlDays(Number(e.target.value))} <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"> 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={30}>30d</option>
<option value={90}>90j</option> <option value={90}>90d</option>
<option value={180}>180j</option> <option value={180}>180d</option>
<option value={365}>1 an</option> <option value={365}>1 yr</option>
</select> </select>
</div> </div>
<EquityCurve timeline={tlData?.timeline ?? []} /> <EquityCurve timeline={tlData?.timeline ?? []} />
@@ -491,11 +491,11 @@ export default function RiskDashboard() {
{/* Correlation pairs */} {/* Correlation pairs */}
<div className="card"> <div className="card">
<div className="text-sm font-semibold text-white mb-3"> <div className="text-sm font-semibold text-white mb-3">
Corrélations entre patterns (P&L historique) Pattern correlations (historical P&L)
</div> </div>
{pairs.length === 0 ? ( {pairs.length === 0 ? (
<div className="text-xs text-slate-600 text-center py-4"> <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>
) : ( ) : (
<div className="space-y-1.5"> <div className="space-y-1.5">
@@ -503,13 +503,13 @@ export default function RiskDashboard() {
<CorrelationPairRow key={i} pair={pair} /> <CorrelationPairRow key={i} pair={pair} />
))} ))}
{pairs.length > 10 && ( {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> </div>
)} )}
{pairs.length > 0 && ( {pairs.length > 0 && (
<div className="text-[10px] text-slate-600 mt-2"> <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>
)} )}
</div> </div>

View File

@@ -14,9 +14,9 @@ import {
// ─── Status badge ───────────────────────────────────────────────────────────── // ─── Status badge ─────────────────────────────────────────────────────────────
function StatusBadge({ status }: { status: string }) { function StatusBadge({ status }: { status: string }) {
const map: Record<string, { icon: React.ReactNode; color: string; label: 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' }, 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'] const s = map[status] || map['tentative']
return ( return (
@@ -61,10 +61,10 @@ function KbEntry({ entry, onStatusChange, onDelete }: {
</button> </button>
<button <button
onClick={() => { 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" 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" /> <Trash2 className="w-3.5 h-3.5" />
</button> </button>
@@ -90,12 +90,12 @@ function KbEntry({ entry, onStatusChange, onDelete }: {
: 'border-slate-700 text-slate-500 hover:border-slate-500 hover:text-slate-300' : '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> </button>
))} ))}
</div> </div>
<p className="text-[10px] text-slate-600"> <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> </p>
</div> </div>
)} )}
@@ -105,11 +105,11 @@ function KbEntry({ entry, onStatusChange, onDelete }: {
// ─── Category section ───────────────────────────────────────────────────────── // ─── Category section ─────────────────────────────────────────────────────────
const CAT_META: Record<string, { icon: React.ReactNode; color: string; label: string }> = { 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' }, 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' }, 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: 'Corrélations' }, 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: 'Général' }, général: { icon: <BookOpen className="w-4 h-4" />, color: 'text-slate-400', label: 'General' },
} }
function CategorySection({ function CategorySection({
@@ -132,7 +132,7 @@ function CategorySection({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className={meta.color}>{meta.icon}</span> <span className={meta.color}>{meta.icon}</span>
<span className="text-sm font-medium text-slate-200">{meta.label}</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> </div>
{open ? <ChevronUp className="w-4 h-4 text-slate-500" /> : <ChevronDown className="w-4 h-4 text-slate-500" />} {open ? <ChevronUp className="w-4 h-4 text-slate-500" /> : <ChevronDown className="w-4 h-4 text-slate-500" />}
</button> </button>
@@ -236,14 +236,14 @@ export default function SuperContexte() {
<Brain className="w-6 h-6 text-violet-400" /> <Brain className="w-6 h-6 text-violet-400" />
<div> <div>
<h1 className="text-xl font-bold text-slate-100">Super Contexte</h1> <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> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{currentState && ( {currentState && (
<div className="text-right text-xs text-slate-500"> <div className="text-right text-xs text-slate-500">
<div>v{currentState.version} · {currentState.created_at?.slice(0, 16)}</div> <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> </div>
)} )}
<button <button
@@ -262,7 +262,7 @@ export default function SuperContexte() {
) : ( ) : (
<Brain className="w-4 h-4" /> <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> </button>
</div> </div>
</div> </div>
@@ -277,7 +277,7 @@ export default function SuperContexte() {
onClick={() => setSelectedHistoryId(null)} 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'}`} 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> </button>
{history.map((h: any) => ( {history.map((h: any) => (
<div <div
@@ -290,18 +290,18 @@ export default function SuperContexte() {
> >
<div className="font-medium">v{h.version}</div> <div className="font-medium">v{h.version}</div>
<div className="text-slate-500">{h.created_at?.slice(0, 16)}</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>
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation() 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) if (selectedHistoryId === h.id) setSelectedHistoryId(null)
deleteState.mutate(h.id) 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" 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" /> <Trash2 className="w-3 h-3" />
</button> </button>
@@ -314,17 +314,17 @@ export default function SuperContexte() {
<div className="flex-1 min-w-0 space-y-6"> <div className="flex-1 min-w-0 space-y-6">
{stateLoading && ( {stateLoading && (
<div className="flex items-center justify-center h-40 text-slate-500"> <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> </div>
)} )}
{!currentState && !stateLoading && ( {!currentState && !stateLoading && (
<div className="border border-violet-500/20 rounded-xl p-8 text-center bg-violet-500/5"> <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" /> <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"> <p className="text-slate-500 text-sm mb-4">
Lance la première synthèse GPT-4o pour initialiser la base de raisonnement. Run the first GPT-4o synthesis to initialize the reasoning base.
Le système analysera tous les rapports et trades disponibles. The system will analyze all available reports and trades.
</p> </p>
<button <button
onClick={() => handleSynthesize()} 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" 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 ? <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> </button>
</div> </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"> <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" /> <Clock className="w-5 h-5 text-amber-400 flex-shrink-0 mt-0.5" />
<div className="flex-1 text-sm"> <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"> <span className="text-slate-400">
Dernière synthèse il y a {synthResult?.age_hours}h (moins de 6h). Last synthesis {synthResult?.age_hours}h ago (less than 6h).
Les trades immatures n'apportent pas encore d'information fiable inutile de recalculer. Immature trades don't yet provide reliable data — no need to recompute.
</span> </span>
<div className="mt-2"> <div className="mt-2">
<button <button
@@ -352,7 +352,7 @@ export default function SuperContexte() {
disabled={synthesize.isPending} 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" 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> </button>
</div> </div>
</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"> <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" /> <CheckCircle className="w-5 h-5 text-emerald-400 flex-shrink-0" />
<div className="text-sm"> <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"> <span className="text-slate-400">
{synthResult?.kb_entries_added ?? 0} entrées KB ajoutées ·{' '} {synthResult?.kb_entries_added ?? 0} KB entries added ·{' '}
{synthResult?.sources?.reports ?? 0} rapports ·{' '} {synthResult?.sources?.reports ?? 0} reports ·{' '}
{synthResult?.sources?.trades ?? 0} trades analysés. {synthResult?.sources?.trades ?? 0} trades analyzed.
</span> </span>
</div> </div>
</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="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"> <div className="flex items-center gap-2 text-violet-300">
<Brain className="w-5 h-5" /> <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 && ( {selectedHistoryId && (
<span className="text-xs text-slate-500 ml-auto">v{currentState.version} (archivé)</span> <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 && ( {synthesis.regime_insights?.length > 0 && (
<div className="border border-slate-700/50 rounded-xl p-4 space-y-3"> <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"> <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>
<div className="space-y-2"> <div className="space-y-2">
{synthesis.regime_insights.map((r: any, i: number) => ( {synthesis.regime_insights.map((r: any, i: number) => (
@@ -407,7 +407,7 @@ export default function SuperContexte() {
{synthesis.pattern_insights?.length > 0 && ( {synthesis.pattern_insights?.length > 0 && (
<div className="border border-slate-700/50 rounded-xl p-4 space-y-3"> <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"> <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>
<div className="space-y-2"> <div className="space-y-2">
{synthesis.pattern_insights.map((p: any, i: number) => ( {synthesis.pattern_insights.map((p: any, i: number) => (
@@ -427,7 +427,7 @@ export default function SuperContexte() {
{synthesis.macro_correlations?.length > 0 && ( {synthesis.macro_correlations?.length > 0 && (
<div className="border border-slate-700/50 rounded-xl p-4 space-y-3"> <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"> <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>
<div className="space-y-2"> <div className="space-y-2">
{synthesis.macro_correlations.map((c: any, i: number) => ( {synthesis.macro_correlations.map((c: any, i: number) => (
@@ -448,25 +448,25 @@ export default function SuperContexte() {
{/* Risk params + insights */} {/* Risk params + insights */}
<div className="border border-slate-700/50 rounded-xl p-4 space-y-4"> <div className="border border-slate-700/50 rounded-xl p-4 space-y-4">
<InsightList <InsightList
title="Priorités stratégiques" title="Strategic priorities"
icon={<Target className="w-4 h-4" />} icon={<Target className="w-4 h-4" />}
items={synthesis.strategic_priorities || []} items={synthesis.strategic_priorities || []}
color="text-violet-400" color="text-violet-400"
/> />
<InsightList <InsightList
title="Forces identifiées" title="Identified strengths"
icon={<Zap className="w-4 h-4" />} icon={<Zap className="w-4 h-4" />}
items={synthesis.strengths || []} items={synthesis.strengths || []}
color="text-emerald-400" color="text-emerald-400"
/> />
<InsightList <InsightList
title="Angles morts" title="Blind spots"
icon={<AlertTriangle className="w-4 h-4" />} icon={<AlertTriangle className="w-4 h-4" />}
items={synthesis.blind_spots || []} items={synthesis.blind_spots || []}
color="text-amber-400" color="text-amber-400"
/> />
<InsightList <InsightList
title="Erreurs récurrentes" title="Recurring mistakes"
icon={<TrendingDown className="w-4 h-4" />} icon={<TrendingDown className="w-4 h-4" />}
items={(synthesis.recurring_mistakes || []).map((m: any) => items={(synthesis.recurring_mistakes || []).map((m: any) =>
typeof m === 'string' ? m : `${m.mistake}${m.mitigation}` typeof m === 'string' ? m : `${m.mistake}${m.mitigation}`
@@ -476,13 +476,13 @@ export default function SuperContexte() {
{synthesis.risk_parameters && ( {synthesis.risk_parameters && (
<> <>
<InsightList <InsightList
title="Préférer quand" title="Prefer when"
icon={<Shield className="w-4 h-4" />} icon={<Shield className="w-4 h-4" />}
items={synthesis.risk_parameters.prefer_when || []} items={synthesis.risk_parameters.prefer_when || []}
color="text-blue-400" color="text-blue-400"
/> />
<InsightList <InsightList
title="Éviter quand" title="Avoid when"
icon={<XCircle className="w-4 h-4" />} icon={<XCircle className="w-4 h-4" />}
items={synthesis.risk_parameters.avoid_when || []} items={synthesis.risk_parameters.avoid_when || []}
color="text-rose-400" color="text-rose-400"
@@ -499,7 +499,7 @@ export default function SuperContexte() {
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<BookOpen className="w-4 h-4 text-slate-400" /> <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>
<div className="space-y-3"> <div className="space-y-3">
{Object.entries(byCategory).map(([cat, entries]: [string, any]) => ( {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 }> = { 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" /> }, 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" /> }, 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 }) { function AiCallRow({ call }: { call: any }) {
@@ -126,19 +126,19 @@ function AiCallRow({ call }: { call: any }) {
{/* Token breakdown */} {/* Token breakdown */}
{totalTokens > 0 && ( {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"> <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>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>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>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> </div>
)} )}
{/* Pane selector */} {/* Pane selector */}
<div className="flex gap-0 border-b border-slate-800"> <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: 'user' as const, label: 'User prompt', icon: <MessageSquare className="w-3 h-3" /> },
{ key: 'system' as const, label: 'Prompt système', icon: <Brain className="w-3 h-3" /> }, { key: 'system' as const, label: 'System prompt', icon: <Brain className="w-3 h-3" /> },
{ key: 'response' as const, label: 'ponse IA', icon: <Zap className="w-3 h-3" /> }, { key: 'response' as const, label: 'AI response', icon: <Zap className="w-3 h-3" /> },
]).map(p => ( ]).map(p => (
<button <button
key={p.key} key={p.key}
@@ -157,13 +157,13 @@ function AiCallRow({ call }: { call: any }) {
{/* Content */} {/* Content */}
<div className="p-3 bg-black/40 max-h-[500px] overflow-y-auto"> <div className="p-3 bg-black/40 max-h-[500px] overflow-y-auto">
{activePane === 'user' && ( {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' && ( {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' && ( {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>
</div> </div>
@@ -176,17 +176,17 @@ function AiCallsSection({ runId }: { runId: string }) {
const { data, isLoading } = useAiCallLogs(runId) const { data, isLoading } = useAiCallLogs(runId)
const calls: any[] = data?.calls ?? [] 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 ( if (calls.length === 0) return (
<div className="p-4 text-xs text-slate-600 text-center"> <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> </div>
) )
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<div className="text-[10px] text-slate-500 px-1 font-mono"> <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> </div>
{calls.map(c => <AiCallRow key={c.id} call={c} />)} {calls.map(c => <AiCallRow key={c.id} call={c} />)}
</div> </div>
@@ -221,10 +221,10 @@ function ContextTab() {
{/* Left: list of snapshots */} {/* Left: list of snapshots */}
<div className="card overflow-y-auto max-h-[700px]"> <div className="card overflow-y-auto max-h-[700px]">
{loadingList ? ( {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 ? ( ) : snapshots.length === 0 ? (
<div className="p-4 text-xs text-slate-600"> <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>
) : ( ) : (
<div className="divide-y divide-slate-800"> <div className="divide-y divide-slate-800">
@@ -250,12 +250,12 @@ function ContextTab() {
{!selectedRunId ? ( {!selectedRunId ? (
<div className="p-8 text-center text-slate-600 text-sm flex-1"> <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" /> <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> </div>
) : loadingSnap ? ( ) : 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 ? ( ) : !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 */} {/* Header strip with meta + replay */}
@@ -273,7 +273,7 @@ function ContextTab() {
<div className="flex items-center gap-2 ml-auto"> <div className="flex items-center gap-2 ml-auto">
<input <input
className="bg-slate-800 border border-slate-700 rounded px-2 py-1 text-[10px] text-slate-300 w-48" 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} value={replayNotes}
onChange={e => setReplayNotes(e.target.value)} 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" 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 {replaying
? <><Loader2 className="w-3 h-3 animate-spin" /> Replay en cours…</> ? <><Loader2 className="w-3 h-3 animate-spin" /> Replaying</>
: <><Play className="w-3 h-3" /> Rejouer ce cycle</>} : <><Play className="w-3 h-3" /> Replay this cycle</>}
</button> </button>
</div> </div>
</div> </div>
@@ -294,7 +294,7 @@ function ContextTab() {
<div className="border-b border-slate-800 bg-green-950/20 p-3"> <div className="border-b border-slate-800 bg-green-950/20 p-3">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<span className="text-xs font-semibold text-green-400"> <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>
<span className="text-[10px] text-slate-500">à {fmtTs(replayResult.replayed_at)}</span> <span className="text-[10px] text-slate-500">à {fmtTs(replayResult.replayed_at)}</span>
</div> </div>
@@ -312,8 +312,8 @@ function ContextTab() {
{/* Sub-tabs: Contexte / Appels IA */} {/* Sub-tabs: Contexte / Appels IA */}
<div className="flex gap-0 border-b border-slate-800 bg-slate-900/40 shrink-0"> <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: 'context' as const, label: 'AI Context', icon: <Brain className="w-3 h-3" /> },
{ key: 'calls' as const, label: 'Appels IA', icon: <Zap className="w-3 h-3" /> }, { key: 'calls' as const, label: 'AI Calls', icon: <Zap className="w-3 h-3" /> },
]).map(t => ( ]).map(t => (
<button <button
key={t.key} 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" />} {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={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> </button>
{open && ( {open && (
<pre className="text-[10px] font-mono text-slate-400 bg-slate-950 p-3 overflow-x-auto max-h-80 leading-relaxed"> <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 = () => { 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'] }) }) clearLogs(30, { onSuccess: () => qc.invalidateQueries({ queryKey: ['system-logs'] }) })
} }
} }
@@ -425,8 +425,8 @@ export default function SystemLogs() {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-xl font-bold text-white">Logs Système</h1> <h1 className="text-xl font-bold text-white">System Logs</h1>
<p className="text-xs text-slate-500 mt-0.5">Erreurs, avertissements et contextes complets des cycles IA</p> <p className="text-xs text-slate-500 mt-0.5">Errors, warnings and full AI cycle contexts</p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <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" 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')} /> <RefreshCw className={clsx('w-3.5 h-3.5', (isLoading || isRefetching) && 'animate-spin')} />
Actualiser Refresh
</button> </button>
{activeTab === 'logs' && ( {activeTab === 'logs' && (
<button <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" 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" /> <Trash2 className="w-3.5 h-3.5" />
Purger &gt;30j Purge &gt;30d
</button> </button>
)} )}
</div> </div>
@@ -452,8 +452,8 @@ export default function SystemLogs() {
{/* Tabs */} {/* Tabs */}
<div className="flex gap-1 border-b border-slate-800"> <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: 'logs', label: 'System Logs', icon: <Info className="w-3.5 h-3.5" /> },
{ key: 'context', label: 'Contexte IA', icon: <Brain 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 => ( ] as const).map(tab => (
<button <button
key={tab.key} key={tab.key}
@@ -493,7 +493,7 @@ export default function SystemLogs() {
{level} · {counts[level as keyof typeof counts] ?? 0} {level} · {counts[level as keyof typeof counts] ?? 0}
</button> </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> </div>
{/* Filters */} {/* Filters */}
@@ -505,7 +505,7 @@ export default function SystemLogs() {
value={filters.source ?? ''} value={filters.source ?? ''}
onChange={e => applyFilter('source', e.target.value)} 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>)} {sources.map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
</div> </div>
@@ -517,7 +517,7 @@ export default function SystemLogs() {
value={filters.cycle_id ?? ''} value={filters.cycle_id ?? ''}
onChange={e => applyFilter('cycle_id', e.target.value)} onChange={e => applyFilter('cycle_id', e.target.value)}
> >
<option value="">Tous</option> <option value="">All</option>
{cycles.map(c => ( {cycles.map(c => (
<option key={c.cycle_id} value={c.cycle_id}> <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)} {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 className="grid grid-cols-2 gap-2">
<div> <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 <input
type="date" type="date"
className="w-full bg-slate-800 border border-slate-700 rounded px-2 py-1 text-xs text-slate-300" 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>
<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 <input
type="date" type="date"
className="w-full bg-slate-800 border border-slate-700 rounded px-2 py-1 text-xs text-slate-300" 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 */} {/* Log table */}
<div className="card overflow-hidden"> <div className="card overflow-hidden">
{isLoading ? ( {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 ? ( ) : logs.length === 0 ? (
<div className="p-8 text-center text-slate-600 text-sm"> <div className="p-8 text-center text-slate-600 text-sm">
Aucun log trouvé pour ces filtres.<br /> No logs found for these filters.<br />
<span className="text-xs text-slate-700">Les WARNING et ERROR des cycles apparaissent ici automatiquement.</span> <span className="text-xs text-slate-700">WARNING and ERROR entries from cycles appear here automatically.</span>
</div> </div>
) : ( ) : (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-xs"> <table className="w-full text-xs">
<thead> <thead>
<tr className="border-b border-slate-700 text-[9px] uppercase tracking-wide text-slate-500"> <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 whitespace-nowrap">Timestamp</th>
<th className="px-3 py-2 text-left font-medium">Niveau</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">Source</th>
<th className="px-3 py-2 text-left font-medium">Cycle</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> <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() { async function fetchLatest() {
const r = await fetch('/api/var/latest') 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() return r.json()
} }
@@ -52,7 +52,7 @@ function MetricCard({
<div className="text-[11px] text-slate-500 mb-3">{method}</div> <div className="text-[11px] text-slate-500 mb-3">{method}</div>
<div className="space-y-2"> <div className="space-y-2">
<div> <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')}> <div className={clsx('text-2xl font-bold', displayed < 0 ? 'text-red-400' : 'text-emerald-400')}>
{pct(displayed)} {pct(displayed)}
</div> </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"> <div className="flex flex-col items-center justify-center py-24 gap-6">
<ShieldAlert className="w-16 h-16 text-slate-700" /> <ShieldAlert className="w-16 h-16 text-slate-700" />
<div className="text-center"> <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"> <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. The VaR calculation does not run automatically to avoid unnecessary network calls.
Cliquez sur "Calculer maintenant" ou configurez le scheduler dans la Configuration. Click "Compute now" or configure the scheduler in Settings.
</p> </p>
</div> </div>
<button <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" 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')} /> <Play className={clsx('w-4 h-4', computing && 'animate-pulse')} />
{computing ? 'Calcul en cours…' : 'Calculer maintenant'} {computing ? 'Computing…' : 'Compute now'}
</button> </button>
</div> </div>
) )
@@ -180,15 +180,15 @@ export default function VaRAnalysis() {
<div> <div>
<div className="flex items-center gap-3 mb-1"> <div className="flex items-center gap-3 mb-1">
<ShieldAlert className="w-6 h-6 text-red-400" /> <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> </div>
<p className="text-slate-400 text-sm"> <p className="text-slate-400 text-sm">
Approche delta Black-Scholes Delta Black-Scholes approach
{displayedMeta && ( {displayedMeta && (
<span className="ml-2 text-slate-500"> <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' && ( {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> </span>
)} )}
@@ -199,7 +199,7 @@ export default function VaRAnalysis() {
<div className="flex flex-wrap gap-3 items-end"> <div className="flex flex-wrap gap-3 items-end">
{/* Confidence */} {/* Confidence */}
<div> <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"> <div className="flex gap-1">
{[0.90, 0.95, 0.99].map(c => ( {[0.90, 0.95, 0.99].map(c => (
<button key={c} onClick={() => setConfidence(c)} <button key={c} onClick={() => setConfidence(c)}
@@ -218,14 +218,14 @@ export default function VaRAnalysis() {
<button key={h} onClick={() => setHorizon(h)} <button key={h} onClick={() => setHorizon(h)}
className={clsx('px-2.5 py-1.5 rounded text-xs font-semibold', 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')}> horizon === h ? 'bg-violet-600 text-white' : 'bg-dark-700 text-slate-400 hover:text-white')}>
{h}j {h}d
</button> </button>
))} ))}
</div> </div>
</div> </div>
{/* Lookback */} {/* Lookback */}
<div> <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"> <div className="flex gap-1">
{[63, 126, 252].map(l => ( {[63, 126, 252].map(l => (
<button key={l} onClick={() => setLookback(l)} <button key={l} onClick={() => setLookback(l)}
@@ -238,7 +238,7 @@ export default function VaRAnalysis() {
</div> </div>
{/* IV */} {/* IV */}
<div> <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"> <div className="flex gap-1">
{[0.15, 0.20, 0.30, 0.40].map(v => ( {[0.15, 0.20, 0.30, 0.40].map(v => (
<button key={v} onClick={() => setIv(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" 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')} /> <Play className={clsx('w-3.5 h-3.5', computing && 'animate-pulse')} />
{computing ? 'Calcul…' : 'Calculer'} {computing ? 'Computing…' : 'Compute'}
</button> </button>
</div> </div>
</div> </div>
@@ -265,7 +265,7 @@ export default function VaRAnalysis() {
{backendDown && ( {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"> <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" /> <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> </div>
)} )}
@@ -281,12 +281,12 @@ export default function VaRAnalysis() {
{snapshots.length > 1 && ( {snapshots.length > 1 && (
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<Database className="w-3.5 h-3.5 text-slate-500" /> <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 <button
onClick={() => setActiveSnap(null)} 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')} 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> </button>
{snapshots.slice(1).map((s: any) => ( {snapshots.slice(1).map((s: any) => (
<button <button
@@ -307,7 +307,7 @@ export default function VaRAnalysis() {
{/* Empty / loading state */} {/* Empty / loading state */}
{loadingLatest && !displayedResult && ( {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 && ( {!loadingLatest && !displayedResult && !backendDown && (
@@ -322,9 +322,9 @@ export default function VaRAnalysis() {
{[ {[
{ label: 'Positions', val: String(portfolio.n_positions) }, { label: 'Positions', val: String(portfolio.n_positions) },
{ label: 'Notionnel', val: `${portfolio.total_notional_eur.toLocaleString('fr-FR', { maximumFractionDigits: 0 })} €` }, { label: 'Notionnel', val: `${portfolio.total_notional_eur.toLocaleString('fr-FR', { maximumFractionDigits: 0 })} €` },
{ label: 'Confiance', val: `${portfolio.confidence_pct}%` }, { label: 'Confidence', val: `${portfolio.confidence_pct}%` },
{ label: 'Lookback', val: `${portfolio.lookback_days}j` }, { label: 'Lookback', val: `${portfolio.lookback_days}d` },
{ label: 'Source', val: portfolio.data_source === 'simulated' ? '⚠ Simulée' : '✓ Live' }, { label: 'Source', val: portfolio.data_source === 'simulated' ? '⚠ Simulated' : '✓ Live' },
].map(({ label, val }) => ( ].map(({ label, val }) => (
<div key={label} className="bg-dark-800 border border-slate-700/30 rounded-lg px-3 py-2"> <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> <div className="text-slate-500">{label}</div>
@@ -336,21 +336,21 @@ export default function VaRAnalysis() {
{/* 3 VaR method cards */} {/* 3 VaR method cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<MetricCard <MetricCard
label="Historique" method="Percentile empirique" label="Historical" method="Empirical percentile"
var1d={varData.historical.var_1d_pct} varNd={varData.historical.var_nd_pct} var1d={varData.historical.var_1d_pct} varNd={varData.historical.var_nd_pct}
cvar={varData.historical.cvar_pct} cvar={varData.historical.cvar_pct}
varEur={varData.historical.var_1d_eur} cvarEur={varData.historical.cvar_eur} varEur={varData.historical.var_1d_eur} cvarEur={varData.historical.cvar_eur}
horizon={horizon} colorClass="border-blue-700/40" horizon={horizon} colorClass="border-blue-700/40"
/> />
<MetricCard <MetricCard
label="Paramétrique" method="Distribution normale" label="Parametric" method="Normal distribution"
var1d={varData.parametric.var_1d_pct} varNd={varData.parametric.var_nd_pct} var1d={varData.parametric.var_1d_pct} varNd={varData.parametric.var_nd_pct}
cvar={varData.parametric.cvar_pct} cvar={varData.parametric.cvar_pct}
varEur={varData.parametric.var_1d_eur} cvarEur={varData.parametric.cvar_eur} varEur={varData.parametric.var_1d_eur} cvarEur={varData.parametric.cvar_eur}
horizon={horizon} colorClass="border-violet-700/40" horizon={horizon} colorClass="border-violet-700/40"
/> />
<MetricCard <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} var1d={varData.monte_carlo.var_1d_pct} varNd={varData.monte_carlo.var_nd_pct}
cvar={varData.monte_carlo.cvar_pct} cvar={varData.monte_carlo.cvar_pct}
varEur={varData.monte_carlo.var_1d_eur} cvarEur={varData.monte_carlo.cvar_eur} 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="bg-dark-800 border border-slate-700/40 rounded-xl p-4">
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
<Activity className="w-4 h-4 text-slate-400" /> <Activity className="w-4 h-4 text-slate-400" />
<h3 className="text-sm font-semibold text-white">Distribution des Retours</h3> <h3 className="text-sm font-semibold text-white">Return Distribution</h3>
<span className="text-[10px] text-slate-600 ml-auto">trait rouge = VaR hist.</span> <span className="text-[10px] text-slate-600 ml-auto">red line = hist. VaR</span>
</div> </div>
{histogram.length > 0 ? ( {histogram.length > 0 ? (
<ResponsiveContainer width="100%" height={210}> <ResponsiveContainer width="100%" height={210}>
@@ -383,7 +383,7 @@ export default function VaRAnalysis() {
</BarChart> </BarChart>
</ResponsiveContainer> </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> </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="bg-dark-800 border border-slate-700/40 rounded-xl p-4">
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
<TrendingDown className="w-4 h-4 text-red-400" /> <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> </div>
{rolling.length > 0 ? ( {rolling.length > 0 ? (
<ResponsiveContainer width="100%" height={210}> <ResponsiveContainer width="100%" height={210}>
@@ -406,7 +406,7 @@ export default function VaRAnalysis() {
</LineChart> </LineChart>
</ResponsiveContainer> </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>
</div> </div>
@@ -415,7 +415,7 @@ export default function VaRAnalysis() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Positions */} {/* Positions */}
<div className="bg-dark-800 border border-slate-700/40 rounded-xl p-4"> <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"> <div className="space-y-1.5">
{positions.map((p: any, i: number) => ( {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"> <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 */} {/* Kupiec backtest */}
{backtest && ( {backtest && (
<div className="bg-dark-800 border border-slate-700/40 rounded-xl p-4"> <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"> <div className="space-y-2.5">
{[ {[
{ label: 'Observations', val: String(backtest.n_observations) }, { label: 'Observations', val: String(backtest.n_observations) },
{ label: 'Violations VaR', val: String(backtest.n_breaches) }, { label: 'VaR Violations', 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: 'Actual rate', 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: 'Expected rate', val: `${backtest.expected_breach_rate_pct}%`, color: 'text-slate-300' },
].map(({ label, val, color }) => ( ].map(({ label, val, color }) => (
<div key={label} className="flex justify-between text-sm"> <div key={label} className="flex justify-between text-sm">
<span className="text-slate-400">{label}</span> <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-emerald-900/20 border border-emerald-700/40 text-emerald-400'
: 'bg-red-900/20 border border-red-700/40 text-red-400')}> : 'bg-red-900/20 border border-red-700/40 text-red-400')}>
{backtest.kupiec_ok {backtest.kupiec_ok
? '✓ Modèle validé — violations dans la tolérance' ? '✓ Model validated — violations within tolerance'
: '✗ Excès de violations — modèle sous-estime le risque'} : '✗ Excess violations — model underestimates risk'}
</div> </div>
<div className="text-[10px] text-slate-600"> <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> </div>
</div> </div>