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:
File diff suppressed because it is too large
Load Diff
@@ -49,7 +49,7 @@ def _max_similarity_vs_existing(candidate_kws: List[str], existing: List[Dict])
|
||||
return max((_jaccard(candidate_kws, p.get("keywords") or []) for p in existing), default=0.0)
|
||||
|
||||
|
||||
_EMBED_SIM_THRESHOLD = 0.75 # seuil cosinus pour considérer deux patterns comme doublons
|
||||
_EMBED_SIM_THRESHOLD = 0.75 # cosine threshold to consider two patterns as duplicates
|
||||
|
||||
|
||||
def _is_duplicate_pattern(
|
||||
@@ -59,8 +59,8 @@ def _is_duplicate_pattern(
|
||||
jaccard_threshold: float = 0.30,
|
||||
) -> bool:
|
||||
"""
|
||||
Retourne True si le candidat est trop similaire à un pattern existant.
|
||||
Essaie les embeddings cosinus (Sprint 4.3) avec fallback Jaccard.
|
||||
Returns True if the candidate is too similar to an existing pattern.
|
||||
Tries cosine embeddings (Sprint 4.3) with Jaccard fallback.
|
||||
"""
|
||||
if not existing:
|
||||
return False
|
||||
@@ -77,7 +77,7 @@ def _is_duplicate_pattern(
|
||||
api_key=api_key,
|
||||
candidate_id=candidate.get("id"),
|
||||
)
|
||||
if sim > 0: # embedding a fonctionné
|
||||
if sim > 0: # embedding worked
|
||||
return sim >= _EMBED_SIM_THRESHOLD
|
||||
except Exception as _emb_err:
|
||||
logger.debug(f"[Cycle] Embedding failed, fallback Jaccard: {_emb_err}")
|
||||
@@ -104,13 +104,13 @@ def _run_portfolio_monitor(risk: dict, cycle_id: str) -> dict:
|
||||
n_alerts = len(risk.get("alerts", []))
|
||||
|
||||
system_prompt = (
|
||||
"Tu es un risk manager spécialisé dans les portefeuilles d'options géopolitiques. "
|
||||
"Analyse l'état du portefeuille simulé ci-dessous et génère des recommandations concrètes. "
|
||||
"Réponds en JSON avec ce schéma exact: "
|
||||
'{"assessment": "<2 phrases max sur l\'état global>", '
|
||||
"You are a risk manager specializing in geopolitical options portfolios. "
|
||||
"Analyze the state of the simulated portfolio below and generate concrete recommendations. "
|
||||
"Respond in JSON with this exact schema: "
|
||||
'{"assessment": "<max 2 sentences on the overall state>", '
|
||||
'"actions": [{"priority": "high|medium", "type": "close_trade|rebalance|monitor", '
|
||||
'"trade_id": <int ou null>, "underlying": "<ticker>", "reason": "<raison courte>"}], '
|
||||
'"rebalance_suggestion": "<suggestion concrète de rééquilibrage en 1 phrase>"}'
|
||||
'"trade_id": <int or null>, "underlying": "<ticker>", "reason": "<short reason>"}], '
|
||||
'"rebalance_suggestion": "<concrete rebalancing suggestion in 1 sentence>"}'
|
||||
)
|
||||
user_prompt = context
|
||||
|
||||
|
||||
@@ -99,13 +99,13 @@ def get_portfolio_concentration(open_trades: List[Dict]) -> Dict[str, int]:
|
||||
def build_portfolio_context_block(open_trades: List[Dict], concentration: Dict[str, int]) -> str:
|
||||
"""Build a prompt section describing current portfolio for injection into AI prompts."""
|
||||
if not open_trades:
|
||||
return "\n## PORTEFEUILLE ACTUEL\nAucun trade en cours — portefeuille vide.\n"
|
||||
return "\n## CURRENT PORTFOLIO\nNo open trades — empty portfolio.\n"
|
||||
|
||||
total = len(open_trades)
|
||||
conc_sorted = sorted(concentration.items(), key=lambda x: -x[1])
|
||||
conc_str = " | ".join(f"{cls.upper()}: {n}" for cls, n in conc_sorted)
|
||||
|
||||
lines: List[str] = [f"Total: {total} trade(s) ouvert(s) | Concentration: {conc_str}", ""]
|
||||
lines: List[str] = [f"Total: {total} open trade(s) | Concentration: {conc_str}", ""]
|
||||
|
||||
for t in open_trades:
|
||||
sym = t["underlying"]
|
||||
@@ -118,27 +118,27 @@ def build_portfolio_context_block(open_trades: List[Dict], concentration: Dict[s
|
||||
pat = t.get("pattern_name", "")
|
||||
entry = t.get("entry_price")
|
||||
cur = t.get("current_price")
|
||||
ep_str = f"entrée {entry:.2f} → actuel {cur:.2f}" if entry and cur else ""
|
||||
ep_str = f"entry {entry:.2f} → current {cur:.2f}" if entry and cur else ""
|
||||
|
||||
line = f" • {sym} | {strat} [{cls}] | {held}j tenu / {rem}j restants | J-1: {m1d_str} | J-5: {m5d_str}"
|
||||
line = f" • {sym} | {strat} [{cls}] | {held}d held / {rem}d remaining | D-1: {m1d_str} | D-5: {m5d_str}"
|
||||
if ep_str:
|
||||
line += f" | {ep_str}"
|
||||
if pat:
|
||||
line += f" | thèse: «{pat}»"
|
||||
line += f" | thesis: «{pat}»"
|
||||
lines.append(line)
|
||||
|
||||
# Identify overweight classes
|
||||
overweight = [cls for cls, n in conc_sorted if n >= 3]
|
||||
ow_str = ", ".join(overweight) if overweight else "aucune"
|
||||
ow_str = ", ".join(overweight) if overweight else "none"
|
||||
|
||||
block = (
|
||||
"\n## PORTEFEUILLE ACTUEL — POSITIONS OUVERTES\n"
|
||||
"\n## CURRENT PORTFOLIO — OPEN POSITIONS\n"
|
||||
+ "\n".join(lines)
|
||||
+ f"\n\nClasses surpondérées (≥3 trades): {ow_str}\n"
|
||||
+ "\n⚠️ CONSIGNES IMPÉRATIVES (non négociables):\n"
|
||||
+ "1. NE PAS suggérer un nouveau trade sur un sous-jacent déjà en portefeuille — doublement interdit.\n"
|
||||
+ "2. Signaler explicitement dans 'rationale' si une suggestion CONTREDIT une position ouverte (signal de clôture potentiel).\n"
|
||||
+ "3. Éviter d'alourdir une classe surpondérée (≥3 trades) sauf catalyseur exceptionnel justifié.\n"
|
||||
+ "4. Un signal opposé à une position ouverte = opportunité de SORTIE à documenter, pas d'entrée inversée.\n"
|
||||
+ f"\n\nOverweight classes (≥3 trades): {ow_str}\n"
|
||||
+ "\n⚠️ MANDATORY RULES (non-negotiable):\n"
|
||||
+ "1. DO NOT suggest a new trade on an underlying already in the portfolio — strictly forbidden.\n"
|
||||
+ "2. Explicitly flag in 'rationale' if a suggestion CONTRADICTS an open position (potential exit signal).\n"
|
||||
+ "3. Avoid adding to an overweight class (≥3 trades) unless an exceptional justified catalyst exists.\n"
|
||||
+ "4. A signal opposing an open position = EXIT opportunity to document, not a reverse entry.\n"
|
||||
)
|
||||
return block
|
||||
|
||||
@@ -33,25 +33,25 @@ const BUCKET_ICONS: Record<string, string> = {
|
||||
}
|
||||
|
||||
export const CATEGORIES = [
|
||||
{ key: 'all', label: 'Tous' },
|
||||
{ key: 'energy', label: '⛽ Énergie' },
|
||||
{ key: 'metals', label: '🥇 Métaux' },
|
||||
{ key: 'all', label: 'All' },
|
||||
{ key: 'energy', label: '⛽ Energy' },
|
||||
{ key: 'metals', label: '🥇 Metals' },
|
||||
{ key: 'agriculture', label: '🌾 Agri' },
|
||||
{ key: 'indices', label: '📊 Indices' },
|
||||
{ key: 'equities', label: '📈 Actions' },
|
||||
{ key: 'equities', label: '📈 Equities' },
|
||||
{ key: 'forex', label: '💱 Forex' },
|
||||
]
|
||||
|
||||
export const THEMATIC_CATEGORIES = [
|
||||
{ key: 'all', label: 'Toutes' },
|
||||
{ key: 'géopolitique', label: '⚔️ Géopo' },
|
||||
{ key: 'all', label: 'All' },
|
||||
{ key: 'géopolitique', label: '⚔️ Geopo' },
|
||||
{ key: 'macro_monétaire', label: '🏦 Macro' },
|
||||
{ key: 'technique', label: '📐 Technique' },
|
||||
{ key: 'technique', label: '📐 Technical' },
|
||||
{ key: 'commodités_supply',label: '🛢️ Supply' },
|
||||
{ key: 'risk_off', label: '🌩️ Risk-off' },
|
||||
{ key: 'flux_saisonnier', label: '📅 Flux' },
|
||||
{ key: 'géo_économique', label: '🌐 Géo-éco' },
|
||||
{ key: 'crédit_stress', label: '💳 Crédit' },
|
||||
{ key: 'flux_saisonnier', label: '📅 Flows' },
|
||||
{ key: 'géo_économique', label: '🌐 Geo-eco' },
|
||||
{ key: 'crédit_stress', label: '💳 Credit' },
|
||||
]
|
||||
|
||||
const SIGNAL_DIR: Record<string, { label: string; color: string }> = {
|
||||
@@ -80,12 +80,12 @@ export interface TradeItem {
|
||||
}
|
||||
|
||||
const BIAS_DISPLAY: Record<string, { label: string; color: string }> = {
|
||||
'bullish+': { label: '★★ Compatible', color: '#10b981' },
|
||||
'bullish+': { label: '★★ Aligned', color: '#10b981' },
|
||||
'bullish': { label: '★ Compatible', color: '#34d399' },
|
||||
'neutral': { label: '→ Neutre', color: '#64748b' },
|
||||
'bearish': { label: '✗ Défavorable', color: '#f97316' },
|
||||
'neutral': { label: '→ Neutral', color: '#64748b' },
|
||||
'bearish': { label: '✗ Unfavorable', color: '#f97316' },
|
||||
'bearish+': { label: '✗✗ Contra', color: '#ef4444' },
|
||||
'defensive':{ label: '⚠ Défensif', color: '#f59e0b' },
|
||||
'defensive':{ label: '⚠ Defensive', color: '#f59e0b' },
|
||||
}
|
||||
|
||||
// ── IBKR helpers ──────────────────────────────────────────────────────────────
|
||||
@@ -234,33 +234,33 @@ function IBKRTicket({ strategy, underlying, strikeGuidance, expiryDays, entryPri
|
||||
return (
|
||||
<div className="bg-blue-950/20 border border-blue-700/30 rounded p-3 space-y-2.5">
|
||||
<div className="text-[10px] font-bold text-blue-300 uppercase tracking-wide flex items-center gap-1.5">
|
||||
<Terminal className="w-3 h-3" /> Ticket IBKR
|
||||
{!entryPrice && <span className="ml-auto text-slate-600 font-normal normal-case">Prix non disponible — vérifier dans IBKR</span>}
|
||||
<Terminal className="w-3 h-3" /> IBKR Ticket
|
||||
{!entryPrice && <span className="ml-auto text-slate-600 font-normal normal-case">Price unavailable — check in IBKR</span>}
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3 text-[10px]">
|
||||
<div>
|
||||
<div className="text-slate-600 mb-0.5">Sous-jacent</div>
|
||||
<div className="text-slate-600 mb-0.5">Underlying</div>
|
||||
<div className="text-white font-mono font-bold text-sm">{underlying}</div>
|
||||
<div className="text-slate-600 text-[9px]">Security Type: OPT</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-slate-600 mb-0.5">Stratégie</div>
|
||||
<div className="text-slate-600 mb-0.5">Strategy</div>
|
||||
<div className="text-slate-200 font-semibold">{strategy}</div>
|
||||
{entryPrice && <div className="text-slate-600 text-[9px]">Sous-jacent: ${entryPrice.toFixed(2)}</div>}
|
||||
{entryPrice && <div className="text-slate-600 text-[9px]">Underlying: ${entryPrice.toFixed(2)}</div>}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-slate-600 mb-0.5">Expiration</div>
|
||||
<div className="text-slate-600 mb-0.5">Expiry</div>
|
||||
<div className="text-slate-200 font-mono">
|
||||
{expiryDate ? format(expiryDate, 'd MMM yyyy', { locale: fr }) : expiryDays ? `~${expiryDays}j` : '—'}
|
||||
{expiryDate ? format(expiryDate, 'd MMM yyyy', { locale: fr }) : expiryDays ? `~${expiryDays}d` : '—'}
|
||||
</div>
|
||||
<div className="text-slate-600 text-[9px]">{expiryDays ? `${expiryDays}j DTE` : ''}</div>
|
||||
<div className="text-slate-600 text-[9px]">{expiryDays ? `${expiryDays}d DTE` : ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{legs.map((leg, i) => (
|
||||
<div key={i} className="flex items-center gap-2 bg-slate-900/70 rounded px-2.5 py-1.5 font-mono text-[10px]">
|
||||
<span className={clsx('w-8 font-bold', leg.action === 'BUY' ? 'text-emerald-400' : 'text-red-400')}>{leg.action}</span>
|
||||
<span className="text-slate-400">1 contrat</span>
|
||||
<span className="text-slate-400">1 contract</span>
|
||||
<span className={clsx('font-bold w-10 text-center', leg.type === 'CALL' ? 'text-blue-300' : leg.type === 'PUT' ? 'text-orange-300' : 'text-slate-300')}>{leg.type}</span>
|
||||
<span className="text-slate-600">Strike</span>
|
||||
<span className="text-yellow-300 font-bold">{fmtStrike(leg.strike)}</span>
|
||||
@@ -270,17 +270,17 @@ function IBKRTicket({ strategy, underlying, strikeGuidance, expiryDays, entryPri
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3 text-[10px]">
|
||||
<div>
|
||||
<div className="text-slate-600 mb-0.5">Type d'ordre</div>
|
||||
<div className="text-slate-600 mb-0.5">Order type</div>
|
||||
<div className="text-slate-300 font-semibold">LIMIT</div>
|
||||
<div className="text-slate-600 text-[9px]">Prix = prime dans IBKR</div>
|
||||
<div className="text-slate-600 text-[9px]">Price = premium in IBKR</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-slate-600 mb-0.5">Budget max</div>
|
||||
<div className="text-slate-300 font-mono">{maxLoss ? `${Math.abs(maxLoss).toFixed(0)}€` : '~1 000€'}</div>
|
||||
<div className="text-slate-600 text-[9px]">Perte max estimée</div>
|
||||
<div className="text-slate-600 mb-0.5">Max budget</div>
|
||||
<div className="text-slate-300 font-mono">{maxLoss ? `${Math.abs(maxLoss).toFixed(0)}€` : '~1,000€'}</div>
|
||||
<div className="text-slate-600 text-[9px]">Estimated max loss</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-slate-600 mb-0.5">Cible</div>
|
||||
<div className="text-slate-600 mb-0.5">Target</div>
|
||||
<div className="text-emerald-400 font-mono">{targetGain ? `+${targetGain.toFixed(0)}€` : '—'}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -373,7 +373,7 @@ export function TradeCard({ item, onAdd, macroInfo, addedInfo, profiles }: {
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="ml-2 shrink-0 text-xs text-slate-500 bg-dark-600 border border-slate-700/40 rounded px-1.5 py-0.5 whitespace-nowrap">à scorer</span>
|
||||
<span className="ml-2 shrink-0 text-xs text-slate-500 bg-dark-600 border border-slate-700/40 rounded px-1.5 py-0.5 whitespace-nowrap">to score</span>
|
||||
)}
|
||||
</div>
|
||||
{convergenceCount > 0 && (
|
||||
@@ -404,7 +404,7 @@ export function TradeCard({ item, onAdd, macroInfo, addedInfo, profiles }: {
|
||||
<><span className="text-slate-700">·</span>
|
||||
{matchedProfile
|
||||
? <span className="font-semibold" style={{ color: matchedProfile.color }}>● {matchedProfile.name}</span>
|
||||
: <span className="text-red-400/80">✗ {gainPct === 0 ? 'Gain non défini' : 'Aucun profil'}</span>
|
||||
: <span className="text-red-400/80">✗ {gainPct === 0 ? 'Gain undefined' : 'No profile'}</span>
|
||||
}</>
|
||||
)}
|
||||
</div>
|
||||
@@ -435,7 +435,7 @@ export function TradeCard({ item, onAdd, macroInfo, addedInfo, profiles }: {
|
||||
<button onClick={() => setExpanded(!expanded)}
|
||||
className="flex items-center gap-1 text-xs text-slate-500 hover:text-slate-300 mb-1.5 transition-colors">
|
||||
{expanded ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
|
||||
{expanded ? 'Masquer la justification' : '↳ Justification du score'}
|
||||
{expanded ? 'Hide breakdown' : '↳ Score breakdown'}
|
||||
</button>
|
||||
{expanded && (
|
||||
<div className="mb-2">
|
||||
@@ -463,7 +463,7 @@ export function TradeCard({ item, onAdd, macroInfo, addedInfo, profiles }: {
|
||||
{addedInfo && (
|
||||
<div className="flex items-center gap-1.5 mb-2 text-[10px] text-emerald-400 bg-emerald-900/20 border border-emerald-700/30 rounded px-2 py-1">
|
||||
<CheckCircle2 className="w-3 h-3 shrink-0" />
|
||||
<span>Ajouté le {format(new Date(addedInfo.entry_date), "d MMM yyyy", { locale: fr })}</span>
|
||||
<span>Added on {format(new Date(addedInfo.entry_date), "d MMM yyyy", { locale: fr })}</span>
|
||||
</div>
|
||||
)}
|
||||
<button onClick={() => onAdd(item)}
|
||||
@@ -473,7 +473,7 @@ export function TradeCard({ item, onAdd, macroInfo, addedInfo, profiles }: {
|
||||
: 'text-blue-400 hover:text-white hover:bg-blue-600 border-blue-500/30 hover:border-blue-500'
|
||||
)}>
|
||||
<Plus className="w-3 h-3" />
|
||||
{addedInfo ? 'Ajouter à nouveau' : 'Ajouter au portefeuille'}
|
||||
{addedInfo ? 'Add again' : 'Add to portfolio'}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
@@ -560,7 +560,7 @@ export function TradeRow({ item, onAdd, macroInfo, addedInfo, profiles, rank }:
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : <span className="text-slate-600 text-[10px]">à scorer</span>}
|
||||
) : <span className="text-slate-600 text-[10px]">to score</span>}
|
||||
</td>
|
||||
<td className="px-2 py-2 w-20">
|
||||
{effectiveConviction !== null && (
|
||||
@@ -646,7 +646,7 @@ export function TradeRow({ item, onAdd, macroInfo, addedInfo, profiles, rank }:
|
||||
{addedInfo && (
|
||||
<div className="flex items-center gap-1.5 text-[10px] text-emerald-400 bg-emerald-900/20 border border-emerald-700/30 rounded px-2 py-1">
|
||||
<CheckCircle2 className="w-3 h-3 shrink-0" />
|
||||
Ajouté le {format(new Date(addedInfo.entry_date), "d MMM yyyy", { locale: fr })}
|
||||
Added on {format(new Date(addedInfo.entry_date), "d MMM yyyy", { locale: fr })}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
@@ -657,7 +657,7 @@ export function TradeRow({ item, onAdd, macroInfo, addedInfo, profiles, rank }:
|
||||
: 'text-blue-400 hover:text-white hover:bg-blue-600 border-blue-500/30 hover:border-blue-500'
|
||||
)}>
|
||||
<Plus className="w-3 h-3" />
|
||||
{addedInfo ? 'Ajouter à nouveau' : 'Ajouter au portefeuille'}
|
||||
{addedInfo ? 'Add again' : 'Add to portfolio'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -826,7 +826,7 @@ export function TradeIdeasTab() {
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
refetchPositions()
|
||||
setToast({ title: 'Ajouté au portefeuille', sub: `${t.strategy ?? ''} ${t.underlying ?? ''} · ${item.patternName}`.trim() })
|
||||
setToast({ title: 'Added to portfolio', sub: `${t.strategy ?? ''} ${t.underlying ?? ''} · ${item.patternName}`.trim() })
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -844,13 +844,13 @@ export function TradeIdeasTab() {
|
||||
<div className="flex items-center justify-between gap-3 flex-wrap">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h2 className="section-title flex items-center gap-1 mb-0">
|
||||
<Target className="w-3 h-3" /> Idées de trade
|
||||
<Target className="w-3 h-3" /> Trade Ideas
|
||||
</h2>
|
||||
{scoredAt && (
|
||||
<span className="text-xs text-slate-600">— scoré le {format(new Date(scoredAt), "d MMM à HH:mm", { locale: fr })}</span>
|
||||
<span className="text-xs text-slate-600">— scored on {format(new Date(scoredAt), "d MMM HH:mm", { locale: fr })}</span>
|
||||
)}
|
||||
{topScored.length > 0 && (
|
||||
<span className="text-xs text-emerald-500">{topScored.length} scoré{topScored.length > 1 ? 's' : ''} · {allUnscored.length} à scorer</span>
|
||||
<span className="text-xs text-emerald-500">{topScored.length} scored · {allUnscored.length} to score</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
@@ -886,7 +886,7 @@ export function TradeIdeasTab() {
|
||||
'bg-slate-600 text-white': topN === n,
|
||||
'text-slate-400 hover:text-slate-200': topN !== n,
|
||||
})}>
|
||||
{n === 0 ? 'Tous' : `Top ${n}`}
|
||||
{n === 0 ? 'All' : `Top ${n}`}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -904,10 +904,10 @@ export function TradeIdeasTab() {
|
||||
<button onClick={() => scorePatterns({})}
|
||||
disabled={scoring}
|
||||
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white px-3 py-1.5 rounded text-xs font-semibold transition-all">
|
||||
{scoring ? <><RefreshCw className="w-3 h-3 animate-spin" /> Scoring GPT-4o...</> : <><Brain className="w-3 h-3" /> Scorer les patterns</>}
|
||||
{scoring ? <><RefreshCw className="w-3 h-3 animate-spin" /> Scoring GPT-4o...</> : <><Brain className="w-3 h-3" /> Score patterns</>}
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-xs text-slate-600 border border-slate-700/30 rounded px-2 py-1">Clé OpenAI requise</span>
|
||||
<span className="text-xs text-slate-600 border border-slate-700/30 rounded px-2 py-1">OpenAI key required</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -927,16 +927,16 @@ export function TradeIdeasTab() {
|
||||
<tr className="border-b border-slate-700/60 text-slate-600 text-[10px] uppercase tracking-wide">
|
||||
<th className="pl-3 py-2 text-left w-6">#</th>
|
||||
<th className="px-2 py-2 text-left">Pattern / Ticker</th>
|
||||
<th className="px-2 py-2 text-left">Stratégie</th>
|
||||
<th className="px-2 py-2 text-left">Strategy</th>
|
||||
<th className="px-2 py-2 text-right">Score</th>
|
||||
<th className="px-2 py-2 w-20"></th>
|
||||
<th className="px-2 py-2 text-right">EV</th>
|
||||
<th className="px-2 py-2 text-right">Gain</th>
|
||||
<th className="px-2 py-2 text-right">Max/Cible</th>
|
||||
<th className="px-2 py-2">Profil</th>
|
||||
<th className="px-2 py-2 text-right">Move</th>
|
||||
<th className="px-2 py-2 text-right">Max/Target</th>
|
||||
<th className="px-2 py-2">Profile</th>
|
||||
<th className="px-2 py-2">Macro</th>
|
||||
<th className="px-2 py-2 text-right">Entrée</th>
|
||||
<th className="px-2 py-2 text-right">Durée</th>
|
||||
<th className="px-2 py-2 text-right">Entry</th>
|
||||
<th className="px-2 py-2 text-right">Duration</th>
|
||||
<th className="pr-3 py-2 w-6"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -956,7 +956,7 @@ export function TradeIdeasTab() {
|
||||
{topScored.length > 0 && (
|
||||
<div className="flex items-center gap-2 text-xs text-slate-600 mb-3">
|
||||
<span className="border-b border-slate-700/30 flex-1"></span>
|
||||
{allUnscored.length} trade{allUnscored.length > 1 ? 's' : ''} à scorer
|
||||
{allUnscored.length} trade{allUnscored.length > 1 ? 's' : ''} to score
|
||||
<span className="border-b border-slate-700/30 flex-1"></span>
|
||||
</div>
|
||||
)}
|
||||
@@ -982,7 +982,7 @@ export function TradeIdeasTab() {
|
||||
|
||||
{allPatterns.length === 0 && (
|
||||
<div className="card text-center py-8 text-slate-500 text-sm">
|
||||
Démarrer le backend — patterns en cours de chargement…
|
||||
Start the backend — patterns loading…
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -8,23 +8,23 @@ import clsx from 'clsx'
|
||||
|
||||
const nav = [
|
||||
{ to: '/', icon: LayoutDashboard, label: 'Cockpit' },
|
||||
{ to: '/geo', icon: Globe, label: 'Radar Géopolitique' },
|
||||
{ to: '/markets', icon: BarChart2, label: 'Marchés & Prix' },
|
||||
{ to: '/macro', icon: Activity, label: 'Régime Macro' },
|
||||
{ to: '/geo', icon: Globe, label: 'Geopolitical Radar' },
|
||||
{ to: '/markets', icon: BarChart2, label: 'Markets & Prices' },
|
||||
{ to: '/macro', icon: Activity, label: 'Macro Regime' },
|
||||
{ to: '/options', icon: TrendingUp, label: 'Options Lab' },
|
||||
{ to: '/patterns', icon: Zap, label: 'Patterns' },
|
||||
{ to: '/portfolio', icon: DollarSign, label: 'Portefeuille' },
|
||||
{ to: '/journal', icon: BookOpen, label: 'Journal de Bord' },
|
||||
{ to: '/rapport', icon: FileBarChart, label: 'Rapport de Cycle' },
|
||||
{ to: '/super-contexte', icon: Brain, label: 'Super Contexte' },
|
||||
{ to: '/portfolio', icon: DollarSign, label: 'Portfolio' },
|
||||
{ to: '/journal', icon: BookOpen, label: 'Trading Journal' },
|
||||
{ to: '/rapport', icon: FileBarChart, label: 'Cycle Report' },
|
||||
{ to: '/super-contexte', icon: Brain, label: 'Super Context' },
|
||||
{ to: '/analytics', icon: FlaskConical, label: 'Analytics' },
|
||||
{ to: '/analytics-advanced', icon: Microscope, label: 'Analytics Avancées' },
|
||||
{ to: '/analytics-advanced', icon: Microscope, label: 'Advanced Analytics' },
|
||||
{ to: '/risk', icon: ShieldAlert, label: 'Risk Dashboard' },
|
||||
{ to: '/var', icon: Gauge, label: 'VaR Analyse' },
|
||||
{ to: '/position-history', icon: GitCompare, label: 'Historique Positions' },
|
||||
{ to: '/var', icon: Gauge, label: 'VaR Analysis' },
|
||||
{ to: '/position-history', icon: GitCompare, label: 'Position History' },
|
||||
{ to: '/backtest', icon: History, label: 'Backtest' },
|
||||
{ to: '/calendar', icon: Calendar, label: 'Calendrier' },
|
||||
{ to: '/logs', icon: ScrollText, label: 'Logs Système' },
|
||||
{ to: '/calendar', icon: Calendar, label: 'Calendar' },
|
||||
{ to: '/logs', icon: ScrollText, label: 'System Logs' },
|
||||
{ to: '/config', icon: Settings, label: 'Configuration' },
|
||||
]
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function Sidebar() {
|
||||
{/* Geo risk indicator */}
|
||||
{riskScore && (
|
||||
<div className={clsx('mx-3 mt-3 px-3 py-2 rounded border text-xs', riskColors[riskScore.level])}>
|
||||
<div className="font-semibold uppercase tracking-wider">Risque Géo</div>
|
||||
<div className="font-semibold uppercase tracking-wider">Geo Risk</div>
|
||||
<div className="text-lg font-bold">{riskScore.score}/100</div>
|
||||
<div className="capitalize">{riskScore.level}</div>
|
||||
</div>
|
||||
@@ -65,7 +65,7 @@ export default function Sidebar() {
|
||||
{/* Portfolio mini summary */}
|
||||
{summary && (summary.open_positions > 0 || summary.unrealized_pnl !== 0) && (
|
||||
<div className="mx-3 mt-2 px-3 py-2 rounded border border-slate-700/30 bg-dark-700/50 text-xs">
|
||||
<div className="text-slate-500 uppercase tracking-wider text-xs mb-1">Portefeuille</div>
|
||||
<div className="text-slate-500 uppercase tracking-wider text-xs mb-1">Portfolio</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-400">{summary.open_positions} positions</span>
|
||||
<span className={clsx('font-bold', summary.unrealized_pnl >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
@@ -97,7 +97,7 @@ export default function Sidebar() {
|
||||
'bg-dark-700 text-slate-600': !aiStatus?.enabled,
|
||||
})}>
|
||||
<BrainCircuit className="w-3.5 h-3.5" />
|
||||
<span>{aiStatus?.enabled ? 'IA GPT-4o active' : 'IA non configurée'}</span>
|
||||
<span>{aiStatus?.enabled ? 'AI GPT-4o active' : 'AI not configured'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ function ReliabilityTable({ data }: { data: any[] }) {
|
||||
return (
|
||||
<div className="card text-center py-10 text-slate-500">
|
||||
<BarChart2 className="w-8 h-8 mx-auto mb-2 opacity-20" />
|
||||
<div>Aucune donnée de fiabilité</div>
|
||||
<div className="text-xs mt-1">Les données apparaissent après 3+ trades matures (≥35% de l'horizon écoulé) par pattern</div>
|
||||
<div>No reliability data</div>
|
||||
<div className="text-xs mt-1">Data appears after 3+ mature trades (≥35% of horizon elapsed) per pattern</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -22,10 +22,10 @@ function ReliabilityTable({ data }: { data: any[] }) {
|
||||
<th className="text-left py-2 pr-3 font-medium">Pattern</th>
|
||||
<th className="text-right py-2 px-2 font-medium">Trades</th>
|
||||
<th className="text-right py-2 px-2 font-medium">Win Rate</th>
|
||||
<th className="text-right py-2 px-2 font-medium">PnL moyen</th>
|
||||
<th className="text-right py-2 px-2 font-medium">Avg PnL</th>
|
||||
<th className="text-right py-2 px-2 font-medium">Max gain</th>
|
||||
<th className="text-right py-2 px-2 font-medium">Max perte</th>
|
||||
<th className="text-right py-2 px-2 font-medium">Score fiabilité</th>
|
||||
<th className="text-right py-2 px-2 font-medium">Max loss</th>
|
||||
<th className="text-right py-2 px-2 font-medium">Reliability score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800/50">
|
||||
@@ -69,8 +69,8 @@ function CalibrationSection({ data }: { data: any }) {
|
||||
return (
|
||||
<div className="card text-center py-10 text-slate-500">
|
||||
<Target className="w-8 h-8 mx-auto mb-2 opacity-20" />
|
||||
<div>Pas encore de données de calibration</div>
|
||||
<div className="text-xs mt-1">Nécessite des trades matures avec probabilité stockée vs résultat réalisé</div>
|
||||
<div>No calibration data yet</div>
|
||||
<div className="text-xs mt-1">Requires mature trades with stored probability vs realized outcome</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -84,31 +84,31 @@ function CalibrationSection({ data }: { data: any }) {
|
||||
<div className="card text-center">
|
||||
<div className={clsx('text-2xl font-bold font-mono', brierColor)}>{brier_score?.toFixed(3) ?? '—'}</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Brier Score</div>
|
||||
<div className="text-xs text-slate-600">(0 = parfait, 1 = nul)</div>
|
||||
<div className="text-xs text-slate-600">(0 = perfect, 1 = null)</div>
|
||||
</div>
|
||||
<div className="card text-center">
|
||||
<div className="text-2xl font-bold text-white">{sample_size}</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Trades analysés</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Trades analyzed</div>
|
||||
</div>
|
||||
<div className="card text-center">
|
||||
<div className={clsx('text-sm font-bold mt-1', brierColor)}>{interpretation ?? '—'}</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Interprétation</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Interpretation</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Calibration buckets */}
|
||||
{buckets && buckets.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs text-slate-500 mb-2 font-medium">Calibration par décile (prédit vs réalisé)</div>
|
||||
<div className="text-xs text-slate-500 mb-2 font-medium">Calibration by decile (predicted vs realized)</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-slate-500 border-b border-slate-700/30">
|
||||
<th className="text-left py-1.5 pr-3 font-medium">Prob. prédite</th>
|
||||
<th className="text-right py-1.5 px-2 font-medium">Taux réel</th>
|
||||
<th className="text-right py-1.5 px-2 font-medium">Biais</th>
|
||||
<th className="text-left py-1.5 pr-3 font-medium">Predicted prob.</th>
|
||||
<th className="text-right py-1.5 px-2 font-medium">Actual rate</th>
|
||||
<th className="text-right py-1.5 px-2 font-medium">Bias</th>
|
||||
<th className="text-right py-1.5 px-2 font-medium">Trades</th>
|
||||
<th className="py-1.5 pl-3 font-medium">Barre</th>
|
||||
<th className="py-1.5 pl-3 font-medium">Bar</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800/50">
|
||||
@@ -142,7 +142,7 @@ function CalibrationSection({ data }: { data: any }) {
|
||||
</table>
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-600 mt-2">
|
||||
Barre grise = prob. prédite · Barre colorée = taux réalisé · Vert si réel ≥ prédit, rouge sinon
|
||||
Grey bar = predicted prob. · Colored bar = realized rate · Green if actual ≥ predicted, red otherwise
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -166,7 +166,7 @@ export default function Analytics() {
|
||||
<BarChart2 className="w-5 h-5 text-blue-400" /> Analytics & Calibration
|
||||
</h1>
|
||||
<p className="text-xs text-slate-500 mt-0.5">
|
||||
Fiabilité historique des patterns · Calibration probabiliste · Brier score
|
||||
Historical pattern reliability · Probabilistic calibration · Brier score
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -175,18 +175,18 @@ export default function Analytics() {
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||
<div className="card">
|
||||
<div className="text-2xl font-bold text-white font-mono">{reliability.length}</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Patterns avec historique</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Patterns with history</div>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="text-2xl font-bold text-white font-mono">
|
||||
{reliability.reduce((s: number, r: any) => s + r.trade_count, 0)}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Trades matures analysés</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Mature trades analyzed</div>
|
||||
</div>
|
||||
{topPatterns[0] && (
|
||||
<div className="card border-emerald-700/30">
|
||||
<div className="text-xs text-slate-500 mb-1 flex items-center gap-1">
|
||||
<TrendingUp className="w-3 h-3 text-emerald-400" /> Meilleur pattern
|
||||
<TrendingUp className="w-3 h-3 text-emerald-400" /> Best pattern
|
||||
</div>
|
||||
<div className="text-sm font-semibold text-emerald-400 truncate">{topPatterns[0].pattern_name}</div>
|
||||
<div className="text-xs font-mono text-slate-400">{topPatterns[0].win_rate_pct}% WR</div>
|
||||
@@ -195,7 +195,7 @@ export default function Analytics() {
|
||||
{bottomPatterns[0] && (
|
||||
<div className="card border-red-700/30">
|
||||
<div className="text-xs text-slate-500 mb-1 flex items-center gap-1">
|
||||
<AlertTriangle className="w-3 h-3 text-red-400" /> À éviter
|
||||
<AlertTriangle className="w-3 h-3 text-red-400" /> To avoid
|
||||
</div>
|
||||
<div className="text-sm font-semibold text-red-400 truncate">{bottomPatterns[0].pattern_name}</div>
|
||||
<div className="text-xs font-mono text-slate-400">{bottomPatterns[0].win_rate_pct}% WR</div>
|
||||
@@ -207,8 +207,8 @@ export default function Analytics() {
|
||||
{/* Reliability table */}
|
||||
<div className="card">
|
||||
<div className="text-sm font-semibold text-white mb-3 flex items-center gap-2">
|
||||
<BarChart2 className="w-4 h-4 text-blue-400" /> Fiabilité par pattern
|
||||
<span className="text-xs text-slate-500 font-normal">(trades ≥35% de l'horizon uniquement)</span>
|
||||
<BarChart2 className="w-4 h-4 text-blue-400" /> Reliability by pattern
|
||||
<span className="text-xs text-slate-500 font-normal">(trades ≥35% of horizon only)</span>
|
||||
</div>
|
||||
{loadingR ? (
|
||||
<div className="space-y-2">{[1,2,3].map(i => <div key={i} className="h-8 bg-dark-700 animate-pulse rounded" />)}</div>
|
||||
@@ -221,17 +221,17 @@ export default function Analytics() {
|
||||
<div className="card">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="text-sm font-semibold text-white flex items-center gap-2">
|
||||
<Target className="w-4 h-4 text-blue-400" /> Calibration probabiliste
|
||||
<Target className="w-4 h-4 text-blue-400" /> Probabilistic calibration
|
||||
</div>
|
||||
<select
|
||||
value={calDays}
|
||||
onChange={e => setCalDays(Number(e.target.value))}
|
||||
className="bg-dark-700 border border-slate-700 rounded px-2 py-1 text-xs text-slate-300"
|
||||
>
|
||||
<option value={90}>90 jours</option>
|
||||
<option value={180}>180 jours</option>
|
||||
<option value={365}>1 an</option>
|
||||
<option value={730}>2 ans</option>
|
||||
<option value={90}>90 days</option>
|
||||
<option value={180}>180 days</option>
|
||||
<option value={365}>1 year</option>
|
||||
<option value={730}>2 years</option>
|
||||
</select>
|
||||
</div>
|
||||
{loadingC ? (
|
||||
|
||||
@@ -11,8 +11,8 @@ function BayesianTable({ data }: { data: any[] }) {
|
||||
return (
|
||||
<div className="text-center py-10 text-slate-500">
|
||||
<Brain className="w-8 h-8 mx-auto mb-2 opacity-20" />
|
||||
<div>Pas encore de données bayésiennes</div>
|
||||
<div className="text-xs mt-1">Les posteriors se calculent après des trades matures (≥35% de l'horizon)</div>
|
||||
<div>No Bayesian data yet</div>
|
||||
<div className="text-xs mt-1">Posteriors are computed after mature trades (≥35% of the horizon)</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -29,11 +29,11 @@ function BayesianTable({ data }: { data: any[] }) {
|
||||
<tr className="text-slate-500 border-b border-slate-700/30">
|
||||
<th className="text-left py-2 pr-3 font-medium">Pattern</th>
|
||||
<th className="text-right py-2 px-2 font-medium">Prior GPT</th>
|
||||
<th className="text-right py-2 px-2 font-medium">WR Bayésien</th>
|
||||
<th className="text-right py-2 px-2 font-medium">IC 95%</th>
|
||||
<th className="text-right py-2 px-2 font-medium">Trades matures</th>
|
||||
<th className="text-right py-2 px-2 font-medium">Dérive</th>
|
||||
<th className="text-center py-2 px-2 font-medium">Confiance</th>
|
||||
<th className="text-right py-2 px-2 font-medium">Bayesian WR</th>
|
||||
<th className="text-right py-2 px-2 font-medium">CI 95%</th>
|
||||
<th className="text-right py-2 px-2 font-medium">Mature trades</th>
|
||||
<th className="text-right py-2 px-2 font-medium">Drift</th>
|
||||
<th className="text-center py-2 px-2 font-medium">Confidence</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800/50">
|
||||
@@ -78,7 +78,7 @@ function BayesianTable({ data }: { data: any[] }) {
|
||||
)}
|
||||
{withoutData.length > 0 && (
|
||||
<div className="text-xs text-slate-600 mt-1">
|
||||
{withoutData.length} pattern(s) sans trades matures — prior GPT uniquement
|
||||
{withoutData.length} pattern(s) without mature trades — GPT prior only
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -92,8 +92,8 @@ function TransitionMatrix({ data }: { data: any }) {
|
||||
return (
|
||||
<div className="text-center py-8 text-slate-500">
|
||||
<GitBranch className="w-8 h-8 mx-auto mb-2 opacity-20" />
|
||||
<div>Pas encore de transitions détectées</div>
|
||||
<div className="text-xs mt-1">Les clusters se remplissent au fil des cycles</div>
|
||||
<div>No transitions detected yet</div>
|
||||
<div className="text-xs mt-1">Clusters fill up as cycles run</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -104,13 +104,13 @@ function TransitionMatrix({ data }: { data: any }) {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<div className="text-xs text-slate-500 mb-2">
|
||||
Probabilité de transition d'un régime à l'autre (lignes = source, colonnes = destination)
|
||||
· {data.n_transitions} transitions totales
|
||||
Transition probability from one regime to another (rows = source, columns = destination)
|
||||
· {data.n_transitions} total transitions
|
||||
</div>
|
||||
<table className="text-xs border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-slate-500 p-2 font-normal text-right pr-3">De ↓ / Vers →</th>
|
||||
<th className="text-slate-500 p-2 font-normal text-right pr-3">From ↓ / To →</th>
|
||||
{ids.map(dst => (
|
||||
<th key={dst} className="p-2 font-medium text-slate-300 text-center min-w-[90px]">
|
||||
<div className="text-[10px] text-slate-500">C{dst}</div>
|
||||
@@ -145,7 +145,7 @@ function TransitionMatrix({ data }: { data: any }) {
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="text-[10px] text-slate-600 mt-2">
|
||||
Diagonal = reste dans le même cluster · Vert = transition dominante · Bleu = auto-transition
|
||||
Diagonal = stays in same cluster · Green = dominant transition · Blue = self-transition
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -165,7 +165,7 @@ function ClusterTimeline({ data }: { data: any[] }) {
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-6 text-slate-500 text-xs">
|
||||
Aucun cluster détecté encore — lancez un cycle pour démarrer
|
||||
No cluster detected yet — run a cycle to get started
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -187,7 +187,7 @@ function ClusterTimeline({ data }: { data: any[] }) {
|
||||
{recent.map((r: any, i: number) => (
|
||||
<div
|
||||
key={i}
|
||||
title={`${r.timestamp?.slice(0, 16)} · ${r.cluster_label} · ${r.dominant_regime}${r.anomaly_flag ? ' ⚠️ anomalie' : ''}`}
|
||||
title={`${r.timestamp?.slice(0, 16)} · ${r.cluster_label} · ${r.dominant_regime}${r.anomaly_flag ? ' ⚠️ anomaly' : ''}`}
|
||||
className={clsx(
|
||||
'w-5 h-5 rounded cursor-default',
|
||||
CLUSTER_COLORS[r.cluster_id] ?? 'bg-slate-600',
|
||||
@@ -197,20 +197,20 @@ function ClusterTimeline({ data }: { data: any[] }) {
|
||||
))}
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-600">
|
||||
Chaque carré = 1 snapshot · Blanc cerclé = anomalie détectée · {data.length} entrées au total
|
||||
Each square = 1 snapshot · White ring = anomaly detected · {data.length} total entries
|
||||
</div>
|
||||
|
||||
{/* Latest cluster detail */}
|
||||
{data[0] && (
|
||||
<div className="mt-3 p-3 rounded bg-dark-700/60 border border-slate-700/30 text-xs">
|
||||
<div className="text-slate-400 mb-1">Cluster actuel</div>
|
||||
<div className="text-slate-400 mb-1">Current cluster</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={clsx('w-3 h-3 rounded-full', CLUSTER_COLORS[data[0].cluster_id])} />
|
||||
<span className="text-white font-semibold">{data[0].cluster_label}</span>
|
||||
<span className="text-slate-500">({data[0].dominant_regime})</span>
|
||||
{data[0].anomaly_flag ? (
|
||||
<span className="flex items-center gap-1 text-amber-400 font-bold">
|
||||
<AlertTriangle className="w-3 h-3" /> Anomalie
|
||||
<AlertTriangle className="w-3 h-3" /> Anomaly
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -228,21 +228,21 @@ function EmbeddingsSummary({ data }: { data: any[] }) {
|
||||
return (
|
||||
<div className="text-center py-6 text-slate-500 text-xs">
|
||||
<Cpu className="w-6 h-6 mx-auto mb-1 opacity-20" />
|
||||
Aucun embedding disponible — ils se génèrent automatiquement lors du filtrage des patterns
|
||||
No embeddings available — they are generated automatically during pattern filtering
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs text-slate-500">{data.length} patterns vectorisés</div>
|
||||
<div className="text-xs text-slate-500">{data.length} vectorized patterns</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-slate-500 border-b border-slate-700/30">
|
||||
<th className="text-left py-1.5 pr-3 font-medium">Pattern</th>
|
||||
<th className="text-left py-1.5 px-2 font-medium">Classe</th>
|
||||
<th className="text-right py-1.5 px-2 font-medium">Modèle</th>
|
||||
<th className="text-right py-1.5 px-2 font-medium">Mis à jour</th>
|
||||
<th className="text-left py-1.5 px-2 font-medium">Class</th>
|
||||
<th className="text-right py-1.5 px-2 font-medium">Model</th>
|
||||
<th className="text-right py-1.5 px-2 font-medium">Updated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800/30">
|
||||
@@ -301,10 +301,10 @@ export default function AnalyticsAdvanced() {
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-white flex items-center gap-2">
|
||||
<Brain className="w-5 h-5 text-purple-400" /> Analytics Avancées — Phase 4
|
||||
<Brain className="w-5 h-5 text-purple-400" /> Advanced Analytics — Phase 4
|
||||
</h1>
|
||||
<p className="text-xs text-slate-500 mt-0.5">
|
||||
Bayesian updating · Clustering de régimes · Embeddings sémantiques
|
||||
Bayesian updating · Regime clustering · Semantic embeddings
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
@@ -322,7 +322,7 @@ export default function AnalyticsAdvanced() {
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-blue-600/20 hover:bg-blue-600/30 border border-blue-500/30 text-blue-300 rounded transition-colors disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={clsx('w-3 h-3', regimeDetect.isPending && 'animate-spin')} />
|
||||
Détecter régime
|
||||
Detect regime
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -333,25 +333,25 @@ export default function AnalyticsAdvanced() {
|
||||
<div className="text-2xl font-bold text-purple-400 font-mono">
|
||||
{patternsWithData.length}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Patterns bayésiens actifs</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Active Bayesian patterns</div>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="text-2xl font-bold text-white font-mono">
|
||||
{avgBayesWR !== null ? `${avgBayesWR}%` : '—'}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Win rate bayésien moyen</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Average Bayesian win rate</div>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className={clsx('text-2xl font-bold font-mono', latestCluster ? CLUSTER_COLORS[latestCluster.cluster_id]?.replace('bg-', 'text-') ?? 'text-slate-400' : 'text-slate-500')}>
|
||||
{latestCluster ? `C${latestCluster.cluster_id}` : '—'}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-1">
|
||||
{latestCluster?.cluster_label ?? 'Aucun cluster'}
|
||||
{latestCluster?.cluster_label ?? 'No cluster'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="text-2xl font-bold text-blue-400 font-mono">{embeddings.length}</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Patterns vectorisés</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Vectorized patterns</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -360,8 +360,8 @@ export default function AnalyticsAdvanced() {
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="text-sm font-semibold text-white flex items-center gap-2">
|
||||
<Brain className="w-4 h-4 text-purple-400" />
|
||||
Posteriors bayésiens par pattern
|
||||
<span className="text-xs text-slate-500 font-normal">Beta(α,β) · IC 95%</span>
|
||||
Bayesian posteriors by pattern
|
||||
<span className="text-xs text-slate-500 font-normal">Beta(α,β) · CI 95%</span>
|
||||
</div>
|
||||
{bayesUpdate.data && (
|
||||
<span className="text-xs text-emerald-400">{bayesUpdate.data.message}</span>
|
||||
@@ -374,9 +374,9 @@ export default function AnalyticsAdvanced() {
|
||||
)}
|
||||
|
||||
<div className="mt-3 p-3 bg-dark-700/40 rounded text-xs text-slate-500 border border-slate-700/20">
|
||||
<strong className="text-slate-400">Lecture :</strong> Prior GPT = probabilité annoncée par GPT-4o ·
|
||||
WR Bayésien = win rate observé avec lissage Laplace (β a priori faible) ·
|
||||
Dérive = écart posterior − prior · IC 95% basé sur la distribution Beta
|
||||
<strong className="text-slate-400">Reading:</strong> GPT Prior = probability stated by GPT-4o ·
|
||||
Bayesian WR = observed win rate with Laplace smoothing (weak β prior) ·
|
||||
Drift = posterior − prior gap · CI 95% based on Beta distribution
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -387,16 +387,16 @@ export default function AnalyticsAdvanced() {
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="text-sm font-semibold text-white flex items-center gap-2">
|
||||
<Layers className="w-4 h-4 text-blue-400" />
|
||||
Historique des clusters
|
||||
Cluster history
|
||||
</div>
|
||||
<select
|
||||
value={regimeDays}
|
||||
onChange={e => setRegimeDays(Number(e.target.value))}
|
||||
className="bg-dark-700 border border-slate-700 rounded px-2 py-1 text-xs text-slate-300"
|
||||
>
|
||||
<option value={30}>30 jours</option>
|
||||
<option value={90}>90 jours</option>
|
||||
<option value={180}>180 jours</option>
|
||||
<option value={30}>30 days</option>
|
||||
<option value={90}>90 days</option>
|
||||
<option value={180}>180 days</option>
|
||||
</select>
|
||||
</div>
|
||||
{loadingClusters ? (
|
||||
@@ -410,8 +410,8 @@ export default function AnalyticsAdvanced() {
|
||||
<div className="card">
|
||||
<div className="text-sm font-semibold text-white flex items-center gap-2 mb-3">
|
||||
<GitBranch className="w-4 h-4 text-blue-400" />
|
||||
Matrice de transition
|
||||
<span className="text-xs text-slate-500 font-normal">P(régime j | régime i)</span>
|
||||
Transition matrix
|
||||
<span className="text-xs text-slate-500 font-normal">P(regime j | regime i)</span>
|
||||
</div>
|
||||
{loadingTrans ? (
|
||||
<div className="h-32 bg-dark-700 animate-pulse rounded" />
|
||||
@@ -425,9 +425,9 @@ export default function AnalyticsAdvanced() {
|
||||
<div className="card">
|
||||
<div className="text-sm font-semibold text-white flex items-center gap-2 mb-3">
|
||||
<Cpu className="w-4 h-4 text-emerald-400" />
|
||||
Embeddings sémantiques
|
||||
Semantic embeddings
|
||||
<span className="text-xs text-slate-500 font-normal">
|
||||
text-embedding-3-small · Similarité cosinus remplace Jaccard
|
||||
text-embedding-3-small · Cosine similarity replaces Jaccard
|
||||
</span>
|
||||
</div>
|
||||
{loadingEmb ? (
|
||||
@@ -436,9 +436,9 @@ export default function AnalyticsAdvanced() {
|
||||
<EmbeddingsSummary data={embeddings} />
|
||||
)}
|
||||
<div className="mt-3 p-3 bg-dark-700/40 rounded text-xs text-slate-500 border border-slate-700/20">
|
||||
<strong className="text-slate-400">Comment ça marche :</strong> Chaque nouveau pattern suggéré est vectorisé et comparé
|
||||
aux patterns existants via similarité cosinus (seuil : 0.75). Si la similarité est > 0.75,
|
||||
le pattern est rejeté comme doublon sémantique — plus précis que Jaccard qui ne voit que les mots communs.
|
||||
<strong className="text-slate-400">How it works:</strong> Each newly suggested pattern is vectorized and compared
|
||||
to existing patterns via cosine similarity (threshold: 0.75). If similarity is > 0.75,
|
||||
the pattern is rejected as a semantic duplicate — more precise than Jaccard which only sees common words.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -63,7 +63,7 @@ export default function Backtest() {
|
||||
<History className="w-5 h-5 text-blue-400" /> Backtest & Simulation
|
||||
</h1>
|
||||
<p className="text-xs text-slate-500 mt-0.5">
|
||||
Testez vos stratégies options sur des données historiques réelles
|
||||
Test your options strategies on real historical data
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -74,7 +74,7 @@ export default function Backtest() {
|
||||
<div className="section-title">Configuration</div>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Sous-jacent</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Underlying</label>
|
||||
<div className="flex flex-wrap gap-1 mb-1">
|
||||
{SYMBOLS.slice(0, 7).map(s => (
|
||||
<button
|
||||
@@ -98,7 +98,7 @@ export default function Backtest() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Stratégie</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Strategy</label>
|
||||
{STRATEGIES.map(s => (
|
||||
<button
|
||||
key={s.key}
|
||||
@@ -114,7 +114,7 @@ export default function Backtest() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Période</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Period</label>
|
||||
<input
|
||||
type="date"
|
||||
value={form.start_date}
|
||||
@@ -142,7 +142,7 @@ export default function Backtest() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Expiration: {form.expiry_days}j</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Expiration: {form.expiry_days}d</label>
|
||||
<div className="flex gap-1 mb-1">
|
||||
{[30, 60, 90, 180].map(d => (
|
||||
<button
|
||||
@@ -153,14 +153,14 @@ export default function Backtest() {
|
||||
'border-slate-700 text-slate-500': form.expiry_days !== d,
|
||||
})}
|
||||
>
|
||||
{d}j
|
||||
{d}d
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Capital initial (€)</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Initial capital (€)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={form.capital}
|
||||
@@ -175,7 +175,7 @@ export default function Backtest() {
|
||||
className="w-full bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white rounded py-2 text-sm font-semibold flex items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
<Play className="w-4 h-4" />
|
||||
{isPending ? 'Calcul...' : 'Lancer le backtest'}
|
||||
{isPending ? 'Computing...' : 'Run backtest'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -196,12 +196,12 @@ export default function Backtest() {
|
||||
{/* KPIs */}
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<StatCard
|
||||
label="Retour total"
|
||||
label="Total return"
|
||||
value={`${typed.total_return_pct >= 0 ? '+' : ''}${typed.total_return_pct.toFixed(2)}%`}
|
||||
positive={typed.total_return_pct >= 0}
|
||||
/>
|
||||
<StatCard
|
||||
label="Taux de succès"
|
||||
label="Win rate"
|
||||
value={`${typed.win_rate.toFixed(1)}%`}
|
||||
sub={`${typed.wins}W / ${typed.losses}L`}
|
||||
positive={typed.win_rate >= 50}
|
||||
@@ -221,9 +221,9 @@ export default function Backtest() {
|
||||
|
||||
{/* Equity curve */}
|
||||
<div className="card">
|
||||
<div className="section-title">Courbe d'équité — {form.symbol} {STRATEGIES.find(s => s.key === form.strategy)?.label}</div>
|
||||
<div className="section-title">Equity curve — {form.symbol} {STRATEGIES.find(s => s.key === form.strategy)?.label}</div>
|
||||
<div className="text-xs text-slate-500 mb-3">
|
||||
Capital final: <span className="text-white font-bold">{typed.final_capital.toFixed(2)}€</span>
|
||||
Final capital: <span className="text-white font-bold">{typed.final_capital.toFixed(2)}€</span>
|
||||
{' '}(initial: {form.capital}€ · P&L: {typed.total_pnl >= 0 ? '+' : ''}{typed.total_pnl.toFixed(2)}€)
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
@@ -254,17 +254,17 @@ export default function Backtest() {
|
||||
|
||||
{/* Last trades */}
|
||||
<div className="card">
|
||||
<div className="section-title">Derniers trades exécutés</div>
|
||||
<div className="section-title">Last executed trades</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-slate-600 border-b border-slate-700/40">
|
||||
<th className="text-left pb-2">Entrée</th>
|
||||
<th className="text-left pb-2">Sortie</th>
|
||||
<th className="text-right pb-2">Prix entrée</th>
|
||||
<th className="text-left pb-2">Entry</th>
|
||||
<th className="text-left pb-2">Exit</th>
|
||||
<th className="text-right pb-2">Entry price</th>
|
||||
<th className="text-right pb-2">Strike</th>
|
||||
<th className="text-right pb-2">Prime</th>
|
||||
<th className="text-right pb-2">Prix sortie</th>
|
||||
<th className="text-right pb-2">Premium</th>
|
||||
<th className="text-right pb-2">Exit price</th>
|
||||
<th className="text-right pb-2">P&L</th>
|
||||
<th className="text-right pb-2">Capital</th>
|
||||
</tr>
|
||||
@@ -298,8 +298,8 @@ export default function Backtest() {
|
||||
<div className="card h-80 flex items-center justify-center text-slate-600">
|
||||
<div className="text-center">
|
||||
<History className="w-10 h-10 mx-auto mb-3 opacity-20" />
|
||||
<div className="text-sm">Configurer et lancer un backtest</div>
|
||||
<div className="text-xs mt-1">Données yfinance — historique complet disponible</div>
|
||||
<div className="text-sm">Configure and run a backtest</div>
|
||||
<div className="text-xs mt-1">yfinance data — full history available</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,6 @@ import clsx from 'clsx'
|
||||
import { Calendar, Clock, Globe, AlertTriangle } from 'lucide-react'
|
||||
import type { EconomicEvent, AssetClass } from '../types'
|
||||
import { format, parseISO, isAfter, isBefore, addDays } from 'date-fns'
|
||||
import { fr } from 'date-fns/locale'
|
||||
|
||||
const ASSET_EMOJIS: Record<string, string> = {
|
||||
energy: '⛽', metals: '🥇', agriculture: '🌾', equities: '📈',
|
||||
@@ -16,9 +15,9 @@ const COUNTRY_FLAGS: Record<string, string> = {
|
||||
}
|
||||
|
||||
const IMPORTANCE_CONFIG: Record<string, { color: string; label: string; dots: number }> = {
|
||||
high: { color: 'text-red-400 border-red-700/40', label: 'Majeur', dots: 3 },
|
||||
medium: { color: 'text-yellow-400 border-yellow-700/40', label: 'Modéré', dots: 2 },
|
||||
low: { color: 'text-slate-400 border-slate-700/40', label: 'Mineur', dots: 1 },
|
||||
high: { color: 'text-red-400 border-red-700/40', label: 'Major', dots: 3 },
|
||||
medium: { color: 'text-yellow-400 border-yellow-700/40', label: 'Moderate', dots: 2 },
|
||||
low: { color: 'text-slate-400 border-slate-700/40', label: 'Minor', dots: 1 },
|
||||
}
|
||||
|
||||
function EventCard({ ev }: { ev: EconomicEvent }) {
|
||||
@@ -42,19 +41,19 @@ function EventCard({ ev }: { ev: EconomicEvent }) {
|
||||
<span className={clsx('text-xs font-bold uppercase tracking-wider', cfg.color.split(' ')[0])}>
|
||||
{'●'.repeat(cfg.dots)}
|
||||
</span>
|
||||
{isToday && <span className="badge badge-yellow text-xs">AUJOURD'HUI</span>}
|
||||
{isSoon && !isToday && <span className="badge badge-blue text-xs">BIENTÔT</span>}
|
||||
{isToday && <span className="badge badge-yellow text-xs">TODAY</span>}
|
||||
{isSoon && !isToday && <span className="badge badge-blue text-xs">SOON</span>}
|
||||
</div>
|
||||
<div className="text-sm text-white font-semibold">{ev.title}</div>
|
||||
<div className="text-xs text-slate-500 mt-0.5">
|
||||
{format(evDate, "EEEE d MMM yyyy", { locale: fr })} · {ev.country}
|
||||
{format(evDate, "EEEE, MMM d yyyy")} · {ev.country}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right shrink-0">
|
||||
<div className={clsx('text-xs font-semibold', cfg.color.split(' ')[0])}>{cfg.label}</div>
|
||||
{ev.previous && <div className="text-xs text-slate-600 mt-0.5">Préc: {ev.previous}</div>}
|
||||
{ev.forecast && <div className="text-xs text-slate-500">Prév: {ev.forecast}</div>}
|
||||
{ev.actual && <div className="text-xs text-emerald-400 font-bold">Réel: {ev.actual}</div>}
|
||||
{ev.previous && <div className="text-xs text-slate-600 mt-0.5">Prev: {ev.previous}</div>}
|
||||
{ev.forecast && <div className="text-xs text-slate-500">Fcst: {ev.forecast}</div>}
|
||||
{ev.actual && <div className="text-xs text-emerald-400 font-bold">Actual: {ev.actual}</div>}
|
||||
</div>
|
||||
</div>
|
||||
{ev.asset_impact && ev.asset_impact.length > 0 && (
|
||||
@@ -84,25 +83,25 @@ export default function CalendarPage() {
|
||||
<div className="p-6 space-y-5">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-white flex items-center gap-2">
|
||||
<Calendar className="w-5 h-5 text-blue-400" /> Calendrier Économique & Géopolitique
|
||||
<Calendar className="w-5 h-5 text-blue-400" /> Economic & Geopolitical Calendar
|
||||
</h1>
|
||||
<p className="text-xs text-slate-500 mt-0.5">
|
||||
Événements macro, catalyseurs géopolitiques, dates clés
|
||||
Macro events, geopolitical catalysts, key dates
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="flex items-center gap-4 text-xs text-slate-500">
|
||||
<div className="flex items-center gap-1"><span className="text-red-400">●●●</span> Majeur (forte volatilité attendue)</div>
|
||||
<div className="flex items-center gap-1"><span className="text-yellow-400">●●</span> Modéré</div>
|
||||
<div className="flex items-center gap-1"><span className="text-slate-400">●</span> Mineur</div>
|
||||
<div className="flex items-center gap-1"><span className="text-red-400">●●●</span> Major (high volatility expected)</div>
|
||||
<div className="flex items-center gap-1"><span className="text-yellow-400">●●</span> Moderate</div>
|
||||
<div className="flex items-center gap-1"><span className="text-slate-400">●</span> Minor</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-5">
|
||||
{/* Economic calendar */}
|
||||
<div className="col-span-2 space-y-3">
|
||||
<div className="section-title flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" /> Événements à venir ({upcoming.length})
|
||||
<Clock className="w-3 h-3" /> Upcoming events ({upcoming.length})
|
||||
</div>
|
||||
{isLoading ? (
|
||||
[1,2,3].map(i => <div key={i} className="card animate-pulse h-20 bg-dark-700"></div>)
|
||||
@@ -110,14 +109,14 @@ export default function CalendarPage() {
|
||||
upcoming.map((ev, i) => <EventCard key={i} ev={ev} />)
|
||||
) : (
|
||||
<div className="card text-center py-8 text-slate-500 text-sm">
|
||||
Démarrer le backend pour charger le calendrier
|
||||
Start the backend to load the calendar
|
||||
</div>
|
||||
)}
|
||||
|
||||
{past.length > 0 && (
|
||||
<>
|
||||
<div className="section-title mt-6 flex items-center gap-1 opacity-60">
|
||||
Événements passés ({past.length})
|
||||
Past events ({past.length})
|
||||
</div>
|
||||
{past.map((ev, i) => <EventCard key={i} ev={ev} />)}
|
||||
</>
|
||||
@@ -128,7 +127,7 @@ export default function CalendarPage() {
|
||||
<div className="col-span-1 space-y-4">
|
||||
<div className="card">
|
||||
<div className="section-title flex items-center gap-1">
|
||||
<AlertTriangle className="w-3 h-3 text-orange-400" /> Alertes géopolitiques
|
||||
<AlertTriangle className="w-3 h-3 text-orange-400" /> Geopolitical alerts
|
||||
</div>
|
||||
{highImpactNews.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
@@ -145,20 +144,20 @@ export default function CalendarPage() {
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs text-slate-600 text-center py-4">Charger les actualités géopolitiques</div>
|
||||
<div className="text-xs text-slate-600 text-center py-4">Load geopolitical news</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Trade opportunity windows */}
|
||||
<div className="card">
|
||||
<div className="section-title">Fenêtres d'opportunité</div>
|
||||
<div className="section-title">Opportunity windows</div>
|
||||
<div className="space-y-2 text-xs">
|
||||
{[
|
||||
{ window: 'Pré-FOMC (-3j)', strategy: 'Straddle sur SPY', rationale: 'IV monte avant décision' },
|
||||
{ window: 'Pré-NFP (-2j)', strategy: 'Straddle sur indices', rationale: 'Directional uncertainty' },
|
||||
{ window: 'Post-OPEC', strategy: 'Bull Call Spread USO', rationale: 'Cut → oil spike probable' },
|
||||
{ window: 'Pré-USDA Crop', strategy: 'Long Call WEAT', rationale: 'Supply news catalyst' },
|
||||
{ window: 'Élections US approche', strategy: 'Long VIX Call', rationale: 'Vol expansion garantie' },
|
||||
{ window: 'Pre-FOMC (-3d)', strategy: 'Straddle on SPY', rationale: 'IV rises ahead of decision' },
|
||||
{ window: 'Pre-NFP (-2d)', strategy: 'Straddle on indices', rationale: 'Directional uncertainty' },
|
||||
{ window: 'Post-OPEC', strategy: 'Bull Call Spread USO', rationale: 'Cut → oil spike likely' },
|
||||
{ window: 'Pre-USDA Crop', strategy: 'Long Call WEAT', rationale: 'Supply news catalyst' },
|
||||
{ window: 'US Election approaching', strategy: 'Long VIX Call', rationale: 'Vol expansion guaranteed' },
|
||||
].map((op, i) => (
|
||||
<div key={i} className="card-sm">
|
||||
<div className="font-semibold text-blue-400">{op.window}</div>
|
||||
@@ -172,16 +171,16 @@ export default function CalendarPage() {
|
||||
{/* Geo-event impact guide */}
|
||||
<div className="card">
|
||||
<div className="section-title flex items-center gap-1">
|
||||
<Globe className="w-3 h-3" /> Guide d'impact
|
||||
<Globe className="w-3 h-3" /> Impact guide
|
||||
</div>
|
||||
<div className="space-y-1.5 text-xs">
|
||||
{[
|
||||
{ event: 'Conflit Moyen-Orient', impact: 'Oil +10-20%', cls: 'energy' },
|
||||
{ event: 'Sanctions Russie', impact: 'Gaz +15-40%', cls: 'energy' },
|
||||
{ event: 'Tarifs US-Chine', impact: 'Soja -8%', cls: 'agriculture' },
|
||||
{ event: 'Crise sanitaire', impact: 'Or +7-12%', cls: 'metals' },
|
||||
{ event: 'Hausses Fed', impact: 'USD +2-4%', cls: 'forex' },
|
||||
{ event: 'Guerre Ukraine', impact: 'Blé +15-50%', cls: 'agriculture' },
|
||||
{ event: 'Middle East conflict', impact: 'Oil +10-20%', cls: 'energy' },
|
||||
{ event: 'Russia sanctions', impact: 'Gas +15-40%', cls: 'energy' },
|
||||
{ event: 'US-China tariffs', impact: 'Soy -8%', cls: 'agriculture' },
|
||||
{ event: 'Health crisis', impact: 'Gold +7-12%', cls: 'metals' },
|
||||
{ event: 'Fed hikes', impact: 'USD +2-4%', cls: 'forex' },
|
||||
{ event: 'Ukraine war', impact: 'Wheat +15-50%', cls: 'agriculture' },
|
||||
].map((g, i) => (
|
||||
<div key={i} className="flex justify-between items-center py-1 border-b border-slate-700/20 last:border-0">
|
||||
<span className="text-slate-400">{g.event}</span>
|
||||
|
||||
@@ -7,39 +7,39 @@ import clsx from 'clsx'
|
||||
const API = ''
|
||||
|
||||
const SOURCE_CATEGORIES = {
|
||||
'Flux RSS actifs': ['reuters_world', 'reuters_business', 'reuters_energy', 'ap_top', 'aljazeera', 'ft', 'bloomberg'],
|
||||
'Données économiques': ['newsapi', 'gdelt', 'eia', 'fred', 'usda'],
|
||||
'Santé & Catastrophes': ['who', 'emdat'],
|
||||
'Réseaux sociaux': ['twitter_trump'],
|
||||
'Active RSS Feeds': ['reuters_world', 'reuters_business', 'reuters_energy', 'ap_top', 'aljazeera', 'ft', 'bloomberg'],
|
||||
'Economic Data': ['newsapi', 'gdelt', 'eia', 'fred', 'usda'],
|
||||
'Health & Disasters': ['who', 'emdat'],
|
||||
'Social Media': ['twitter_trump'],
|
||||
}
|
||||
|
||||
const SOURCE_DOCS: Record<string, { description: string; link?: string; cost: string }> = {
|
||||
reuters_world: { description: 'Actualités mondiales Reuters', cost: 'Gratuit' },
|
||||
reuters_business: { description: 'Business et marchés Reuters', cost: 'Gratuit' },
|
||||
reuters_energy: { description: 'Énergie et commodités Reuters', cost: 'Gratuit' },
|
||||
ap_top: { description: 'Associated Press — Top Stories', cost: 'Gratuit' },
|
||||
aljazeera: { description: 'Al Jazeera — couverture Moyen-Orient/Monde', cost: 'Gratuit' },
|
||||
ft: { description: 'Financial Times — finance internationale', cost: 'Gratuit' },
|
||||
bloomberg: { description: 'Bloomberg Markets RSS', cost: 'Gratuit' },
|
||||
newsapi: { description: '100 req/jour gratuit — actualités mondiales multi-sources', link: 'https://newsapi.org', cost: '100 req/j gratuit' },
|
||||
gdelt: { description: 'Base de données géopolitique mondiale — 300K events/jour', link: 'https://gdeltproject.org', cost: 'Gratuit total' },
|
||||
eia: { description: 'US Energy Information Administration — données pétrole/gaz hebdo', link: 'https://www.eia.gov/opendata', cost: 'Gratuit avec clé' },
|
||||
fred: { description: 'Federal Reserve St. Louis — macro US (PIB, inflation, emploi)', link: 'https://fred.stlouisfed.org/docs/api/fred/', cost: 'Gratuit avec clé' },
|
||||
usda: { description: 'USDA — rapports agricoles officiels US', cost: 'Gratuit' },
|
||||
who: { description: 'OMS — alertes sanitaires mondiales RSS', cost: 'Gratuit' },
|
||||
emdat: { description: 'EM-DAT — base de données catastrophes naturelles', link: 'https://www.emdat.be', cost: 'Inscription gratuite' },
|
||||
twitter_trump: { description: 'Flux X/Twitter Trump — discours, annonces tarifs', cost: 'API payante ($100/mois+)' },
|
||||
reuters_world: { description: 'Reuters World News', cost: 'Free' },
|
||||
reuters_business: { description: 'Reuters Business & Markets', cost: 'Free' },
|
||||
reuters_energy: { description: 'Reuters Energy & Commodities', cost: 'Free' },
|
||||
ap_top: { description: 'Associated Press — Top Stories', cost: 'Free' },
|
||||
aljazeera: { description: 'Al Jazeera — Middle East/World coverage', cost: 'Free' },
|
||||
ft: { description: 'Financial Times — international finance', cost: 'Free' },
|
||||
bloomberg: { description: 'Bloomberg Markets RSS', cost: 'Free' },
|
||||
newsapi: { description: '100 req/day free — multi-source world news', link: 'https://newsapi.org', cost: '100 req/d free' },
|
||||
gdelt: { description: 'Global geopolitical database — 300K events/day', link: 'https://gdeltproject.org', cost: 'Totally free' },
|
||||
eia: { description: 'US Energy Information Administration — weekly oil/gas data', link: 'https://www.eia.gov/opendata', cost: 'Free with key' },
|
||||
fred: { description: 'Federal Reserve St. Louis — US macro (GDP, inflation, employment)', link: 'https://fred.stlouisfed.org/docs/api/fred/', cost: 'Free with key' },
|
||||
usda: { description: 'USDA — official US agricultural reports', cost: 'Free' },
|
||||
who: { description: 'WHO — global health alerts RSS', cost: 'Free' },
|
||||
emdat: { description: 'EM-DAT — natural disasters database', link: 'https://www.emdat.be', cost: 'Free registration' },
|
||||
twitter_trump: { description: 'X/Twitter Trump feed — speeches, tariff announcements', cost: 'Paid API ($100/mo+)' },
|
||||
}
|
||||
|
||||
// ── Risk Profiles Component ───────────────────────────────────────────────────
|
||||
|
||||
const PROFILE_COLORS = [
|
||||
{ value: '#22c55e', label: 'Vert' },
|
||||
{ value: '#3b82f6', label: 'Bleu' },
|
||||
{ value: '#22c55e', label: 'Green' },
|
||||
{ value: '#3b82f6', label: 'Blue' },
|
||||
{ value: '#f97316', label: 'Orange' },
|
||||
{ value: '#ef4444', label: 'Rouge' },
|
||||
{ value: '#8b5cf6', label: 'Violet' },
|
||||
{ value: '#eab308', label: 'Jaune' },
|
||||
{ value: '#ef4444', label: 'Red' },
|
||||
{ value: '#8b5cf6', label: 'Purple' },
|
||||
{ value: '#eab308', label: 'Yellow' },
|
||||
]
|
||||
|
||||
function NextRunCountdown({ nextRunAt }: { nextRunAt: string }) {
|
||||
@@ -64,16 +64,16 @@ function NextRunCountdown({ nextRunAt }: { nextRunAt: string }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-emerald-400/80">
|
||||
<span className="inline-block w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
|
||||
<span>Prochain cycle dans <span className="font-mono font-semibold text-emerald-300">{remaining}</span></span>
|
||||
<span className="text-slate-600">({absTime} heure locale)</span>
|
||||
<span>Next cycle in <span className="font-mono font-semibold text-emerald-300">{remaining}</span></span>
|
||||
<span className="text-slate-600">({absTime} local time)</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function evNetLabel(evNet: number): { text: string; cls: string } {
|
||||
if (evNet > 0.05) return { text: `EV nette +${(evNet * 100).toFixed(0)}%`, cls: 'text-emerald-400' }
|
||||
if (evNet >= -0.01) return { text: 'EV nette ≈ 0', cls: 'text-yellow-400' }
|
||||
return { text: `EV nette ${(evNet * 100).toFixed(0)}%`, cls: 'text-slate-600' }
|
||||
if (evNet > 0.05) return { text: `Net EV +${(evNet * 100).toFixed(0)}%`, cls: 'text-emerald-400' }
|
||||
if (evNet >= -0.01) return { text: 'Net EV ≈ 0', cls: 'text-yellow-400' }
|
||||
return { text: `Net EV ${(evNet * 100).toFixed(0)}%`, cls: 'text-slate-600' }
|
||||
}
|
||||
|
||||
function ProfileRow({
|
||||
@@ -113,7 +113,7 @@ function ProfileRow({
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className={clsx('text-sm font-semibold', enabled ? 'text-slate-200' : 'text-slate-600')}>{profile.name}</span>
|
||||
{!enabled && <span className="text-[10px] text-slate-700 italic">désactivé</span>}
|
||||
{!enabled && <span className="text-[10px] text-slate-700 italic">disabled</span>}
|
||||
<span className="text-xs text-slate-500">Score ≥ <span className="text-slate-300 font-mono">{profile.min_score}</span></span>
|
||||
<span className="text-xs text-slate-500">Gain ≥ <span className="text-slate-300 font-mono">{profile.min_gain_pct}%</span></span>
|
||||
<span className={clsx('text-[11px] font-mono', fevc)}>{fev}</span>
|
||||
@@ -138,7 +138,7 @@ function ProfileRow({
|
||||
<div className="bg-dark-700/60 rounded-lg p-3 border border-slate-600/40 space-y-3">
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="text-[10px] text-slate-500 mb-1 block">Nom du profil</label>
|
||||
<label className="text-[10px] text-slate-500 mb-1 block">Profile name</label>
|
||||
<input value={name} onChange={e => setName(e.target.value)}
|
||||
className="w-full bg-dark-800 border border-slate-700 rounded px-2 py-1 text-sm text-white" />
|
||||
</div>
|
||||
@@ -158,7 +158,7 @@ function ProfileRow({
|
||||
|
||||
{/* Live EV preview */}
|
||||
<div className="bg-dark-800 rounded px-3 py-2 text-xs flex items-center gap-4 flex-wrap">
|
||||
<span className="text-slate-500">À la frontière (score={score}, gain={gain}%)</span>
|
||||
<span className="text-slate-500">At the frontier (score={score}, gain={gain}%)</span>
|
||||
<span className={clsx('font-mono font-bold', evCls)}>{evText}</span>
|
||||
<span className="text-slate-600">Trade Score = <span className="text-slate-400">{trade_score.toFixed(1)}</span>/100</span>
|
||||
<span className="text-slate-600">
|
||||
@@ -179,14 +179,14 @@ function ProfileRow({
|
||||
<button onClick={() => setEnabled(!enabled)}
|
||||
className={clsx('text-xs px-2 py-0.5 rounded border transition-all',
|
||||
enabled ? 'border-emerald-600/40 text-emerald-400 bg-emerald-900/20' : 'border-slate-700 text-slate-600')}>
|
||||
{enabled ? '✓ Activé' : '○ Désactivé'}
|
||||
{enabled ? '✓ Enabled' : '○ Disabled'}
|
||||
</button>
|
||||
<div className="ml-auto flex gap-2">
|
||||
<button onClick={() => setEditing(false)}
|
||||
className="text-xs text-slate-500 hover:text-slate-300 px-2 py-1">Annuler</button>
|
||||
className="text-xs text-slate-500 hover:text-slate-300 px-2 py-1">Cancel</button>
|
||||
<button onClick={save}
|
||||
className="text-xs bg-blue-600 hover:bg-blue-500 text-white px-3 py-1 rounded font-semibold">
|
||||
Sauvegarder
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -227,22 +227,22 @@ function RiskProfilesCard() {
|
||||
<div className="card bg-dark-700/20 mb-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-slate-300">Profils de risque</div>
|
||||
<div className="text-sm font-semibold text-slate-300">Risk Profiles</div>
|
||||
<div className="text-[10px] text-slate-600 mt-0.5">
|
||||
Un trade est loggé dans le journal s'il passe <span className="text-slate-500">au moins un</span> profil activé
|
||||
· Formule : EV nette = (score/100 × gain/100) − (1 − score/100)
|
||||
A trade is logged in the journal if it passes <span className="text-slate-500">at least one</span> enabled profile
|
||||
· Formula: Net EV = (score/100 × gain/100) − (1 − score/100)
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={() => setShowNew(true)}
|
||||
className="flex items-center gap-1 text-xs border border-blue-500/40 text-blue-400 hover:bg-blue-900/20 px-2.5 py-1 rounded">
|
||||
<Plus className="w-3 h-3" /> Nouveau profil
|
||||
<Plus className="w-3 h-3" /> New profile
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="h-16 animate-pulse bg-dark-700 rounded" />
|
||||
) : profiles.length === 0 ? (
|
||||
<div className="text-center py-6 text-slate-600 text-sm">Aucun profil — tous les trades seront ignorés</div>
|
||||
<div className="text-center py-6 text-slate-600 text-sm">No profiles — all trades will be ignored</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{profiles.map((p: any) => (
|
||||
@@ -257,13 +257,13 @@ function RiskProfilesCard() {
|
||||
{showNew && (
|
||||
<div className="bg-dark-700/60 rounded-lg p-3 border border-blue-700/30 space-y-3 mt-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-semibold text-blue-400">Nouveau profil</span>
|
||||
<span className="text-xs font-semibold text-blue-400">New profile</span>
|
||||
<button onClick={() => setShowNew(false)} className="text-slate-600 hover:text-slate-400"><X className="w-3.5 h-3.5" /></button>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="text-[10px] text-slate-500 mb-1 block">Nom</label>
|
||||
<input value={newName} onChange={e => setNewName(e.target.value)} placeholder="ex: Ultra-risqué"
|
||||
<label className="text-[10px] text-slate-500 mb-1 block">Name</label>
|
||||
<input value={newName} onChange={e => setNewName(e.target.value)} placeholder="e.g. Ultra-aggressive"
|
||||
className="w-full bg-dark-800 border border-slate-700 rounded px-2 py-1 text-sm text-white placeholder:text-slate-700" />
|
||||
</div>
|
||||
<div>
|
||||
@@ -282,7 +282,7 @@ function RiskProfilesCard() {
|
||||
<div className="bg-dark-800 rounded px-3 py-2 text-xs flex items-center gap-4 flex-wrap">
|
||||
<span className={clsx('font-mono font-bold', evNetLabel(nEvNet).cls)}>{evNetLabel(nEvNet).text}</span>
|
||||
<span className="text-slate-600">Trade Score = <span className="text-slate-400">{nTradeScore.toFixed(1)}</span>/100</span>
|
||||
<span className="text-slate-700">Score min pour EV=0 avec gain {newGain}% : <span className="text-slate-500 font-mono">{nMinScore}</span>/100</span>
|
||||
<span className="text-slate-700">Min score for EV=0 with gain {newGain}% : <span className="text-slate-500 font-mono">{nMinScore}</span>/100</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -295,10 +295,10 @@ function RiskProfilesCard() {
|
||||
))}
|
||||
</div>
|
||||
<div className="ml-auto flex gap-2">
|
||||
<button onClick={() => setShowNew(false)} className="text-xs text-slate-500 hover:text-slate-300 px-2 py-1">Annuler</button>
|
||||
<button onClick={() => setShowNew(false)} className="text-xs text-slate-500 hover:text-slate-300 px-2 py-1">Cancel</button>
|
||||
<button onClick={saveNew} disabled={!newName.trim()}
|
||||
className="text-xs bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-3 py-1 rounded font-semibold">
|
||||
Créer
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -308,7 +308,7 @@ function RiskProfilesCard() {
|
||||
{/* Frontier visualization */}
|
||||
{profiles.length > 0 && (
|
||||
<div className="mt-3 pt-3 border-t border-slate-700/30">
|
||||
<div className="text-[10px] text-slate-600 mb-2">Frontière d'acceptation — chaque point représente le minimum requis par profil</div>
|
||||
<div className="text-[10px] text-slate-600 mb-2">Acceptance frontier — each point represents the minimum required per profile</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{profiles.filter((p: any) => p.enabled).map((p: any) => {
|
||||
const { text: ev, cls } = evNetLabel(p.ev_net_at_frontier ?? 0)
|
||||
@@ -412,15 +412,15 @@ export default function Config() {
|
||||
mutationFn: (cfg: any) => fetch(`${API}/api/var/scheduler/config`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(cfg),
|
||||
}).then(r => r.json()),
|
||||
onSuccess: () => { refetchSched(); setSavedMsg('Schedulers sauvegardés'); setTimeout(() => setSavedMsg(''), 2000) },
|
||||
onSuccess: () => { refetchSched(); setSavedMsg('Schedulers saved'); setTimeout(() => setSavedMsg(''), 2000) },
|
||||
})
|
||||
const { mutate: triggerVarNow, isPending: triggeringVar } = useMutation({
|
||||
mutationFn: () => fetch(`${API}/api/var/run-now`, { method: 'POST' }).then(r => r.json()),
|
||||
onSuccess: () => { setSavedMsg('Snapshot VaR sauvegardé'); setTimeout(() => setSavedMsg(''), 2000) },
|
||||
onSuccess: () => { setSavedMsg('VaR snapshot saved'); setTimeout(() => setSavedMsg(''), 2000) },
|
||||
})
|
||||
const { mutate: triggerPnlNow, isPending: triggeringPnl } = useMutation({
|
||||
mutationFn: () => fetch(`${API}/api/var/pnl/run-now`, { method: 'POST' }).then(r => r.json()),
|
||||
onSuccess: () => { setSavedMsg('Snapshot PnL sauvegardé'); setTimeout(() => setSavedMsg(''), 2000) },
|
||||
onSuccess: () => { setSavedMsg('PnL snapshot saved'); setTimeout(() => setSavedMsg(''), 2000) },
|
||||
})
|
||||
|
||||
// Auto-cycle local state
|
||||
@@ -486,7 +486,7 @@ export default function Config() {
|
||||
|
||||
const saveSources = () => {
|
||||
updateSources(displaySources, {
|
||||
onSuccess: () => { setSavedMsg('Sources sauvegardées'); setTimeout(() => setSavedMsg(''), 2000) }
|
||||
onSuccess: () => { setSavedMsg('Sources saved'); setTimeout(() => setSavedMsg(''), 2000) }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -498,7 +498,7 @@ export default function Config() {
|
||||
if (fredKey) keys.fred_api_key = fredKey
|
||||
updateApiKeys(keys, {
|
||||
onSuccess: () => {
|
||||
setSavedMsg('Clés API sauvegardées')
|
||||
setSavedMsg('API keys saved')
|
||||
setTimeout(() => setSavedMsg(''), 2000)
|
||||
setOpenaiKey(''); setNewsapiKey(''); setEiaKey(''); setFredKey('')
|
||||
}
|
||||
@@ -506,11 +506,11 @@ export default function Config() {
|
||||
}
|
||||
|
||||
const CONFIG_TABS = [
|
||||
{ key: 'ia', label: 'IA & Analyse', icon: Brain },
|
||||
{ key: 'ia', label: 'AI & Analysis', icon: Brain },
|
||||
{ key: 'cycle', label: 'Auto-Cycle & Logging', icon: Zap },
|
||||
{ key: 'sources', label: 'Sources', icon: Globe },
|
||||
{ key: 'options', label: 'Options — Paramètres', icon: TrendingUp },
|
||||
{ key: 'profiles', label: 'Profils de risque', icon: SlidersHorizontal },
|
||||
{ key: 'options', label: 'Options — Settings', icon: TrendingUp },
|
||||
{ key: 'profiles', label: 'Risk Profiles', icon: SlidersHorizontal },
|
||||
] as const
|
||||
|
||||
return (
|
||||
@@ -520,7 +520,7 @@ export default function Config() {
|
||||
<h1 className="text-xl font-bold text-white flex items-center gap-2">
|
||||
<Settings className="w-5 h-5 text-blue-400" /> Configuration
|
||||
</h1>
|
||||
<p className="text-xs text-slate-500 mt-0.5">Clés API, sources, paramètres IA & cycle</p>
|
||||
<p className="text-xs text-slate-500 mt-0.5">API keys, sources, AI & cycle settings</p>
|
||||
</div>
|
||||
{savedMsg && (
|
||||
<div className="flex items-center gap-2 text-emerald-400 text-sm bg-emerald-900/10 border border-emerald-700/30 rounded px-3 py-1.5">
|
||||
@@ -549,30 +549,30 @@ export default function Config() {
|
||||
<div className="space-y-4">
|
||||
{/* AI Status */}
|
||||
<div className={clsx('card', aiStatus?.enabled ? 'border-emerald-700/40' : 'border-slate-700/40')}>
|
||||
<div className="section-title flex items-center gap-1"><Key className="w-3 h-3" /> Statut IA</div>
|
||||
<div className="section-title flex items-center gap-1"><Key className="w-3 h-3" /> AI Status</div>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
{aiStatus?.enabled ? (
|
||||
<><CheckCircle className="w-5 h-5 text-emerald-400" />
|
||||
<div><div className="text-sm text-emerald-400 font-semibold">OpenAI Connecté</div>
|
||||
<div><div className="text-sm text-emerald-400 font-semibold">OpenAI Connected</div>
|
||||
<div className="text-xs text-slate-500">GPT-4o + GPT-4o-mini</div></div></>
|
||||
) : (
|
||||
<><XCircle className="w-5 h-5 text-red-400" />
|
||||
<div><div className="text-sm text-red-400 font-semibold">OpenAI Non configuré</div>
|
||||
<div className="text-xs text-slate-500">Entrer la clé ci-dessous</div></div></>
|
||||
<div><div className="text-sm text-red-400 font-semibold">OpenAI Not configured</div>
|
||||
<div className="text-xs text-slate-500">Enter key below</div></div></>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-slate-600 space-y-1">
|
||||
<div>• Analyse de discours (Trump, Powell...)</div>
|
||||
<div>• Classification IA des actualités</div>
|
||||
<div>• Évaluation de patterns</div>
|
||||
<div>• Top 10 idées contextualisées</div>
|
||||
<div>• Speech analysis (Trump, Powell...)</div>
|
||||
<div>• AI news classification</div>
|
||||
<div>• Pattern evaluation</div>
|
||||
<div>• Top 10 contextualized ideas</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* OpenAI API Key */}
|
||||
<div className="card">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="section-title mb-0 flex items-center gap-1"><Key className="w-3 h-3" /> Clé OpenAI</div>
|
||||
<div className="section-title mb-0 flex items-center gap-1"><Key className="w-3 h-3" /> OpenAI Key</div>
|
||||
<button onClick={() => setShowKeys(!showKeys)} className="text-slate-500 hover:text-slate-300">
|
||||
{showKeys ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
|
||||
</button>
|
||||
@@ -582,7 +582,7 @@ export default function Config() {
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<label className="text-xs text-slate-500">OpenAI API Key</label>
|
||||
<span className={clsx('text-xs', aiStatus?.enabled ? 'text-emerald-400' : 'text-slate-600')}>
|
||||
{aiStatus?.enabled ? '✓ Actif' : '○ Inactif'}
|
||||
{aiStatus?.enabled ? '✓ Active' : '○ Inactive'}
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
@@ -599,7 +599,7 @@ export default function Config() {
|
||||
className="w-full bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white rounded py-1.5 text-xs font-semibold flex items-center justify-center gap-1.5"
|
||||
>
|
||||
<Save className="w-3.5 h-3.5" />
|
||||
{savingKeys ? 'Sauvegarde...' : 'Sauvegarder la clé'}
|
||||
{savingKeys ? 'Saving...' : 'Save key'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -608,11 +608,11 @@ export default function Config() {
|
||||
{/* Right: Analysis params */}
|
||||
<div className="card">
|
||||
<h2 className="text-base font-bold text-white flex items-center gap-2 mb-4">
|
||||
<SlidersHorizontal className="w-4 h-4 text-blue-400" /> Paramètres d'analyse IA
|
||||
<SlidersHorizontal className="w-4 h-4 text-blue-400" /> AI Analysis Settings
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Top N résultats par défaut</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Default top N results</label>
|
||||
<div className="flex gap-1">
|
||||
{[5, 10, 15, 20].map(n => (
|
||||
<button key={n} onClick={() => setAnalysisTopN(n)}
|
||||
@@ -626,46 +626,46 @@ export default function Config() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Catégorie par défaut</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Default category</label>
|
||||
<select value={analysisCategoryDefault} onChange={e => setAnalysisCategoryDefault(e.target.value)}
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500">
|
||||
<option value="all">Toutes les catégories</option>
|
||||
<option value="energy">Énergie</option>
|
||||
<option value="metals">Métaux</option>
|
||||
<option value="all">All categories</option>
|
||||
<option value="energy">Energy</option>
|
||||
<option value="metals">Metals</option>
|
||||
<option value="agriculture">Agriculture</option>
|
||||
<option value="indices">Indices</option>
|
||||
<option value="equities">Actions</option>
|
||||
<option value="equities">Equities</option>
|
||||
<option value="forex">Forex</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<label className="text-xs text-slate-500">Template d'analyse (prompt GPT-4o)</label>
|
||||
<label className="text-xs text-slate-500">Analysis template (GPT-4o prompt)</label>
|
||||
<button onClick={() => setAnalysisTemplate('')} className="text-xs text-slate-600 hover:text-slate-400">
|
||||
Par défaut
|
||||
Default
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
value={analysisTemplate}
|
||||
onChange={e => setAnalysisTemplate(e.target.value)}
|
||||
rows={8}
|
||||
placeholder="Laisser vide pour utiliser le template par défaut..."
|
||||
placeholder="Leave empty to use default template..."
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-3 py-2 text-sm text-white font-mono focus:outline-none focus:border-blue-500 resize-y"
|
||||
/>
|
||||
<div className="text-xs text-slate-600 mt-1">
|
||||
Contexte marché (prix, IV, variation 1j) et news filtrées sont injectés automatiquement.
|
||||
Market context (prices, IV, 1d change) and filtered news are automatically injected.
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => saveAnalysis(
|
||||
{ top_n: analysisTopN, category_filter: analysisCategoryDefault, template: analysisTemplate || undefined },
|
||||
{ onSuccess: () => { setSavedMsg('Paramètres d\'analyse sauvegardés'); setTimeout(() => setSavedMsg(''), 2000) } }
|
||||
{ onSuccess: () => { setSavedMsg('Analysis settings saved'); setTimeout(() => setSavedMsg(''), 2000) } }
|
||||
)}
|
||||
disabled={savingAnalysis}
|
||||
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
|
||||
<Save className="w-4 h-4" />
|
||||
{savingAnalysis ? 'Sauvegarde...' : 'Sauvegarder'}
|
||||
{savingAnalysis ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -677,11 +677,11 @@ export default function Config() {
|
||||
{/* Score min threshold — prominent */}
|
||||
<div className="card border-blue-700/40 bg-blue-900/5">
|
||||
<h2 className="text-base font-bold text-white flex items-center gap-2 mb-1">
|
||||
<SlidersHorizontal className="w-4 h-4 text-blue-400" /> Score minimum pour le cycle
|
||||
<SlidersHorizontal className="w-4 h-4 text-blue-400" /> Minimum score for the cycle
|
||||
</h2>
|
||||
<p className="text-xs text-slate-500 mb-4">
|
||||
Pré-filtre du cycle — les patterns avec un score inférieur ne sont pas soumis à l'analyse.
|
||||
Les profils de risque déterminent ensuite quels trades sont loggés.
|
||||
Cycle pre-filter — patterns with a score below this are not submitted for analysis.
|
||||
Risk profiles then determine which trades get logged.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
@@ -694,12 +694,12 @@ export default function Config() {
|
||||
onChange={e => setMinScore(parseInt(e.target.value))}
|
||||
className="w-full accent-blue-500" />
|
||||
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
|
||||
<span>0 (tous)</span><span>80 (très sélectif)</span>
|
||||
<span>0 (all)</span><span>80 (very selective)</span>
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-2">
|
||||
{minScore === 0
|
||||
? 'Tous les patterns analysés par le cycle'
|
||||
: `Seuls les patterns avec score ≥ ${minScore} sont soumis au cycle`}
|
||||
? 'All patterns processed by the cycle'
|
||||
: `Only patterns with score ≥ ${minScore} are submitted to the cycle`}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -716,10 +716,10 @@ export default function Config() {
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-2">
|
||||
{minEv === 0
|
||||
? 'Toute EV nette acceptée'
|
||||
? 'Any net EV accepted'
|
||||
: minEv > 0
|
||||
? `EV nette ≥ +${(minEv * 100).toFixed(0)}% requise`
|
||||
: `Filtre désactivé (EV < ${(minEv * 100).toFixed(0)}%)`}
|
||||
? `Net EV ≥ +${(minEv * 100).toFixed(0)}% required`
|
||||
: `Filter disabled (EV < ${(minEv * 100).toFixed(0)}%)`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -735,38 +735,38 @@ export default function Config() {
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
{cs.running ? (
|
||||
<span className="flex items-center gap-1 text-blue-400 bg-blue-900/30 border border-blue-700/30 px-2 py-0.5 rounded animate-pulse">
|
||||
<RefreshCw className="w-3 h-3 animate-spin" /> En cours...
|
||||
<RefreshCw className="w-3 h-3 animate-spin" /> Running...
|
||||
</span>
|
||||
) : cs.scheduler_alive ? (
|
||||
<span className="text-emerald-400 bg-emerald-900/30 border border-emerald-700/30 px-2 py-0.5 rounded">
|
||||
● Scheduler actif
|
||||
● Scheduler active
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-slate-600 bg-dark-700 border border-slate-700/30 px-2 py-0.5 rounded">
|
||||
○ Scheduler inactif
|
||||
○ Scheduler inactive
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mb-4">
|
||||
Toutes les N heures : suggère de nouveaux patterns, filtre les doublons, score tout, log les prix et génère un commentaire IA.
|
||||
Every N hours: suggests new patterns, deduplicates, scores everything, logs prices, and generates an AI comment.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4 mb-4">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-2 block">Activer l'auto-cycle</label>
|
||||
<label className="text-xs text-slate-500 mb-2 block">Enable auto-cycle</label>
|
||||
<button
|
||||
onClick={() => setCycleEnabled(!cycleEnabled)}
|
||||
className={clsx('w-full py-2 rounded border text-sm font-semibold transition-all', {
|
||||
'bg-blue-600 border-blue-500 text-white': cycleEnabled,
|
||||
'bg-dark-700 border-slate-700 text-slate-400 hover:border-slate-500': !cycleEnabled,
|
||||
})}>
|
||||
{cycleEnabled ? '✓ Activé' : '○ Désactivé'}
|
||||
{cycleEnabled ? '✓ Enabled' : '○ Disabled'}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-2 block">Intervalle (heures)</label>
|
||||
<label className="text-xs text-slate-500 mb-2 block">Interval (hours)</label>
|
||||
<div className="flex gap-1">
|
||||
{[1, 3, 6, 12].map(h => (
|
||||
<button key={h} onClick={() => setCycleHours(h)}
|
||||
@@ -781,7 +781,7 @@ export default function Config() {
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-2 block">
|
||||
Similarité max ({Math.round(cycleSimilarity * 100)}%)
|
||||
Max similarity ({Math.round(cycleSimilarity * 100)}%)
|
||||
</label>
|
||||
<input type="range" min="0.1" max="0.8" step="0.05"
|
||||
value={cycleSimilarity}
|
||||
@@ -796,7 +796,7 @@ export default function Config() {
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-2 block">
|
||||
Rétention Journal ({retentionDays}j)
|
||||
Journal retention ({retentionDays}d)
|
||||
</label>
|
||||
<div className="flex gap-1">
|
||||
{[30, 60, 90, 180].map(d => (
|
||||
@@ -812,7 +812,7 @@ export default function Config() {
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-2 block">
|
||||
Seuil maturité ({maturityThreshold}%)
|
||||
Maturity threshold ({maturityThreshold}%)
|
||||
</label>
|
||||
<div className="flex gap-1">
|
||||
{[20, 30, 35, 50].map(p => (
|
||||
@@ -832,8 +832,8 @@ export default function Config() {
|
||||
<div className="border border-slate-700/50 rounded-lg p-3 mb-4 bg-dark-700/30">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<span className="text-xs font-semibold text-slate-300">Cycles weekend</span>
|
||||
<p className="text-[10px] text-slate-500 mt-0.5">Sam/Dim — Globex ouvre dim ~18h UTC. Heures en UTC.</p>
|
||||
<span className="text-xs font-semibold text-slate-300">Weekend cycles</span>
|
||||
<p className="text-[10px] text-slate-500 mt-0.5">Sat/Sun — Globex opens Sun ~18h UTC. Times in UTC.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setWeekendEnabled(!weekendEnabled)}
|
||||
@@ -841,12 +841,12 @@ export default function Config() {
|
||||
'bg-amber-600/20 border-amber-500/50 text-amber-300': weekendEnabled,
|
||||
'bg-dark-700 border-slate-700 text-slate-500 hover:border-slate-500': !weekendEnabled,
|
||||
})}>
|
||||
{weekendEnabled ? '✓ Activé' : '○ Désactivé'}
|
||||
{weekendEnabled ? '✓ Enabled' : '○ Disabled'}
|
||||
</button>
|
||||
</div>
|
||||
{weekendEnabled && (
|
||||
<div>
|
||||
<label className="text-[10px] text-slate-500 mb-1.5 block">Horaires UTC (cliquer pour activer/désactiver)</label>
|
||||
<label className="text-[10px] text-slate-500 mb-1.5 block">UTC times (click to enable/disable)</label>
|
||||
<div className="flex gap-1.5 flex-wrap">
|
||||
{['06:00', '08:00', '12:00', '18:00', '22:00', '00:00'].map(t => {
|
||||
const active = weekendTimes.includes(t)
|
||||
@@ -865,7 +865,7 @@ export default function Config() {
|
||||
})}
|
||||
</div>
|
||||
{weekendTimes.length === 0 && (
|
||||
<p className="text-[10px] text-red-400 mt-1">Sélectionner au moins un horaire</p>
|
||||
<p className="text-[10px] text-red-400 mt-1">Select at least one time</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -874,14 +874,14 @@ export default function Config() {
|
||||
{cs?.last_cycle && (
|
||||
<div className="card bg-dark-700/50 mb-2 text-xs space-y-2">
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<span className="text-slate-500">Dernier cycle :</span>
|
||||
<span className="text-slate-500">Last cycle:</span>
|
||||
<span className="text-slate-300">{cs.last_cycle.started_at?.slice(0, 16)} UTC</span>
|
||||
<span className={clsx('badge', cs.last_cycle.status === 'completed' ? 'badge-green' : 'badge-red')}>
|
||||
{cs.last_cycle.status}
|
||||
</span>
|
||||
<span className="text-slate-500">+{cs.last_cycle.patterns_added} patterns</span>
|
||||
<span className="text-slate-500">{cs.last_cycle.patterns_scored} scorés</span>
|
||||
<span className="text-slate-500">Géo: {cs.last_cycle.geo_score}</span>
|
||||
<span className="text-slate-500">{cs.last_cycle.patterns_scored} scored</span>
|
||||
<span className="text-slate-500">Geo: {cs.last_cycle.geo_score}</span>
|
||||
<span className="text-slate-500 capitalize">{cs.last_cycle.dominant_regime}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -906,19 +906,19 @@ export default function Config() {
|
||||
weekend_cycle_enabled: weekendEnabled,
|
||||
weekend_cycle_times: weekendTimes.length > 0 ? weekendTimes.join(',') : '08:00,22:00',
|
||||
},
|
||||
{ onSuccess: () => { refetchCycle(); setSavedMsg('Auto-cycle configuré'); setTimeout(() => setSavedMsg(''), 2000) } }
|
||||
{ onSuccess: () => { refetchCycle(); setSavedMsg('Auto-cycle configured'); setTimeout(() => setSavedMsg(''), 2000) } }
|
||||
)}
|
||||
disabled={savingCycle}
|
||||
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
|
||||
<Save className="w-4 h-4" />
|
||||
{savingCycle ? 'Sauvegarde...' : 'Appliquer'}
|
||||
{savingCycle ? 'Saving...' : 'Apply'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => triggerCycle(undefined, { onSuccess: () => { refetchCycle(); setSavedMsg('Cycle lancé !'); setTimeout(() => setSavedMsg(''), 3000) } })}
|
||||
onClick={() => triggerCycle(undefined, { onSuccess: () => { refetchCycle(); setSavedMsg('Cycle started!'); setTimeout(() => setSavedMsg(''), 3000) } })}
|
||||
disabled={triggeringCycle || !aiStatus?.enabled}
|
||||
className="flex items-center gap-1.5 border border-blue-500/50 text-blue-400 hover:bg-blue-900/20 disabled:opacity-40 px-4 py-2 rounded text-sm font-semibold">
|
||||
<RefreshCw className={clsx('w-4 h-4', triggeringCycle && 'animate-spin')} />
|
||||
Lancer maintenant
|
||||
Run now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -929,8 +929,8 @@ export default function Config() {
|
||||
<Gauge className="w-4 h-4 text-red-400" /> Schedulers VaR & PnL
|
||||
</h2>
|
||||
<p className="text-xs text-slate-500 mb-4">
|
||||
Calcule et sauvegarde automatiquement les snapshots VaR (Black-Scholes delta) et PnL à intervalles définis.
|
||||
Les données sont accessibles depuis la page VaR Analyse avec un historique complet.
|
||||
Automatically computes and saves VaR (Black-Scholes delta) and PnL snapshots at defined intervals.
|
||||
Data is accessible from the VaR Analysis page with full history.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-6 mb-4">
|
||||
{/* VaR scheduler */}
|
||||
@@ -939,21 +939,21 @@ export default function Config() {
|
||||
<Gauge className="w-3.5 h-3.5 text-red-400" />
|
||||
<span className="text-sm font-semibold text-slate-300">Scheduler VaR</span>
|
||||
{schedStatus?.var?.alive
|
||||
? <span className="text-[10px] bg-emerald-900/30 text-emerald-400 border border-emerald-700/30 px-1.5 py-0.5 rounded ml-auto">● Actif</span>
|
||||
: <span className="text-[10px] bg-dark-700 text-slate-600 border border-slate-700/30 px-1.5 py-0.5 rounded ml-auto">○ Inactif</span>}
|
||||
? <span className="text-[10px] bg-emerald-900/30 text-emerald-400 border border-emerald-700/30 px-1.5 py-0.5 rounded ml-auto">● Active</span>
|
||||
: <span className="text-[10px] bg-dark-700 text-slate-600 border border-slate-700/30 px-1.5 py-0.5 rounded ml-auto">○ Inactive</span>}
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Activer</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Enable</label>
|
||||
<button onClick={() => setVarSchedEnabled(!varSchedEnabled)}
|
||||
className={clsx('w-full py-2 rounded border text-sm font-semibold transition-all', {
|
||||
'bg-red-700 border-red-600 text-white': varSchedEnabled,
|
||||
'bg-dark-700 border-slate-700 text-slate-400': !varSchedEnabled,
|
||||
})}>
|
||||
{varSchedEnabled ? '✓ Activé' : '○ Désactivé'}
|
||||
{varSchedEnabled ? '✓ Enabled' : '○ Disabled'}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Intervalle</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Interval</label>
|
||||
<div className="flex gap-1">
|
||||
{[1, 3, 6, 12, 24].map(h => (
|
||||
<button key={h} onClick={() => setVarSchedHours(h)}
|
||||
@@ -974,21 +974,21 @@ export default function Config() {
|
||||
<DollarSign className="w-3.5 h-3.5 text-emerald-400" />
|
||||
<span className="text-sm font-semibold text-slate-300">Scheduler PnL</span>
|
||||
{schedStatus?.pnl?.alive
|
||||
? <span className="text-[10px] bg-emerald-900/30 text-emerald-400 border border-emerald-700/30 px-1.5 py-0.5 rounded ml-auto">● Actif</span>
|
||||
: <span className="text-[10px] bg-dark-700 text-slate-600 border border-slate-700/30 px-1.5 py-0.5 rounded ml-auto">○ Inactif</span>}
|
||||
? <span className="text-[10px] bg-emerald-900/30 text-emerald-400 border border-emerald-700/30 px-1.5 py-0.5 rounded ml-auto">● Active</span>
|
||||
: <span className="text-[10px] bg-dark-700 text-slate-600 border border-slate-700/30 px-1.5 py-0.5 rounded ml-auto">○ Inactive</span>}
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Activer</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Enable</label>
|
||||
<button onClick={() => setPnlSchedEnabled(!pnlSchedEnabled)}
|
||||
className={clsx('w-full py-2 rounded border text-sm font-semibold transition-all', {
|
||||
'bg-emerald-700 border-emerald-600 text-white': pnlSchedEnabled,
|
||||
'bg-dark-700 border-slate-700 text-slate-400': !pnlSchedEnabled,
|
||||
})}>
|
||||
{pnlSchedEnabled ? '✓ Activé' : '○ Désactivé'}
|
||||
{pnlSchedEnabled ? '✓ Enabled' : '○ Disabled'}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Intervalle</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Interval</label>
|
||||
<div className="flex gap-1">
|
||||
{[1, 2, 4, 8, 24].map(h => (
|
||||
<button key={h} onClick={() => setPnlSchedHours(h)}
|
||||
@@ -1010,21 +1010,21 @@ export default function Config() {
|
||||
disabled={savingSched}
|
||||
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
|
||||
<Save className="w-4 h-4" />
|
||||
{savingSched ? 'Sauvegarde…' : 'Appliquer'}
|
||||
{savingSched ? 'Saving…' : 'Apply'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => triggerVarNow()}
|
||||
disabled={triggeringVar}
|
||||
className="flex items-center gap-1.5 border border-red-500/50 text-red-400 hover:bg-red-900/20 disabled:opacity-40 px-4 py-2 rounded text-sm font-semibold">
|
||||
<Gauge className={clsx('w-4 h-4', triggeringVar && 'animate-spin')} />
|
||||
Snapshot VaR maintenant
|
||||
VaR snapshot now
|
||||
</button>
|
||||
<button
|
||||
onClick={() => triggerPnlNow()}
|
||||
disabled={triggeringPnl}
|
||||
className="flex items-center gap-1.5 border border-emerald-500/50 text-emerald-400 hover:bg-emerald-900/20 disabled:opacity-40 px-4 py-2 rounded text-sm font-semibold">
|
||||
<DollarSign className={clsx('w-4 h-4', triggeringPnl && 'animate-spin')} />
|
||||
Snapshot PnL maintenant
|
||||
PnL snapshot now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1037,16 +1037,16 @@ export default function Config() {
|
||||
{/* Data API Keys */}
|
||||
<div className="card">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="section-title mb-0 flex items-center gap-1"><Key className="w-3 h-3" /> Clés API données</div>
|
||||
<div className="section-title mb-0 flex items-center gap-1"><Key className="w-3 h-3" /> Data API Keys</div>
|
||||
<button onClick={() => setShowKeys(!showKeys)} className="text-slate-500 hover:text-slate-300">
|
||||
{showKeys ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{[
|
||||
{ label: 'NewsAPI Key', value: newsapiKey, setter: setNewsapiKey, placeholder: 'Obtenir sur newsapi.org' },
|
||||
{ label: 'EIA API Key', value: eiaKey, setter: setEiaKey, placeholder: 'Obtenir sur eia.gov' },
|
||||
{ label: 'FRED API Key', value: fredKey, setter: setFredKey, placeholder: 'Obtenir sur fred.stlouisfed.org' },
|
||||
{ label: 'NewsAPI Key', value: newsapiKey, setter: setNewsapiKey, placeholder: 'Get at newsapi.org' },
|
||||
{ label: 'EIA API Key', value: eiaKey, setter: setEiaKey, placeholder: 'Get at eia.gov' },
|
||||
{ label: 'FRED API Key', value: fredKey, setter: setFredKey, placeholder: 'Get at fred.stlouisfed.org' },
|
||||
].map(({ label, value, setter, placeholder }) => (
|
||||
<div key={label}>
|
||||
<label className="text-xs text-slate-500 mb-1 block">{label}</label>
|
||||
@@ -1065,7 +1065,7 @@ export default function Config() {
|
||||
disabled={savingKeys || (!newsapiKey && !eiaKey && !fredKey)}
|
||||
className="mt-3 flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
|
||||
<Save className="w-4 h-4" />
|
||||
{savingKeys ? 'Sauvegarde...' : 'Sauvegarder les clés'}
|
||||
{savingKeys ? 'Saving...' : 'Save keys'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1093,14 +1093,14 @@ export default function Config() {
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-white font-semibold">{src.name || key}</span>
|
||||
<span className={clsx('badge text-xs', {
|
||||
'badge-green': doc?.cost === 'Gratuit' || doc?.cost === 'Gratuit total',
|
||||
'badge-blue': doc?.cost?.includes('gratuit') || doc?.cost?.includes('Inscription'),
|
||||
'badge-yellow': doc?.cost?.includes('clé'),
|
||||
'badge-red': doc?.cost?.includes('payante'),
|
||||
'badge-green': doc?.cost === 'Free' || doc?.cost === 'Totally free',
|
||||
'badge-blue': doc?.cost?.includes('free') || doc?.cost?.includes('registration'),
|
||||
'badge-yellow': doc?.cost?.includes('key') || doc?.cost?.includes('key'),
|
||||
'badge-red': doc?.cost?.includes('Paid'),
|
||||
})}>
|
||||
{doc?.cost}
|
||||
</span>
|
||||
{requiresKey && !keySet && <span className="badge badge-orange text-xs">Clé requise</span>}
|
||||
{requiresKey && !keySet && <span className="badge badge-orange text-xs">Key required</span>}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-0.5">{doc?.description}</div>
|
||||
</div>
|
||||
@@ -1121,7 +1121,7 @@ export default function Config() {
|
||||
<button onClick={saveSources} disabled={savingSources || !localSources}
|
||||
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
|
||||
<Save className="w-4 h-4" />
|
||||
{savingSources ? 'Sauvegarde...' : 'Appliquer les changements'}
|
||||
{savingSources ? 'Saving...' : 'Apply changes'}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
@@ -1137,7 +1137,7 @@ export default function Config() {
|
||||
<div className="card border-blue-700/30 bg-blue-900/5">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<h2 className="text-base font-bold text-white flex items-center gap-2">
|
||||
<ShieldAlert className="w-4 h-4 text-blue-400" /> IV Gate — Filtres d'entrée
|
||||
<ShieldAlert className="w-4 h-4 text-blue-400" /> IV Gate — Entry Filters
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setIvGateEnabled(!ivGateEnabled)}
|
||||
@@ -1145,13 +1145,13 @@ export default function Config() {
|
||||
'bg-blue-600 border-blue-500 text-white': ivGateEnabled,
|
||||
'bg-dark-700 border-slate-700 text-slate-400': !ivGateEnabled,
|
||||
})}>
|
||||
{ivGateEnabled ? '✓ Gate actif' : '○ Gate désactivé'}
|
||||
{ivGateEnabled ? '✓ Gate active' : '○ Gate disabled'}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mb-5">
|
||||
Bloque automatiquement les trades avec verdict <span className="text-red-400 font-semibold">ALERT</span> avant
|
||||
qu'ils soient loggés dans le journal. Un trade ALERT est une stratégie incompatible avec le régime de volatilité actuel
|
||||
(ex: Long Call à IVR 100%). Les trades bloqués apparaissent dans les trades skippés avec la raison <span className="text-slate-400 font-mono">[IV_GATE]</span>.
|
||||
Automatically blocks trades with verdict <span className="text-red-400 font-semibold">ALERT</span> before
|
||||
they are logged in the journal. An ALERT trade is a strategy incompatible with the current volatility regime
|
||||
(e.g. Long Call at IVR 100%). Blocked trades appear in skipped trades with reason <span className="text-slate-400 font-mono">[IV_GATE]</span>.
|
||||
</p>
|
||||
|
||||
<div className={clsx('space-y-5 transition-opacity', !ivGateEnabled && 'opacity-40 pointer-events-none')}>
|
||||
@@ -1160,7 +1160,7 @@ export default function Config() {
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="text-xs text-slate-400 font-medium">
|
||||
Seuil vol chère (IVR High)
|
||||
High vol threshold (IVR High)
|
||||
</label>
|
||||
<span className={clsx('font-mono font-bold text-base', ivGateHigh >= 70 ? 'text-orange-400' : 'text-yellow-400')}>
|
||||
{ivGateHigh}%
|
||||
@@ -1186,7 +1186,7 @@ export default function Config() {
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="text-xs text-slate-400 font-medium">
|
||||
Seuil vol extrême (IVR Extreme)
|
||||
Extreme vol threshold (IVR Extreme)
|
||||
</label>
|
||||
<span className="font-mono font-bold text-base text-red-400">{ivGateExtreme}%</span>
|
||||
</div>
|
||||
@@ -1202,8 +1202,8 @@ export default function Config() {
|
||||
<span>50%</span><span>100%</span>
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-500 mt-1.5 space-y-0.5">
|
||||
<div>IVR > {ivGateExtreme}% → <span className="text-red-400 font-semibold">BLOQUÉ</span> — naked long vol → ALERT</div>
|
||||
<div>Straddle / Strangle → ALERT dès IVR > {ivGateExtreme}% (pénalité ×1.3)</div>
|
||||
<div>IVR > {ivGateExtreme}% → <span className="text-red-400 font-semibold">BLOCKED</span> — naked long vol → ALERT</div>
|
||||
<div>Straddle / Strangle → ALERT when IVR > {ivGateExtreme}% (penalty ×1.3)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1213,7 +1213,7 @@ export default function Config() {
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="text-xs text-slate-400 font-medium">
|
||||
Seuil skew put (protection chère)
|
||||
Put skew threshold (expensive protection)
|
||||
</label>
|
||||
<span className="font-mono font-bold text-base text-orange-400">{ivGateSkew} pts</span>
|
||||
</div>
|
||||
@@ -1222,31 +1222,31 @@ export default function Config() {
|
||||
onChange={e => setIvGateSkew(parseInt(e.target.value))}
|
||||
className="w-full accent-orange-500" />
|
||||
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
|
||||
<span>2pts (très sensible)</span><span>20pts (très tolérant)</span>
|
||||
<span>2pts (very sensitive)</span><span>20pts (very tolerant)</span>
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-500 mt-1.5">
|
||||
Skew put > {ivGateSkew}pts → puts très chers, signal WARN sur Long Put
|
||||
Put skew > {ivGateSkew}pts → puts very expensive, WARN signal on Long Put
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Visual summary */}
|
||||
<div className="bg-dark-700/60 rounded-lg px-4 py-3 text-xs space-y-2">
|
||||
<div className="text-slate-500 font-semibold mb-2 text-[10px] uppercase tracking-wide">Résumé du gate actuel</div>
|
||||
<div className="text-slate-500 font-semibold mb-2 text-[10px] uppercase tracking-wide">Current gate summary</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-emerald-500" />
|
||||
<span className="text-slate-400">IVR < <span className="text-emerald-400 font-mono">{ivGateHigh}%</span> → Toute stratégie OK</span>
|
||||
<span className="text-slate-400">IVR < <span className="text-emerald-400 font-mono">{ivGateHigh}%</span> → Any strategy OK</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-yellow-500" />
|
||||
<span className="text-slate-400">IVR <span className="text-yellow-400 font-mono">{ivGateHigh}–{ivGateExtreme}%</span> → Spreads préférés</span>
|
||||
<span className="text-slate-400">IVR <span className="text-yellow-400 font-mono">{ivGateHigh}–{ivGateExtreme}%</span> → Spreads preferred</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-red-500 animate-pulse" />
|
||||
<span className="text-slate-400">IVR > <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 > <span className="text-red-400 font-mono">{ivGateExtreme}%</span> → Long naked → <strong className="text-red-400">BLOCKED</strong></span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-orange-500" />
|
||||
<span className="text-slate-400">Skew > <span className="text-orange-400 font-mono">{ivGateSkew}pts</span> → Puts chers (WARN)</span>
|
||||
<span className="text-slate-400">Skew > <span className="text-orange-400 font-mono">{ivGateSkew}pts</span> → Expensive puts (WARN)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1257,12 +1257,12 @@ export default function Config() {
|
||||
onClick={() => saveOptionsGate(
|
||||
{ iv_gate_enabled: ivGateEnabled, iv_gate_ivr_high: ivGateHigh,
|
||||
iv_gate_ivr_extreme: ivGateExtreme, iv_gate_skew_threshold: ivGateSkew },
|
||||
{ onSuccess: () => { setSavedMsg('IV Gate sauvegardé'); setTimeout(() => setSavedMsg(''), 2000) } }
|
||||
{ onSuccess: () => { setSavedMsg('IV Gate saved'); setTimeout(() => setSavedMsg(''), 2000) } }
|
||||
)}
|
||||
disabled={savingOptionsGate}
|
||||
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
|
||||
<Save className="w-4 h-4" />
|
||||
{savingOptionsGate ? 'Sauvegarde...' : 'Sauvegarder le gate'}
|
||||
{savingOptionsGate ? 'Saving...' : 'Save gate'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1270,16 +1270,16 @@ export default function Config() {
|
||||
{/* ── Paramètres de sortie ── */}
|
||||
<div className="card">
|
||||
<h2 className="text-base font-bold text-white flex items-center gap-2 mb-1">
|
||||
<Lock className="w-4 h-4 text-amber-400" /> Paramètres de sortie des trades
|
||||
<Lock className="w-4 h-4 text-amber-400" /> Trade Exit Settings
|
||||
</h2>
|
||||
<p className="text-xs text-slate-500 mb-5">
|
||||
Valeurs par défaut pour les objectifs et stop-loss. Chaque trade peut avoir ses propres seuils (Journal → Ouverts).
|
||||
Default values for targets and stop-losses. Each trade can have its own thresholds (Journal → Open).
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-6 mb-4">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-2 block">
|
||||
Objectif de gain : <span className="text-emerald-400 font-mono font-bold">+{exitTarget}%</span>
|
||||
<span className="text-slate-600 ml-1">(P&L sous-jacent)</span>
|
||||
Profit target: <span className="text-emerald-400 font-mono font-bold">+{exitTarget}%</span>
|
||||
<span className="text-slate-600 ml-1">(underlying P&L)</span>
|
||||
</label>
|
||||
<input type="range" min="5" max="150" step="5"
|
||||
value={exitTarget}
|
||||
@@ -1291,8 +1291,8 @@ export default function Config() {
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-2 block">
|
||||
Stop-loss : <span className="text-red-400 font-mono font-bold">{exitStop}%</span>
|
||||
<span className="text-slate-600 ml-1">(P&L sous-jacent)</span>
|
||||
Stop-loss: <span className="text-red-400 font-mono font-bold">{exitStop}%</span>
|
||||
<span className="text-slate-600 ml-1">(underlying P&L)</span>
|
||||
</label>
|
||||
<input type="range" min="-90" max="-5" step="5"
|
||||
value={exitStop}
|
||||
@@ -1305,11 +1305,11 @@ export default function Config() {
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-6 mb-5">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-2 block">Mode signal IA de retournement</label>
|
||||
<label className="text-xs text-slate-500 mb-2 block">AI reversal signal mode</label>
|
||||
<div className="flex gap-2">
|
||||
{[
|
||||
{ value: 'badge_only', label: '🔔 Badge uniquement' },
|
||||
{ value: 'auto', label: '⚡ Auto (futur)' },
|
||||
{ value: 'badge_only', label: '🔔 Badge only' },
|
||||
{ value: 'auto', label: '⚡ Auto (future)' },
|
||||
].map(opt => (
|
||||
<button key={opt.value} onClick={() => setExitSignalMode(opt.value)}
|
||||
className={clsx('flex-1 py-2 rounded text-xs border transition-all', {
|
||||
@@ -1321,19 +1321,19 @@ export default function Config() {
|
||||
))}
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-600 mt-1">
|
||||
Badge only : alerte visible, vous confirmez manuellement. Auto : fermeture proposée.
|
||||
Badge only: visible alert, you confirm manually. Auto: closure suggested.
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-2 block">
|
||||
Seuil retournement signal : <span className="text-blue-400 font-mono font-bold">{exitSignalThreshold}pts</span>
|
||||
Signal reversal threshold: <span className="text-blue-400 font-mono font-bold">{exitSignalThreshold}pts</span>
|
||||
</label>
|
||||
<input type="range" min="10" max="60" step="5"
|
||||
value={exitSignalThreshold}
|
||||
onChange={e => setExitSignalThreshold(parseFloat(e.target.value))}
|
||||
className="w-full accent-blue-500" />
|
||||
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
|
||||
<span>10pts (sensible)</span><span>60pts (tolérant)</span>
|
||||
<span>10pts (sensitive)</span><span>60pts (tolerant)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1341,12 +1341,12 @@ export default function Config() {
|
||||
onClick={() => saveExitDefaults(
|
||||
{ target_pct: exitTarget, stop_loss_pct: exitStop,
|
||||
signal_reversal_mode: exitSignalMode, signal_reversal_threshold: exitSignalThreshold },
|
||||
{ onSuccess: () => { setSavedMsg('Paramètres de sortie sauvegardés'); setTimeout(() => setSavedMsg(''), 2000) } }
|
||||
{ onSuccess: () => { setSavedMsg('Exit settings saved'); setTimeout(() => setSavedMsg(''), 2000) } }
|
||||
)}
|
||||
disabled={savingExitDefaults}
|
||||
className="flex items-center gap-1.5 bg-amber-600 hover:bg-amber-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
|
||||
<Save className="w-4 h-4" />
|
||||
{savingExitDefaults ? 'Sauvegarde...' : 'Sauvegarder les seuils'}
|
||||
{savingExitDefaults ? 'Saving...' : 'Save thresholds'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1354,7 +1354,7 @@ export default function Config() {
|
||||
<div className="card border-purple-700/30 bg-purple-900/5">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<h2 className="text-base font-bold text-white flex items-center gap-2">
|
||||
<TrendingUp className="w-4 h-4 text-purple-400" /> Indicateurs techniques du sous-jacent
|
||||
<TrendingUp className="w-4 h-4 text-purple-400" /> Underlying Technical Indicators
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setTechEnabled(!techEnabled)}
|
||||
@@ -1362,23 +1362,23 @@ export default function Config() {
|
||||
'bg-purple-600 border-purple-500 text-white': techEnabled,
|
||||
'bg-dark-700 border-slate-700 text-slate-400': !techEnabled,
|
||||
})}>
|
||||
{techEnabled ? '✓ Activé' : '○ Désactivé'}
|
||||
{techEnabled ? '✓ Enabled' : '○ Disabled'}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mb-4">
|
||||
Injecte RSI, MA, Bollinger et ATR dans le contexte IA à chaque cycle. Les périodes sont calibrées automatiquement selon l'horizon de l'option analysée.
|
||||
Injects RSI, MA, Bollinger and ATR into the AI context at each cycle. Periods are automatically calibrated based on the option's horizon.
|
||||
</p>
|
||||
|
||||
<div className={clsx('space-y-4 transition-opacity', !techEnabled && 'opacity-40 pointer-events-none')}>
|
||||
{/* Indicateurs à activer */}
|
||||
<div>
|
||||
<label className="text-xs text-slate-400 font-medium block mb-2">Indicateurs actifs</label>
|
||||
<label className="text-xs text-slate-400 font-medium block mb-2">Active indicators</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{[
|
||||
{ key: 'rsi', label: 'RSI', desc: 'Momentum / surachat-survente' },
|
||||
{ key: 'ma', label: 'MA fast/slow', desc: 'Tendance & golden/death cross' },
|
||||
{ key: 'bollinger', label: 'Bollinger', desc: 'Position dans les bandes de vol' },
|
||||
{ key: 'atr', label: 'ATR', desc: 'Volatilité réalisée récente' },
|
||||
{ key: 'rsi', label: 'RSI', desc: 'Momentum / overbought-oversold' },
|
||||
{ key: 'ma', label: 'MA fast/slow', desc: 'Trend & golden/death cross' },
|
||||
{ key: 'bollinger', label: 'Bollinger', desc: 'Position in vol bands' },
|
||||
{ key: 'atr', label: 'ATR', desc: 'Recent realized volatility' },
|
||||
].map(({ key, label, desc }) => {
|
||||
const active = techList.includes(key)
|
||||
return (
|
||||
@@ -1397,13 +1397,13 @@ export default function Config() {
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-600 mt-1.5">Survolez pour voir la description de chaque indicateur.</p>
|
||||
<p className="text-[10px] text-slate-600 mt-1.5">Hover to see each indicator's description.</p>
|
||||
</div>
|
||||
|
||||
{/* Calibration auto */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-slate-300 font-medium">Calibration automatique selon horizon</p>
|
||||
<p className="text-xs text-slate-300 font-medium">Auto-calibration by horizon</p>
|
||||
<p className="text-[10px] text-slate-500">≤30j → RSI14, MA20/50 | ≤90j → RSI21, MA50/100 | >90j → RSI28, MA100/200</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -1412,7 +1412,7 @@ export default function Config() {
|
||||
'bg-purple-600 border-purple-500 text-white': techAutoCalibrate,
|
||||
'bg-dark-700 border-slate-700 text-slate-400': !techAutoCalibrate,
|
||||
})}>
|
||||
{techAutoCalibrate ? '✓ Auto' : '○ Fixe'}
|
||||
{techAutoCalibrate ? '✓ Auto' : '○ Fixed'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1420,12 +1420,12 @@ export default function Config() {
|
||||
<button
|
||||
onClick={() => saveTechIndicators(
|
||||
{ tech_indicators_enabled: techEnabled, tech_indicators_list: techList.join(','), tech_indicators_auto_calibrate: techAutoCalibrate },
|
||||
{ onSuccess: () => { setSavedMsg('Indicateurs techniques sauvegardés'); setTimeout(() => setSavedMsg(''), 2000) } }
|
||||
{ onSuccess: () => { setSavedMsg('Technical indicators saved'); setTimeout(() => setSavedMsg(''), 2000) } }
|
||||
)}
|
||||
disabled={savingTechIndicators}
|
||||
className="mt-4 flex items-center gap-1.5 bg-purple-700 hover:bg-purple-600 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
|
||||
<Save className="w-4 h-4" />
|
||||
{savingTechIndicators ? 'Sauvegarde...' : 'Sauvegarder les indicateurs'}
|
||||
{savingTechIndicators ? 'Saving...' : 'Save indicators'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,10 +19,10 @@ import {
|
||||
import { scoreColor } from '../components/TradeIdeas'
|
||||
|
||||
const riskGauge = (score: number) => {
|
||||
if (score < 25) return { color: 'text-emerald-400', bg: 'bg-emerald-500', label: 'FAIBLE' }
|
||||
if (score < 50) return { color: 'text-yellow-400', bg: 'bg-yellow-500', label: 'MODÉRÉ' }
|
||||
if (score < 75) return { color: 'text-orange-400', bg: 'bg-orange-500', label: 'ÉLEVÉ' }
|
||||
return { color: 'text-red-400', bg: 'bg-red-500', label: 'EXTRÊME' }
|
||||
if (score < 25) return { color: 'text-emerald-400', bg: 'bg-emerald-500', label: 'LOW' }
|
||||
if (score < 50) return { color: 'text-yellow-400', bg: 'bg-yellow-500', label: 'MODERATE' }
|
||||
if (score < 75) return { color: 'text-orange-400', bg: 'bg-orange-500', label: 'HIGH' }
|
||||
return { color: 'text-red-400', bg: 'bg-red-500', label: 'EXTREME' }
|
||||
}
|
||||
|
||||
const assetEmoji: Record<string, string> = {
|
||||
@@ -37,9 +37,9 @@ const ASSET_COLORS: Record<string, string> = {
|
||||
}
|
||||
|
||||
const REGIME_LABELS: Record<string, string> = {
|
||||
goldilocks: 'Goldilocks', desinflation: 'Désinflation', stagflation: 'Stagflation',
|
||||
recession: 'Récession', crise_liquidite: 'Crise liq.', reflation: 'Reflation',
|
||||
soft_landing: 'Soft Landing', inflation_shock: 'Infl. Choc', incertain: 'Incertain',
|
||||
goldilocks: 'Goldilocks', desinflation: 'Disinflation', stagflation: 'Stagflation',
|
||||
recession: 'Recession', crise_liquidite: 'Liq. Crisis', reflation: 'Reflation',
|
||||
soft_landing: 'Soft Landing', inflation_shock: 'Infl. Shock', incertain: 'Uncertain',
|
||||
}
|
||||
|
||||
function ViewToggle({ value, onChange }: { value: 'simulated' | 'portfolio'; onChange: (v: 'simulated' | 'portfolio') => void }) {
|
||||
@@ -51,7 +51,7 @@ function ViewToggle({ value, onChange }: { value: 'simulated' | 'portfolio'; onC
|
||||
<button onClick={click('simulated')}
|
||||
className={clsx('px-1.5 py-0.5 rounded transition-colors',
|
||||
value === 'simulated' ? 'bg-blue-600 text-white' : 'text-slate-600 hover:text-slate-400')}>
|
||||
Simulé
|
||||
Simulated
|
||||
</button>
|
||||
<button onClick={click('portfolio')}
|
||||
className={clsx('px-1.5 py-0.5 rounded transition-colors',
|
||||
@@ -195,7 +195,7 @@ export default function Dashboard() {
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-white">Cockpit GeoOptions</h1>
|
||||
<h1 className="text-xl font-bold text-white">GeoOptions Cockpit</h1>
|
||||
<p className="text-xs text-slate-500 mt-0.5">
|
||||
{format(new Date(), "EEEE d MMMM yyyy · HH:mm", { locale: fr })}
|
||||
</p>
|
||||
@@ -235,10 +235,10 @@ export default function Dashboard() {
|
||||
<div className="card col-span-1">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="section-title flex items-center gap-1 mb-0">
|
||||
<Globe className="w-3 h-3" /> Risque Géopolitique
|
||||
<Globe className="w-3 h-3" /> Geopolitical Risk
|
||||
</div>
|
||||
<Link to="/geo" className="flex items-center gap-0.5 text-[10px] text-slate-600 hover:text-slate-300 transition-colors">
|
||||
News géo <ArrowUpRight className="w-2.5 h-2.5" />
|
||||
Geo news <ArrowUpRight className="w-2.5 h-2.5" />
|
||||
</Link>
|
||||
</div>
|
||||
{riskLoading ? (
|
||||
@@ -259,12 +259,12 @@ export default function Dashboard() {
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : <div className="text-slate-500 text-xs">Backend requis</div>}
|
||||
) : <div className="text-slate-500 text-xs">Backend required</div>}
|
||||
</div>
|
||||
|
||||
{/* Radar */}
|
||||
<div className="card col-span-1">
|
||||
<div className="section-title">Cartographie risques</div>
|
||||
<div className="section-title">Risk Map</div>
|
||||
{radarData.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height={160}>
|
||||
<RadarChart data={radarData}>
|
||||
@@ -273,18 +273,18 @@ export default function Dashboard() {
|
||||
<Radar dataKey="score" stroke="#3b82f6" fill="#3b82f6" fillOpacity={0.25} />
|
||||
</RadarChart>
|
||||
</ResponsiveContainer>
|
||||
) : <div className="h-40 flex items-center justify-center text-slate-600 text-xs">Chargement...</div>}
|
||||
) : <div className="h-40 flex items-center justify-center text-slate-600 text-xs">Loading...</div>}
|
||||
</div>
|
||||
|
||||
{/* Patterns — AI scores */}
|
||||
<div className="card col-span-1 overflow-y-auto max-h-64">
|
||||
<div className="section-title flex items-center gap-1">
|
||||
<Brain className="w-3 h-3" /> Scores patterns
|
||||
<Brain className="w-3 h-3" /> Pattern Scores
|
||||
{allPatterns.length > 0 && <span className="text-slate-600 text-xs ml-auto font-normal">{allPatterns.length} patterns</span>}
|
||||
</div>
|
||||
<div className="space-y-1.5 mt-1">
|
||||
{allPatterns.length === 0 ? (
|
||||
<div className="text-slate-600 text-xs">Backend requis</div>
|
||||
<div className="text-slate-600 text-xs">Backend required</div>
|
||||
) : (
|
||||
[...allPatterns]
|
||||
.sort((a, b) => {
|
||||
@@ -321,7 +321,7 @@ export default function Dashboard() {
|
||||
|
||||
{/* Calendrier */}
|
||||
<div className="card col-span-1 space-y-3">
|
||||
<div className="section-title flex items-center gap-1"><Clock className="w-3 h-3" /> Prochains catalyseurs</div>
|
||||
<div className="section-title flex items-center gap-1"><Clock className="w-3 h-3" /> Upcoming Catalysts</div>
|
||||
<div className="space-y-1.5">
|
||||
{calendar?.slice(0, 4).map((ev, i) => (
|
||||
<div key={i} className="flex items-center gap-2 text-xs">
|
||||
@@ -397,7 +397,7 @@ export default function Dashboard() {
|
||||
<div className="grid grid-cols-2 gap-2 flex-shrink-0">
|
||||
{/* Left: Unrealized */}
|
||||
<div className="bg-dark-700/40 rounded px-2.5 py-2 border border-slate-700/30">
|
||||
<div className="text-[9px] text-slate-600 uppercase tracking-wide mb-1">Ouvertes</div>
|
||||
<div className="text-[9px] text-slate-600 uppercase tracking-wide mb-1">Open</div>
|
||||
{pnlView === 'simulated' ? (
|
||||
<>
|
||||
<div className={clsx('text-xl font-bold font-mono leading-none',
|
||||
@@ -437,7 +437,7 @@ export default function Dashboard() {
|
||||
|
||||
{/* Right: Realized */}
|
||||
<div className="bg-dark-700/40 rounded px-2.5 py-2 border border-slate-700/30">
|
||||
<div className="text-[9px] text-slate-600 uppercase tracking-wide mb-1">Réalisé</div>
|
||||
<div className="text-[9px] text-slate-600 uppercase tracking-wide mb-1">Realized</div>
|
||||
{pnlView === 'simulated' ? (
|
||||
<>
|
||||
<div className={clsx('text-xl font-bold font-mono leading-none',
|
||||
@@ -451,10 +451,10 @@ export default function Dashboard() {
|
||||
closedProfitEur >= 0 ? 'text-emerald-500' : 'text-red-400')}>
|
||||
{closedCapital > 0
|
||||
? `${closedProfitEur >= 0 ? '+' : ''}${closedProfitEur.toFixed(0)}€`
|
||||
: closedTrades.length > 0 ? 'sans capital' : ''}
|
||||
: closedTrades.length > 0 ? 'no capital' : ''}
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-500 mt-1">
|
||||
{closedTrades.length} fermés · <span className="text-emerald-500">{closedWinners}✓</span>{' '}
|
||||
{closedTrades.length} closed · <span className="text-emerald-500">{closedWinners}✓</span>{' '}
|
||||
<span className="text-red-400">{closedLosers}✗</span>
|
||||
</div>
|
||||
</>
|
||||
@@ -473,7 +473,7 @@ export default function Dashboard() {
|
||||
</div>
|
||||
)}
|
||||
<div className="text-[10px] text-slate-500 mt-1">
|
||||
{pf?.closed_positions ?? 0} fermées
|
||||
{pf?.closed_positions ?? 0} closed
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -483,7 +483,7 @@ export default function Dashboard() {
|
||||
{/* Total line */}
|
||||
<div className="mt-1.5 flex items-center justify-between text-[10px] flex-shrink-0">
|
||||
<span className="text-slate-600">
|
||||
Investi{isEstimated ? ' ~' : ''} {pnlView === 'simulated'
|
||||
Invested{isEstimated ? ' ~' : ''} {pnlView === 'simulated'
|
||||
? (totalCapital > 0 ? `${totalCapital.toFixed(0)}€` : '—')
|
||||
: (pfInvest != null ? `${pfInvest.toFixed(0)}€` : '—')}
|
||||
</span>
|
||||
@@ -509,7 +509,7 @@ export default function Dashboard() {
|
||||
const gradId = 'pnlGrad'
|
||||
return (
|
||||
<div className="mt-1.5 pt-1.5 border-t border-slate-700/30">
|
||||
<div className="text-[9px] text-slate-600 mb-0.5">Courbe PnL historique ({snaps.length} pts)</div>
|
||||
<div className="text-[9px] text-slate-600 mb-0.5">Historical PnL curve ({snaps.length} pts)</div>
|
||||
<ResponsiveContainer width="100%" height={58}>
|
||||
<AreaChart data={chartData} margin={{ top: 2, right: 0, left: -38, bottom: 0 }}>
|
||||
<defs>
|
||||
@@ -552,14 +552,14 @@ export default function Dashboard() {
|
||||
return (
|
||||
<Link to="/risk" className="card flex flex-col h-[288px] overflow-hidden hover:border-slate-600/60 transition-all cursor-pointer">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="section-title mb-0 text-[10px]">🛡️ Risque</span>
|
||||
<span className="section-title mb-0 text-[10px]">🛡️ Risk</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ViewToggle value={riskView} onChange={setRiskViewPersist} />
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
</div>
|
||||
</div>
|
||||
<div className={clsx('text-lg font-bold mt-0.5', alertCount > 0 ? 'text-red-400' : 'text-emerald-400')}>
|
||||
{alertCount > 0 ? `${alertCount} alerte${alertCount > 1 ? 's' : ''}` : 'OK'}
|
||||
{alertCount > 0 ? `${alertCount} alert${alertCount > 1 ? 's' : ''}` : 'OK'}
|
||||
</div>
|
||||
{sortedClasses.length > 0 && (
|
||||
<div className="mt-2 space-y-1">
|
||||
@@ -568,11 +568,11 @@ export default function Dashboard() {
|
||||
<span className="text-[9px] text-slate-500 w-12 truncate capitalize">{cls}</span>
|
||||
<div className="flex-1 h-2 bg-dark-700 rounded-full overflow-hidden flex">
|
||||
{data.bullish > 0 && (
|
||||
<div className="h-full rounded-l-full" title={`${data.bullish} haussier`}
|
||||
<div className="h-full rounded-l-full" title={`${data.bullish} bullish`}
|
||||
style={{ width: `${(data.bullish / openCount) * 100}%`, background: ASSET_COLORS[cls] ?? '#3b82f6', opacity: 0.9 }} />
|
||||
)}
|
||||
{data.bearish > 0 && (
|
||||
<div className="h-full rounded-r-full" title={`${data.bearish} baissier`}
|
||||
<div className="h-full rounded-r-full" title={`${data.bearish} bearish`}
|
||||
style={{ width: `${(data.bearish / openCount) * 100}%`, background: '#ef4444', opacity: 0.7 }} />
|
||||
)}
|
||||
</div>
|
||||
@@ -580,15 +580,15 @@ export default function Dashboard() {
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-center gap-1 text-[9px] text-slate-700 pt-0.5">
|
||||
<span className="inline-block w-2 h-1.5 rounded-sm bg-blue-500/60"></span>haussier
|
||||
<span className="inline-block w-2 h-1.5 rounded-sm bg-red-500/60 ml-1"></span>baissier
|
||||
{conflictCount > 0 && <span className="ml-auto text-red-400">{conflictCount} conflit{conflictCount > 1 ? 's' : ''}</span>}
|
||||
<span className="inline-block w-2 h-1.5 rounded-sm bg-blue-500/60"></span>bullish
|
||||
<span className="inline-block w-2 h-1.5 rounded-sm bg-red-500/60 ml-1"></span>bearish
|
||||
{conflictCount > 0 && <span className="ml-auto text-red-400">{conflictCount} conflict{conflictCount > 1 ? 's' : ''}</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{sortedClasses.length === 0 && (
|
||||
<div className="text-[10px] text-slate-600 mt-1">
|
||||
{openCount > 0 ? `${openCount} positions` : 'Aucune position'}
|
||||
{openCount > 0 ? `${openCount} positions` : 'No positions'}
|
||||
</div>
|
||||
)}
|
||||
{/* Latest VaR snapshot */}
|
||||
@@ -629,7 +629,7 @@ export default function Dashboard() {
|
||||
return (
|
||||
<Link to="/journal" className="card flex flex-col h-[288px] overflow-hidden hover:border-slate-600/60 transition-all cursor-pointer">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="section-title mb-0 text-[10px]">🔄 Derniers Cycles</span>
|
||||
<span className="section-title mb-0 text-[10px]">🔄 Recent Cycles</span>
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
</div>
|
||||
{/* Column headers */}
|
||||
@@ -638,7 +638,7 @@ export default function Dashboard() {
|
||||
<span className="text-[8px] text-slate-700 w-8 shrink-0 text-right">Pat.</span>
|
||||
<span className="text-[8px] text-slate-700 w-8 shrink-0 text-right">Log.</span>
|
||||
<span className="text-[8px] text-slate-700 w-8 shrink-0 text-right">Clos</span>
|
||||
<span className="text-[8px] text-slate-700 flex-1 truncate">Régime</span>
|
||||
<span className="text-[8px] text-slate-700 flex-1 truncate">Regime</span>
|
||||
</div>
|
||||
{runs.length > 0 ? (
|
||||
<div className="space-y-2.5 flex-1 overflow-hidden">
|
||||
@@ -668,7 +668,7 @@ export default function Dashboard() {
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-[10px] text-slate-600 mt-1">Aucun cycle enregistré</div>
|
||||
<div className="text-[10px] text-slate-600 mt-1">No cycles recorded</div>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
@@ -694,38 +694,38 @@ export default function Dashboard() {
|
||||
key: 'vix', label: 'VIX',
|
||||
color: (v: number) => v < 18 ? 'text-emerald-400' : v < 25 ? 'text-yellow-400' : 'text-red-400',
|
||||
fmt: (v: number) => v.toFixed(1),
|
||||
hint: (v: number) => v < 18 ? '↓ risque bas' : v < 25 ? '↑ alerte' : '↑ crise',
|
||||
hint: (v: number) => v < 18 ? '↓ low risk' : v < 25 ? '↑ alert' : '↑ crisis',
|
||||
},
|
||||
{
|
||||
key: 'spx_vs_200d', label: 'S&P/200j',
|
||||
key: 'spx_vs_200d', label: 'S&P/200d',
|
||||
color: (v: number) => v >= 0 ? 'text-emerald-400' : 'text-red-400',
|
||||
fmt: (v: number) => `${v >= 0 ? '+' : ''}${v.toFixed(1)}%`,
|
||||
hint: (v: number) => v >= 5 ? 'bull fort' : v >= 0 ? 'bull mod.' : 'bear',
|
||||
hint: (v: number) => v >= 5 ? 'strong bull' : v >= 0 ? 'mod. bull' : 'bear',
|
||||
},
|
||||
{
|
||||
key: 'slope_10y3m', label: 'Pente 10Y-3M',
|
||||
key: 'slope_10y3m', label: 'Slope 10Y-3M',
|
||||
color: (v: number) => v >= 0.5 ? 'text-emerald-400' : v >= 0 ? 'text-yellow-400' : 'text-red-400',
|
||||
fmt: (v: number) => `${v >= 0 ? '+' : ''}${v.toFixed(2)}`,
|
||||
hint: (v: number) => v >= 0.5 ? 'normale' : v >= 0 ? 'plate' : 'inversée',
|
||||
hint: (v: number) => v >= 0.5 ? 'normal' : v >= 0 ? 'flat' : 'inverted',
|
||||
},
|
||||
{
|
||||
key: 'copper', label: 'Cuivre',
|
||||
key: 'copper', label: 'Copper',
|
||||
color: (v: number, chg: number) => chg >= 0 ? 'text-emerald-400' : 'text-red-400',
|
||||
fmt: (v: number, chg: number) => `${chg >= 0 ? '+' : ''}${chg.toFixed(2)}%`,
|
||||
hint: (v: number, chg: number) => chg >= 0 ? '↑ croissance' : '↓ ralentis.',
|
||||
hint: (v: number, chg: number) => chg >= 0 ? '↑ growth' : '↓ slowing',
|
||||
},
|
||||
{
|
||||
key: 'gold', label: 'Or',
|
||||
key: 'gold', label: 'Gold',
|
||||
color: (v: number, chg: number) => chg >= 1 ? 'text-amber-400' : chg <= -1 ? 'text-emerald-400' : 'text-slate-400',
|
||||
fmt: (v: number, chg: number) => `${chg >= 0 ? '+' : ''}${chg.toFixed(2)}%`,
|
||||
hint: (v: number, chg: number) => chg >= 1 ? '↑ refuge' : chg <= -1 ? '↓ risk-on' : '≈ neutre',
|
||||
hint: (v: number, chg: number) => chg >= 1 ? '↑ safe haven' : chg <= -1 ? '↓ risk-on' : '≈ neutral',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Link to="/macro" className="card flex flex-col h-[288px] overflow-hidden hover:border-slate-600/60 transition-all cursor-pointer">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="section-title mb-0 text-[10px]">🌐 Régime Macro</span>
|
||||
<span className="section-title mb-0 text-[10px]">🌐 Macro Regime</span>
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
</div>
|
||||
<div className={clsx('text-base font-bold mt-1', colorClass)}>
|
||||
@@ -834,7 +834,7 @@ export default function Dashboard() {
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-[10px] text-slate-600 mt-1">Aucun trade loggé</div>
|
||||
<div className="text-[10px] text-slate-600 mt-1">No trades logged</div>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
@@ -849,7 +849,7 @@ export default function Dashboard() {
|
||||
return (
|
||||
<Link to="/journal" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="section-title mb-0 text-[10px]">📥 Derniers trades loggés</span>
|
||||
<span className="section-title mb-0 text-[10px]">📥 Latest Logged Trades</span>
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
</div>
|
||||
{recent.length > 0 ? (
|
||||
@@ -873,7 +873,7 @@ export default function Dashboard() {
|
||||
{pnl >= 0 ? '+' : ''}{pnl.toFixed(1)}%
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-slate-700 shrink-0 text-[9px]">en cours</span>
|
||||
<span className="text-slate-700 shrink-0 text-[9px]">in progress</span>
|
||||
)}
|
||||
</div>
|
||||
{entryDate && (
|
||||
@@ -884,7 +884,7 @@ export default function Dashboard() {
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-[10px] text-slate-600 mt-1">Aucun trade loggé</div>
|
||||
<div className="text-[10px] text-slate-600 mt-1">No trades logged</div>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
@@ -899,7 +899,7 @@ export default function Dashboard() {
|
||||
return (
|
||||
<Link to="/patterns" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="section-title mb-0 text-[10px]">⭐ Derniers Patterns ajoutés</span>
|
||||
<span className="section-title mb-0 text-[10px]">⭐ Latest Patterns Added</span>
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
</div>
|
||||
{recent.length > 0 ? (
|
||||
@@ -932,7 +932,7 @@ export default function Dashboard() {
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-[10px] text-slate-600 mt-1">Aucun pattern enregistré</div>
|
||||
<div className="text-[10px] text-slate-600 mt-1">No patterns recorded</div>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
@@ -969,7 +969,7 @@ export default function Dashboard() {
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-[10px] text-slate-600 mt-1">Chargement des news…</div>
|
||||
<div className="text-[10px] text-slate-600 mt-1">Loading news…</div>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
@@ -982,7 +982,7 @@ export default function Dashboard() {
|
||||
<div key={cls} className="card">
|
||||
<div className="section-title">{assetEmoji[cls]} {cls}</div>
|
||||
{allQuotes?.[cls]?.map(q => <QuoteRow key={q.symbol} q={q} />) ?? (
|
||||
<div className="text-xs text-slate-600">Chargement...</div>
|
||||
<div className="text-xs text-slate-600">Loading...</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -13,16 +13,16 @@ const CATEGORY_COLORS: Record<string, string> = {
|
||||
}
|
||||
|
||||
const CATEGORY_LABELS: Record<string, string> = {
|
||||
military: '⚔️ Militaire', sanctions: '🚫 Sanctions', elections: '🗳️ Élections',
|
||||
natural_disaster: '🌪️ Catastrophe', health_crisis: '🏥 Santé',
|
||||
resource_scarcity: '⚠️ Ressources', trade_war: '🤝 Commerce',
|
||||
energy: '⚡ Énergie', political_speech: '🎙️ Discours',
|
||||
financial_crisis: '💸 Finance', general: '📰 Général',
|
||||
military: '⚔️ Military', sanctions: '🚫 Sanctions', elections: '🗳️ Elections',
|
||||
natural_disaster: '🌪️ Disaster', health_crisis: '🏥 Health',
|
||||
resource_scarcity: '⚠️ Resources', trade_war: '🤝 Trade',
|
||||
energy: '⚡ Energy', political_speech: '🎙️ Speech',
|
||||
financial_crisis: '💸 Finance', general: '📰 General',
|
||||
}
|
||||
|
||||
const ASSET_LABELS: Record<string, string> = {
|
||||
energy: '⛽ Énergie', metals: '🥇 Métaux', agriculture: '🌾 Agri',
|
||||
equities: '📈 Actions', indices: '📊 Indices', forex: '💱 Forex',
|
||||
energy: '⛽ Energy', metals: '🥇 Metals', agriculture: '🌾 Agri',
|
||||
equities: '📈 Equities', indices: '📊 Indices', forex: '💱 Forex',
|
||||
}
|
||||
|
||||
function ImpactBar({ value, label }: { value: number; label: string }) {
|
||||
@@ -90,14 +90,14 @@ function NewsCard({ news }: { news: GeoNews }) {
|
||||
)
|
||||
})}
|
||||
{(news as any).ai_resolution && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-blue-900/30 text-blue-400">🕊 résolution</span>
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-blue-900/30 text-blue-400">🕊 resolution</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-slate-400 leading-relaxed mb-3">{news.summary}</p>
|
||||
{Object.keys(news.asset_impacts).length > 0 && (
|
||||
<div className="mb-3">
|
||||
<div className="text-xs text-slate-600 mb-1">Impact par classe d'actif</div>
|
||||
<div className="text-xs text-slate-600 mb-1">Impact by asset class</div>
|
||||
{Object.entries(news.asset_impacts).map(([cls, val]) => (
|
||||
<ImpactBar key={cls} label={ASSET_LABELS[cls] ?? cls} value={val} />
|
||||
))}
|
||||
@@ -114,14 +114,14 @@ function NewsCard({ news }: { news: GeoNews }) {
|
||||
<a href={news.url} target="_blank" rel="noopener noreferrer"
|
||||
className="text-xs text-blue-400 hover:text-blue-300 flex items-center gap-1"
|
||||
onClick={e => e.stopPropagation()}>
|
||||
<ExternalLink className="w-3 h-3" /> Lire l'article
|
||||
<ExternalLink className="w-3 h-3" /> Read article
|
||||
</a>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center justify-between mt-2 text-xs text-slate-600">
|
||||
<span>{news.date?.slice(0, 16)}</span>
|
||||
<span>{expanded ? '▲ Réduire' : '▼ Détail'}</span>
|
||||
<span>{expanded ? '▲ Collapse' : '▼ Details'}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -156,7 +156,7 @@ function PatternRelevanceCard({ p }: { p: any }) {
|
||||
<div className="text-sm font-semibold text-white line-clamp-1">{p.name}</div>
|
||||
<div className="flex items-center gap-1 mt-0.5 flex-wrap">
|
||||
<span className="badge badge-blue text-xs">{p.asset_class}</span>
|
||||
<span className="text-xs text-slate-500">{p.keyword_hits}/{p.keyword_total} mots-clés</span>
|
||||
<span className="text-xs text-slate-500">{p.keyword_hits}/{p.keyword_total} keywords</span>
|
||||
{hasNews && (
|
||||
<span className="text-xs text-emerald-400 flex items-center gap-0.5">
|
||||
<Search className="w-2.5 h-2.5" />{p.matching_news.length} news
|
||||
@@ -170,7 +170,7 @@ function PatternRelevanceCard({ p }: { p: any }) {
|
||||
: 'bg-emerald-900/30 text-emerald-400 border border-emerald-700/30'
|
||||
)}>
|
||||
<Brain className="w-2.5 h-2.5" />
|
||||
{p.ai_contra_signal ? `contre-signal (${p.ai_alignment > 0 ? '+' : ''}${p.ai_alignment}pts)` : `IA aligné (+${p.ai_alignment}pts)`}
|
||||
{p.ai_contra_signal ? `counter-signal (${p.ai_alignment > 0 ? '+' : ''}${p.ai_alignment}pts)` : `AI aligned (+${p.ai_alignment}pts)`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -211,7 +211,7 @@ function PatternRelevanceCard({ p }: { p: any }) {
|
||||
<>
|
||||
<button onClick={() => setExpanded(!expanded)}
|
||||
className="text-xs text-slate-600 hover:text-slate-400 mb-2">
|
||||
{expanded ? '▲ Masquer les news' : `▼ Voir les ${p.matching_news.length} news correspondantes`}
|
||||
{expanded ? '▲ Hide news' : `▼ See ${p.matching_news.length} matching news`}
|
||||
</button>
|
||||
{expanded && (
|
||||
<div className="space-y-1.5">
|
||||
@@ -235,7 +235,7 @@ function PatternRelevanceCard({ p }: { p: any }) {
|
||||
<a href={n.url} target="_blank" rel="noopener noreferrer"
|
||||
className="text-xs text-blue-400 hover:text-blue-300 flex items-center gap-1 mt-1"
|
||||
onClick={e => e.stopPropagation()}>
|
||||
<ExternalLink className="w-2.5 h-2.5" /> Lire
|
||||
<ExternalLink className="w-2.5 h-2.5" /> Read
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
@@ -246,7 +246,7 @@ function PatternRelevanceCard({ p }: { p: any }) {
|
||||
)}
|
||||
|
||||
{!hasNews && p.relevance === 0 && (
|
||||
<div className="text-xs text-slate-600">Aucune news correspondante sur la période</div>
|
||||
<div className="text-xs text-slate-600">No matching news for this period</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
@@ -256,10 +256,10 @@ type SortMode = 'impact_desc' | 'impact_asc' | 'date_desc' | 'date_asc'
|
||||
type DateRange = 'all' | 'today' | '7d' | '30d'
|
||||
|
||||
const DATE_RANGE_LABELS: Record<DateRange, string> = {
|
||||
all: 'Toutes', today: "Aujourd'hui", '7d': '7 jours', '30d': '30 jours',
|
||||
all: 'All', today: 'Today', '7d': '7 days', '30d': '30 days',
|
||||
}
|
||||
const SORT_LABELS: Record<SortMode, string> = {
|
||||
impact_desc: 'Impact ↓', impact_asc: 'Impact ↑', date_desc: 'Récentes ↓', date_asc: 'Anciennes ↑',
|
||||
impact_desc: 'Impact ↓', impact_asc: 'Impact ↑', date_desc: 'Recent ↓', date_asc: 'Oldest ↑',
|
||||
}
|
||||
|
||||
function isWithinRange(dateStr: string, range: DateRange): boolean {
|
||||
@@ -304,10 +304,10 @@ export default function GeoRadar() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-white flex items-center gap-2">
|
||||
<Globe className="w-5 h-5 text-blue-400" /> Radar Géopolitique
|
||||
<Globe className="w-5 h-5 text-blue-400" /> Geopolitical Radar
|
||||
</h1>
|
||||
<p className="text-xs text-slate-500 mt-0.5">
|
||||
Flux d'actualités · Correspondance avec les patterns de trading
|
||||
News feed · Pattern matching for trading signals
|
||||
</p>
|
||||
</div>
|
||||
{riskScore && (
|
||||
@@ -337,7 +337,7 @@ export default function GeoRadar() {
|
||||
'bg-blue-600 text-white': tab === t,
|
||||
'text-slate-400 hover:text-slate-200': tab !== t,
|
||||
})}>
|
||||
{t === 'news' ? `📰 Actualités (${news?.length ?? 0})` : `🧩 Patterns (${patternsWithNews}/${totalPatterns} avec news)`}
|
||||
{t === 'news' ? `📰 News (${news?.length ?? 0})` : `🧩 Patterns (${patternsWithNews}/${totalPatterns} with news)`}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -345,7 +345,7 @@ export default function GeoRadar() {
|
||||
{/* Day selector — only shown on patterns tab */}
|
||||
{tab === 'patterns' && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-slate-500">Fenêtre :</span>
|
||||
<span className="text-xs text-slate-500">Window:</span>
|
||||
<div className="flex gap-0.5 bg-dark-700 p-0.5 rounded">
|
||||
{[1, 2, 7, 30].map(d => (
|
||||
<button key={d} onClick={() => setDays(d)}
|
||||
@@ -353,11 +353,11 @@ export default function GeoRadar() {
|
||||
'bg-slate-600 text-white': days === d,
|
||||
'text-slate-400 hover:text-slate-200': days !== d,
|
||||
})}>
|
||||
{d}j
|
||||
{d}d
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-xs text-slate-600">derniers jours</span>
|
||||
<span className="text-xs text-slate-600">last days</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -375,7 +375,7 @@ export default function GeoRadar() {
|
||||
'bg-blue-600 border-blue-500 text-white': filterCat === cat,
|
||||
'border-slate-700 text-slate-400 hover:border-slate-500': filterCat !== cat,
|
||||
})}>
|
||||
{cat === 'all' ? 'Toutes catégories' : CATEGORY_LABELS[cat] ?? cat}
|
||||
{cat === 'all' ? 'All categories' : CATEGORY_LABELS[cat] ?? cat}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -384,7 +384,7 @@ export default function GeoRadar() {
|
||||
{/* Sort + date range row */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-slate-500">Période :</span>
|
||||
<span className="text-xs text-slate-500">Period:</span>
|
||||
<div className="flex gap-0.5 bg-dark-700 p-0.5 rounded">
|
||||
{(['today', '7d', '30d', 'all'] as DateRange[]).map(r => (
|
||||
<button key={r} onClick={() => setDateRange(r)}
|
||||
@@ -398,7 +398,7 @@ export default function GeoRadar() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-slate-500">Tri :</span>
|
||||
<span className="text-xs text-slate-500">Sort:</span>
|
||||
<div className="flex gap-0.5 bg-dark-700 p-0.5 rounded">
|
||||
{(['impact_desc', 'impact_asc', 'date_desc', 'date_asc'] as SortMode[]).map(s => (
|
||||
<button key={s} onClick={() => setSortMode(s)}
|
||||
@@ -423,7 +423,7 @@ export default function GeoRadar() {
|
||||
{filtered.map((n, i) => <NewsCard key={n.id || i} news={n} />)}
|
||||
{filtered.length === 0 && (
|
||||
<div className="card col-span-2 text-center py-8 text-slate-500">
|
||||
Démarrer le backend pour charger les actualités
|
||||
Start the backend to load news
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -437,11 +437,11 @@ export default function GeoRadar() {
|
||||
<div className="flex items-center gap-3 text-xs text-slate-500 bg-dark-700/40 rounded px-3 py-2">
|
||||
<Search className="w-3.5 h-3.5 shrink-0 text-blue-400" />
|
||||
<span>
|
||||
Correspondance entre les mots-clés de chaque pattern et les news des {days} derniers jours.
|
||||
Ce signal sert à enrichir le contexte donné à l'IA lors du scoring.
|
||||
Matching each pattern's keywords against news from the last {days} days.
|
||||
This signal enriches the context sent to the AI during scoring.
|
||||
{patternsWithNews > 0 && (
|
||||
<span className="text-emerald-400 ml-1">
|
||||
{patternsWithNews} patterns ont des news correspondantes.
|
||||
{patternsWithNews} patterns have matching news.
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
@@ -458,7 +458,7 @@ export default function GeoRadar() {
|
||||
))}
|
||||
{(!patternRelevance || patternRelevance.length === 0) && (
|
||||
<div className="card text-center py-8 text-slate-500">
|
||||
Démarrer le backend pour calculer la correspondance news-patterns
|
||||
Start the backend to compute news-pattern matching
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -9,14 +9,14 @@ import { fr } from 'date-fns/locale'
|
||||
|
||||
const SCENARIO_META: Record<string, { label: string; color: string; emoji: string }> = {
|
||||
goldilocks: { label: 'Goldilocks', color: '#22c55e', emoji: '🌟' },
|
||||
desinflation: { label: 'Désinflation', color: '#06b6d4', emoji: '❄️' },
|
||||
desinflation: { label: 'Disinflation', color: '#06b6d4', emoji: '❄️' },
|
||||
soft_landing: { label: 'Soft Landing', color: '#3b82f6', emoji: '🛬' },
|
||||
reflation: { label: 'Reflation', color: '#f97316', emoji: '🔥' },
|
||||
stagflation: { label: 'Stagflation', color: '#eab308', emoji: '⚡' },
|
||||
inflation_shock: { label: 'Inflation Shock', color: '#ef4444', emoji: '💥' },
|
||||
recession: { label: 'Récession', color: '#8b5cf6', emoji: '📉' },
|
||||
crise_liquidite: { label: 'Crise Liquidité', color: '#ec4899', emoji: '🚨' },
|
||||
incertain: { label: 'Incertain', color: '#64748b', emoji: '❓' },
|
||||
recession: { label: 'Recession', color: '#8b5cf6', emoji: '📉' },
|
||||
crise_liquidite: { label: 'Liquidity Crisis', color: '#ec4899', emoji: '🚨' },
|
||||
incertain: { label: 'Uncertain', color: '#64748b', emoji: '❓' },
|
||||
}
|
||||
|
||||
function ScenarioBadge({ dominant, size = 'sm' }: { dominant: string; size?: 'sm' | 'lg' }) {
|
||||
@@ -97,7 +97,7 @@ function KellyCell({ patternId, capital = 10000 }: { patternId: string; capital?
|
||||
<div className="flex flex-col items-end gap-0.5">
|
||||
<span className={clsx('text-[10px] font-bold font-mono', color)}>{pct?.toFixed(1)}%</span>
|
||||
<span className="text-[8px] text-slate-600">{eur != null ? `${eur}€` : ''}</span>
|
||||
{adjusted && <span className="text-[8px] text-orange-400" title={adjusted}>⚠ ajusté</span>}
|
||||
{adjusted && <span className="text-[8px] text-orange-400" title={adjusted}>⚠ adjusted</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -105,10 +105,10 @@ function KellyCell({ patternId, capital = 10000 }: { patternId: string; capital?
|
||||
// ── Close Trade Modal ─────────────────────────────────────────────────────────
|
||||
|
||||
const CLOSE_REASONS = [
|
||||
{ value: 'target', label: '🎯 Objectif atteint' },
|
||||
{ value: 'stop_loss', label: '🛑 Stop-loss déclenché' },
|
||||
{ value: 'signal_reversal', label: '🔄 Signal IA retourné' },
|
||||
{ value: 'manual', label: '✍️ Fermeture manuelle' },
|
||||
{ value: 'target', label: '🎯 Target reached' },
|
||||
{ value: 'stop_loss', label: '🛑 Stop-loss triggered' },
|
||||
{ value: 'signal_reversal', label: '🔄 AI signal reversed' },
|
||||
{ value: 'manual', label: '✍️ Manual close' },
|
||||
]
|
||||
|
||||
function CloseTradeModal({ trade, onClose }: { trade: any; onClose: () => void }) {
|
||||
@@ -142,10 +142,10 @@ function CloseTradeModal({ trade, onClose }: { trade: any; onClose: () => void }
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-bold text-white flex items-center gap-2">
|
||||
<Lock className="w-4 h-4 text-amber-400" /> Fermer le trade
|
||||
<Lock className="w-4 h-4 text-amber-400" /> Close trade
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-0.5">
|
||||
{trade.underlying} · {trade.strategy} · entrée {trade.entry_price?.toFixed(2) ?? '—'}
|
||||
{trade.underlying} · {trade.strategy} · entry {trade.entry_price?.toFixed(2) ?? '—'}
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={onClose} className="text-slate-600 hover:text-slate-400"><X className="w-4 h-4" /></button>
|
||||
@@ -156,7 +156,7 @@ function CloseTradeModal({ trade, onClose }: { trade: any; onClose: () => void }
|
||||
trade.alert_type === 'target_reached'
|
||||
? 'bg-emerald-900/30 border-emerald-700/40 text-emerald-300'
|
||||
: 'bg-red-900/30 border-red-700/40 text-red-300')}>
|
||||
{trade.alert_type === 'target_reached' ? '🎯 Objectif atteint !' : '🛑 Stop-loss atteint'}
|
||||
{trade.alert_type === 'target_reached' ? '🎯 Target reached!' : '🛑 Stop-loss hit'}
|
||||
{' — P&L actuel '}
|
||||
<span className="font-mono">{trade.pnl_pct >= 0 ? '+' : ''}{trade.pnl_pct?.toFixed(2)}%</span>
|
||||
</div>
|
||||
@@ -164,24 +164,24 @@ function CloseTradeModal({ trade, onClose }: { trade: any; onClose: () => void }
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs text-slate-500 mb-1">Prix de clôture</label>
|
||||
<label className="block text-xs text-slate-500 mb-1">Close price</label>
|
||||
<input
|
||||
type="number" step="0.01"
|
||||
value={closePrice}
|
||||
onChange={e => setClosePrice(e.target.value)}
|
||||
className="w-full bg-dark-700 border border-slate-600/50 rounded px-3 py-1.5 text-sm text-white font-mono"
|
||||
placeholder="Prix actuel du sous-jacent"
|
||||
placeholder="Current underlying price"
|
||||
/>
|
||||
{pnlPreview !== null && (
|
||||
<div className={clsx('text-xs mt-1 font-mono font-bold',
|
||||
pnlPreview >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
P&L réalisé : {pnlPreview >= 0 ? '+' : ''}{pnlPreview}%
|
||||
Realized P&L: {pnlPreview >= 0 ? '+' : ''}{pnlPreview}%
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs text-slate-500 mb-1">Motif de fermeture</label>
|
||||
<label className="block text-xs text-slate-500 mb-1">Close reason</label>
|
||||
<select
|
||||
value={reason}
|
||||
onChange={e => setReason(e.target.value)}
|
||||
@@ -194,24 +194,24 @@ function CloseTradeModal({ trade, onClose }: { trade: any; onClose: () => void }
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs text-slate-500 mb-1">Note (optionnel)</label>
|
||||
<label className="block text-xs text-slate-500 mb-1">Note (optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={note}
|
||||
onChange={e => setNote(e.target.value)}
|
||||
className="w-full bg-dark-700 border border-slate-600/50 rounded px-3 py-1.5 text-sm text-white"
|
||||
placeholder="Contexte, raison…"
|
||||
placeholder="Context, reason…"
|
||||
maxLength={200}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isError && <div className="text-xs text-red-400">Erreur lors de la fermeture du trade.</div>}
|
||||
{isError && <div className="text-xs text-red-400">Error closing trade.</div>}
|
||||
|
||||
<div className="flex gap-3 pt-1">
|
||||
<button onClick={onClose}
|
||||
className="flex-1 px-4 py-2 rounded border border-slate-700/50 text-slate-400 hover:text-slate-200 text-sm">
|
||||
Annuler
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
@@ -219,7 +219,7 @@ function CloseTradeModal({ trade, onClose }: { trade: any; onClose: () => void }
|
||||
className="flex-1 px-4 py-2 rounded bg-amber-600 hover:bg-amber-500 disabled:opacity-50 text-white text-sm font-semibold flex items-center justify-center gap-2"
|
||||
>
|
||||
<Lock className="w-3.5 h-3.5" />
|
||||
{isPending ? 'Fermeture…' : 'Confirmer la fermeture'}
|
||||
{isPending ? 'Closing…' : 'Confirm close'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -230,9 +230,9 @@ function CloseTradeModal({ trade, onClose }: { trade: any; onClose: () => void }
|
||||
// ── Shared filter bar ────────────────────────────────────────────────────────
|
||||
|
||||
const DIRECTIONS = [
|
||||
{ key: 'all', label: 'Tous' },
|
||||
{ key: 'bullish', label: '🐂 Haussier' },
|
||||
{ key: 'bearish', label: '🐻 Baissier' },
|
||||
{ key: 'all', label: 'All' },
|
||||
{ key: 'bullish', label: '🐂 Bullish' },
|
||||
{ key: 'bearish', label: '🐻 Bearish' },
|
||||
]
|
||||
|
||||
function _isBearishStr(strategy: string) {
|
||||
@@ -314,7 +314,7 @@ function FilterBar({ search, onSearch, assetClass, onAssetClass, direction, onDi
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={e => onSearch(e.target.value)}
|
||||
placeholder="Ticker, stratégie…"
|
||||
placeholder="Ticker, strategy…"
|
||||
className="pl-6 pr-2 py-1 bg-dark-700 border border-slate-700/50 rounded text-xs text-slate-300 placeholder-slate-600 w-36 focus:outline-none focus:border-slate-500"
|
||||
/>
|
||||
{search && (
|
||||
@@ -353,9 +353,9 @@ function FilterBar({ search, onSearch, assetClass, onAssetClass, direction, onDi
|
||||
{onPnlFilter && (
|
||||
<div className="flex items-center gap-0.5 bg-dark-700 rounded p-0.5">
|
||||
{[
|
||||
{ key: 'all', label: 'Tous' },
|
||||
{ key: 'winners', label: '✓ Gagnants' },
|
||||
{ key: 'losers', label: '✗ Perdants' },
|
||||
{ key: 'all', label: 'All' },
|
||||
{ key: 'winners', label: '✓ Winners' },
|
||||
{ key: 'losers', label: '✗ Losers' },
|
||||
].map(p => (
|
||||
<button key={p.key} onClick={() => onPnlFilter(p.key)}
|
||||
className={clsx('px-2 py-1 rounded text-xs transition-colors', {
|
||||
@@ -398,10 +398,10 @@ function ClosedTradesSection({ days }: { days: number }) {
|
||||
})
|
||||
|
||||
const REASON_META: Record<string, { label: string; color: string }> = {
|
||||
target: { label: '🎯 Objectif', color: 'text-emerald-400' },
|
||||
target: { label: '🎯 Target', color: 'text-emerald-400' },
|
||||
stop_loss: { label: '🛑 Stop-loss', color: 'text-red-400' },
|
||||
signal_reversal: { label: '🔄 Signal IA', color: 'text-blue-400' },
|
||||
manual: { label: '✍️ Manuel', color: 'text-slate-400' },
|
||||
signal_reversal: { label: '🔄 AI Signal', color: 'text-blue-400' },
|
||||
manual: { label: '✍️ Manual', color: 'text-slate-400' },
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -410,10 +410,10 @@ function ClosedTradesSection({ days }: { days: number }) {
|
||||
{allTrades.length > 0 && (
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
{[
|
||||
{ label: 'Trades fermés', value: stats.total ?? 0, sub: '', color: 'text-white' },
|
||||
{ label: 'Taux de succès', value: stats.win_rate != null ? `${stats.win_rate}%` : '—', sub: '', color: stats.win_rate >= 50 ? 'text-emerald-400' : 'text-red-400' },
|
||||
{ label: 'P&L moyen', value: stats.avg_pnl != null ? `${stats.avg_pnl >= 0 ? '+' : ''}${stats.avg_pnl?.toFixed(1)}%` : '—', sub: '', color: (stats.avg_pnl ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400' },
|
||||
{ label: 'P&L total', value: stats.total_pnl != null ? `${stats.total_pnl >= 0 ? '+' : ''}${stats.total_pnl?.toFixed(1)}%` : '—', sub: `meilleur ${stats.best?.toFixed(1) ?? '—'}%`, color: (stats.total_pnl ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400' },
|
||||
{ label: 'Closed trades', value: stats.total ?? 0, sub: '', color: 'text-white' },
|
||||
{ label: 'Win rate', value: stats.win_rate != null ? `${stats.win_rate}%` : '—', sub: '', color: stats.win_rate >= 50 ? 'text-emerald-400' : 'text-red-400' },
|
||||
{ label: 'Avg P&L', value: stats.avg_pnl != null ? `${stats.avg_pnl >= 0 ? '+' : ''}${stats.avg_pnl?.toFixed(1)}%` : '—', sub: '', color: (stats.avg_pnl ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400' },
|
||||
{ label: 'Total P&L', value: stats.total_pnl != null ? `${stats.total_pnl >= 0 ? '+' : ''}${stats.total_pnl?.toFixed(1)}%` : '—', sub: `best ${stats.best?.toFixed(1) ?? '—'}%`, color: (stats.total_pnl ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400' },
|
||||
].map(s => (
|
||||
<div key={s.label} className="card text-center py-3">
|
||||
<div className={clsx('text-lg font-bold font-mono', s.color)}>{s.value}</div>
|
||||
@@ -426,7 +426,7 @@ function ClosedTradesSection({ days }: { days: number }) {
|
||||
|
||||
<div className="flex items-center justify-between flex-wrap gap-2">
|
||||
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide">
|
||||
{trades.length}/{allTrades.length} trades clôturés · {days} derniers jours
|
||||
{trades.length}/{allTrades.length} closed trades · last {days} days
|
||||
</div>
|
||||
<button onClick={() => refetch()} disabled={isFetching}
|
||||
className="text-slate-600 hover:text-slate-400 disabled:opacity-40">
|
||||
@@ -446,11 +446,11 @@ function ClosedTradesSection({ days }: { days: number }) {
|
||||
) : allTrades.length === 0 ? (
|
||||
<div className="card text-center py-12 text-slate-600 text-sm">
|
||||
<Lock className="w-8 h-8 mx-auto mb-2 opacity-20" />
|
||||
Aucun trade clôturé dans les {days} derniers jours
|
||||
No closed trades in the last {days} days
|
||||
</div>
|
||||
) : trades.length === 0 ? (
|
||||
<div className="card text-center py-8 text-slate-600 text-sm">
|
||||
Aucun résultat pour ces filtres
|
||||
No results for these filters
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto rounded-lg border border-slate-700/40">
|
||||
@@ -458,15 +458,15 @@ function ClosedTradesSection({ days }: { days: number }) {
|
||||
<thead>
|
||||
<tr className="text-slate-600 border-b border-slate-700/40">
|
||||
<th className="text-left px-3 py-2 font-medium">Pattern</th>
|
||||
<th className="text-left px-3 py-2 font-medium">Stratégie</th>
|
||||
<th className="text-left px-3 py-2 font-medium">Strategy</th>
|
||||
<th className="text-left px-3 py-2 font-medium">Ticker</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Entrée</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Sortie</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Date entrée</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Date sortie</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Durée</th>
|
||||
<th className="text-right px-3 py-2 font-medium">P&L réalisé</th>
|
||||
<th className="text-left px-3 py-2 font-medium">Motif</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Entry</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Exit</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Entry date</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Close date</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Duration</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Realized P&L</th>
|
||||
<th className="text-left px-3 py-2 font-medium">Reason</th>
|
||||
<th className="text-left px-3 py-2 font-medium">Note</th>
|
||||
<th className="px-2 py-2 w-8"></th>
|
||||
</tr>
|
||||
@@ -523,7 +523,7 @@ function ClosedTradesSection({ days }: { days: number }) {
|
||||
disabled={deleting}
|
||||
className="text-[10px] font-semibold text-red-400 hover:text-red-300 bg-red-900/30 border border-red-700/40 rounded px-1.5 py-0.5 disabled:opacity-40"
|
||||
>
|
||||
{deleting ? '…' : 'Supprimer'}
|
||||
{deleting ? '…' : 'Delete'}
|
||||
</button>
|
||||
<button onClick={() => setConfirmDeleteId(null)} className="text-slate-600 hover:text-slate-400">
|
||||
<X className="w-3 h-3" />
|
||||
@@ -533,7 +533,7 @@ function ClosedTradesSection({ days }: { days: number }) {
|
||||
<button
|
||||
onClick={() => setConfirmDeleteId(t.id)}
|
||||
className="p-1 rounded text-slate-700 hover:text-red-400 hover:bg-red-900/20 transition-colors"
|
||||
title="Supprimer ce trade"
|
||||
title="Delete this trade"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
@@ -618,27 +618,27 @@ function IBKRTicket({ strategy, underlying, strikeGuidance, expiryDays, entryPri
|
||||
return (
|
||||
<div className="bg-blue-950/20 border border-blue-700/30 rounded p-3 space-y-2.5">
|
||||
<div className="text-[10px] font-bold text-blue-300 uppercase tracking-wide flex items-center gap-1.5">
|
||||
<Terminal className="w-3 h-3" /> Ticket IBKR
|
||||
{!entryPrice && <span className="ml-auto text-slate-600 font-normal normal-case">Prix non disponible — vérifier dans IBKR</span>}
|
||||
<Terminal className="w-3 h-3" /> IBKR Ticket
|
||||
{!entryPrice && <span className="ml-auto text-slate-600 font-normal normal-case">Price unavailable — check in IBKR</span>}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-3 text-[10px]">
|
||||
<div>
|
||||
<div className="text-slate-600 mb-0.5">Sous-jacent</div>
|
||||
<div className="text-slate-600 mb-0.5">Underlying</div>
|
||||
<div className="text-white font-mono font-bold text-sm">{underlying}</div>
|
||||
<div className="text-slate-600 text-[9px]">Security Type: OPT</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-slate-600 mb-0.5">Stratégie</div>
|
||||
<div className="text-slate-600 mb-0.5">Strategy</div>
|
||||
<div className="text-slate-200 font-semibold">{strategy}</div>
|
||||
{entryPrice && <div className="text-slate-600 text-[9px]">Sous-jacent: ${entryPrice.toFixed(2)}</div>}
|
||||
{entryPrice && <div className="text-slate-600 text-[9px]">Underlying: ${entryPrice.toFixed(2)}</div>}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-slate-600 mb-0.5">Expiration</div>
|
||||
<div className="text-slate-600 mb-0.5">Expiry</div>
|
||||
<div className="text-slate-200 font-mono">
|
||||
{expiryDate ? format(expiryDate, 'd MMM yyyy', { locale: fr }) : expiryDays ? `~${expiryDays}j` : '—'}
|
||||
{expiryDate ? format(expiryDate, 'd MMM yyyy', { locale: fr }) : expiryDays ? `~${expiryDays}d` : '—'}
|
||||
</div>
|
||||
<div className="text-slate-600 text-[9px]">{expiryDays ? `${expiryDays}j DTE` : ''}</div>
|
||||
<div className="text-slate-600 text-[9px]">{expiryDays ? `${expiryDays}d DTE` : ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -646,7 +646,7 @@ function IBKRTicket({ strategy, underlying, strikeGuidance, expiryDays, entryPri
|
||||
{legs.map((leg, i) => (
|
||||
<div key={i} className="flex items-center gap-2 bg-slate-900/70 rounded px-2.5 py-1.5 font-mono text-[10px]">
|
||||
<span className={clsx('w-8 font-bold', leg.action === 'BUY' ? 'text-emerald-400' : 'text-red-400')}>{leg.action}</span>
|
||||
<span className="text-slate-400">1 contrat</span>
|
||||
<span className="text-slate-400">1 contract</span>
|
||||
<span className={clsx('font-bold w-10 text-center', leg.type === 'CALL' ? 'text-blue-300' : leg.type === 'PUT' ? 'text-orange-300' : 'text-slate-300')}>{leg.type}</span>
|
||||
<span className="text-slate-600">Strike</span>
|
||||
<span className="text-yellow-300 font-bold">{fmtStrike(leg.strike)}</span>
|
||||
@@ -659,17 +659,17 @@ function IBKRTicket({ strategy, underlying, strikeGuidance, expiryDays, entryPri
|
||||
|
||||
<div className="grid grid-cols-3 gap-3 text-[10px]">
|
||||
<div>
|
||||
<div className="text-slate-600 mb-0.5">Type d'ordre</div>
|
||||
<div className="text-slate-600 mb-0.5">Order type</div>
|
||||
<div className="text-slate-300 font-semibold">LIMIT</div>
|
||||
<div className="text-slate-600 text-[9px]">Prix = prime dans IBKR</div>
|
||||
<div className="text-slate-600 text-[9px]">Price = premium in IBKR</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-slate-600 mb-0.5">Budget max</div>
|
||||
<div className="text-slate-600 mb-0.5">Max budget</div>
|
||||
<div className="text-slate-300 font-mono">{maxLoss ? `${Math.abs(maxLoss).toFixed(0)}€` : '~1 000€'}</div>
|
||||
<div className="text-slate-600 text-[9px]">Perte max estimée</div>
|
||||
<div className="text-slate-600 text-[9px]">Estimated max loss</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-slate-600 mb-0.5">Cible</div>
|
||||
<div className="text-slate-600 mb-0.5">Target</div>
|
||||
<div className="text-emerald-400 font-mono">{targetGain ? `+${targetGain.toFixed(0)}€` : '—'}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -685,7 +685,7 @@ function MacroHistorySection({ days }: { days: number }) {
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide">
|
||||
{history.length} snapshots · {days} derniers jours
|
||||
{history.length} snapshots · last {days} days
|
||||
</div>
|
||||
<button onClick={() => refetch()} disabled={isFetching}
|
||||
className="text-slate-600 hover:text-slate-400 disabled:opacity-40">
|
||||
@@ -698,7 +698,7 @@ function MacroHistorySection({ days }: { days: number }) {
|
||||
) : history.length === 0 ? (
|
||||
<div className="card text-center py-10 text-slate-600 text-sm">
|
||||
<Activity className="w-8 h-8 mx-auto mb-2 opacity-20" />
|
||||
Aucun snapshot — cliquer "Ré-analyser" dans Régime Macro pour commencer à logger
|
||||
No snapshots — click "Re-analyze" in Macro Regime to start logging
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
@@ -714,7 +714,7 @@ function MacroHistorySection({ days }: { days: number }) {
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
{isTransition && (
|
||||
<span className="text-[10px] bg-amber-900/30 text-amber-400 border border-amber-700/30 rounded px-1.5 py-0.5 shrink-0">
|
||||
↕ Transition
|
||||
↕ Transition (regime change)
|
||||
</span>
|
||||
)}
|
||||
<ScenarioBadge dominant={entry.dominant} size="lg" />
|
||||
@@ -800,7 +800,7 @@ function PostmortemPanel({ tradeId, onClose }: { tradeId: number; onClose: () =>
|
||||
{/* Score history sparkline */}
|
||||
{score_history?.length > 0 && (
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-slate-600 shrink-0">Historique :</span>
|
||||
<span className="text-slate-600 shrink-0">History:</span>
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
{[...score_history].reverse().map((h: any, i: number) => (
|
||||
<div key={i} className="flex flex-col items-center gap-0.5">
|
||||
@@ -817,7 +817,7 @@ function PostmortemPanel({ tradeId, onClose }: { tradeId: number; onClose: () =>
|
||||
</div>
|
||||
{score_history.length > 0 && (
|
||||
<span className="text-slate-600 ml-auto shrink-0">
|
||||
{score_history[0]?.macro_dominant ?? ''} — géo {score_history[0]?.geo_score ?? '?'}
|
||||
{score_history[0]?.macro_dominant ?? ''} — geo {score_history[0]?.geo_score ?? '?'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -830,7 +830,7 @@ function PostmortemPanel({ tradeId, onClose }: { tradeId: number; onClose: () =>
|
||||
onClick={() => setShowScoring(v => !v)}
|
||||
>
|
||||
{showScoring ? <ChevronUp className="w-3.5 h-3.5" /> : <ChevronDown className="w-3.5 h-3.5" />}
|
||||
Pourquoi noté {scOut.score ?? '?'}/100
|
||||
Why scored {scOut.score ?? '?'}/100
|
||||
{scOut.key_catalyst && <span className="text-slate-600 font-normal ml-1">· {scOut.key_catalyst}</span>}
|
||||
</button>
|
||||
{showScoring && (
|
||||
@@ -838,13 +838,13 @@ function PostmortemPanel({ tradeId, onClose }: { tradeId: number; onClose: () =>
|
||||
{/* Regime / bias */}
|
||||
<div className="flex flex-wrap gap-3 text-[11px] text-slate-500">
|
||||
{sc?.macro_dominant && (
|
||||
<span>Régime : <ScenarioBadge dominant={sc.macro_dominant} /></span>
|
||||
<span>Regime: <ScenarioBadge dominant={sc.macro_dominant} /></span>
|
||||
)}
|
||||
{scCtx.asset_bias && (
|
||||
<span>Biais asset : <span className="text-slate-300">{scCtx.asset_bias}</span></span>
|
||||
<span>Asset bias: <span className="text-slate-300">{scCtx.asset_bias}</span></span>
|
||||
)}
|
||||
{sc?.geo_score != null && (
|
||||
<span>Géo : <span className="font-mono text-slate-300">{sc.geo_score}/100</span></span>
|
||||
<span>Geo: <span className="font-mono text-slate-300">{sc.geo_score}/100</span></span>
|
||||
)}
|
||||
</div>
|
||||
{/* Buckets */}
|
||||
@@ -885,7 +885,7 @@ function PostmortemPanel({ tradeId, onClose }: { tradeId: number; onClose: () =>
|
||||
onClick={() => setShowSuggestion(v => !v)}
|
||||
>
|
||||
{showSuggestion ? <ChevronUp className="w-3.5 h-3.5" /> : <ChevronDown className="w-3.5 h-3.5" />}
|
||||
Pourquoi ce pattern a été créé
|
||||
Why this pattern was created
|
||||
</button>
|
||||
{showSuggestion && (
|
||||
<div className="pl-5 space-y-1.5 text-[11px] text-slate-400">
|
||||
@@ -893,8 +893,8 @@ function PostmortemPanel({ tradeId, onClose }: { tradeId: number; onClose: () =>
|
||||
{sgOut.description && <p className="text-slate-500">{sgOut.description}</p>}
|
||||
{sg.macro_dominant && (
|
||||
<div className="flex gap-3 mt-1">
|
||||
<span>Créé sous : <ScenarioBadge dominant={sg.macro_dominant} /></span>
|
||||
{sg.geo_score != null && <span>Géo : <span className="font-mono text-slate-300">{sg.geo_score}/100</span></span>}
|
||||
<span>Created under: <ScenarioBadge dominant={sg.macro_dominant} /></span>
|
||||
{sg.geo_score != null && <span>Geo: <span className="font-mono text-slate-300">{sg.geo_score}/100</span></span>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -911,22 +911,22 @@ function PostmortemPanel({ tradeId, onClose }: { tradeId: number; onClose: () =>
|
||||
className="flex items-center gap-2 px-3 py-1.5 rounded bg-blue-900/30 border border-blue-700/40 text-blue-300 hover:bg-blue-900/50 disabled:opacity-50 text-xs font-medium"
|
||||
>
|
||||
<Brain className={clsx('w-3.5 h-3.5', analyzing && 'animate-pulse')} />
|
||||
{analyzing ? 'Analyse GPT-4o en cours…' : 'Analyser avec GPT-4o'}
|
||||
{analyzing ? 'GPT-4o analysis running…' : 'Analyze with GPT-4o'}
|
||||
</button>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 text-blue-400 font-semibold">
|
||||
<Brain className="w-3.5 h-3.5" />
|
||||
Analyse GPT-4o
|
||||
GPT-4o Analysis
|
||||
</div>
|
||||
{[
|
||||
{ key: 'diagnostic', label: 'Diagnostic', color: 'text-slate-200' },
|
||||
{ key: 'what_worked', label: 'Ce qui a marché', color: 'text-emerald-400' },
|
||||
{ key: 'what_missed', label: 'Ce qui a manqué', color: 'text-red-400' },
|
||||
{ key: 'regime_alignment', label: 'Alignement régime', color: 'text-yellow-400' },
|
||||
{ key: 'what_worked', label: 'What worked', color: 'text-emerald-400' },
|
||||
{ key: 'what_missed', label: 'What was missed', color: 'text-red-400' },
|
||||
{ key: 'regime_alignment', label: 'Regime alignment', color: 'text-yellow-400' },
|
||||
{ key: 'contra_assessment', label: 'Contra-signals', color: 'text-orange-400' },
|
||||
{ key: 'lesson', label: 'Leçon', color: 'text-blue-300' },
|
||||
{ key: 'next_cycle', label: 'Prochain cycle', color: 'text-purple-400' },
|
||||
{ key: 'lesson', label: 'Lesson', color: 'text-blue-300' },
|
||||
{ key: 'next_cycle', label: 'Next cycle', color: 'text-purple-400' },
|
||||
].map(({ key, label, color }) =>
|
||||
analysis[key] ? (
|
||||
<div key={key}>
|
||||
@@ -974,10 +974,10 @@ function TradeMtmSection({ days }: { days: number }) {
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between gap-4 flex-wrap">
|
||||
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide">
|
||||
{trades.length}/{allTrades.length} trades · {withPnl.length} pricés
|
||||
{trades.length}/{allTrades.length} trades · {withPnl.length} priced
|
||||
{avgPnl != null && (
|
||||
<span className={clsx('ml-2 font-bold', avgPnl >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
· moy {avgPnl >= 0 ? '+' : ''}{avgPnl.toFixed(1)}%
|
||||
· avg {avgPnl >= 0 ? '+' : ''}{avgPnl.toFixed(1)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -1015,8 +1015,8 @@ function TradeMtmSection({ days }: { days: number }) {
|
||||
<div className="card text-center py-10 text-slate-600 text-sm">
|
||||
<TrendingUp className="w-8 h-8 mx-auto mb-2 opacity-20" />
|
||||
{allTrades.length === 0
|
||||
? 'Aucun trade ouvert — scorer des patterns pour commencer le suivi'
|
||||
: 'Aucun résultat pour ces filtres'}
|
||||
? 'No open trades — score patterns to start tracking'
|
||||
: 'No results for these filters'}
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto rounded-lg border border-slate-700/40">
|
||||
@@ -1024,20 +1024,20 @@ function TradeMtmSection({ days }: { days: number }) {
|
||||
<thead>
|
||||
<tr className="text-slate-600 border-b border-slate-700/40">
|
||||
<th className="text-left px-3 py-2 font-medium">Pattern</th>
|
||||
<th className="text-left px-3 py-2 font-medium">Profil</th>
|
||||
<th className="text-left px-3 py-2 font-medium">Stratégie</th>
|
||||
<th className="text-left px-3 py-2 font-medium">Profile</th>
|
||||
<th className="text-left px-3 py-2 font-medium">Strategy</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Strike</th>
|
||||
<th className="text-right px-3 py-2 font-medium">DTE</th>
|
||||
<th className="text-left px-3 py-2 font-medium">Ticker</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Score</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Trade Score</th>
|
||||
<th className="text-right px-3 py-2 font-medium">EV nette</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Gain prévu</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Net EV</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Expected move</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Date</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Prix entrée</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Prix actuel</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Cible/Stop</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Maturité</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Entry price</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Current price</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Target/Stop</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Maturity</th>
|
||||
<th className="text-right px-3 py-2 font-medium">IV Rank</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Kelly</th>
|
||||
<th className="text-right px-3 py-2 font-medium">P&L</th>
|
||||
@@ -1074,7 +1074,7 @@ function TradeMtmSection({ days }: { days: number }) {
|
||||
{t.strike_guidance ?? '—'}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right font-mono text-[11px] text-slate-400">
|
||||
{t.expiry_days_at_entry != null ? `${t.expiry_days_at_entry}j` : t.horizon_days != null ? `${t.horizon_days}j` : '—'}
|
||||
{t.expiry_days_at_entry != null ? `${t.expiry_days_at_entry}d` : t.horizon_days != null ? `${t.horizon_days}d` : '—'}
|
||||
</td>
|
||||
<td className="px-3 py-2 font-mono">
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -1082,7 +1082,7 @@ function TradeMtmSection({ days }: { days: number }) {
|
||||
{t.price_warning && (
|
||||
<span
|
||||
className="text-[8px] font-bold bg-amber-900/40 border border-amber-600/40 text-amber-400 rounded px-1 py-0.5 leading-none shrink-0"
|
||||
title={t.price_warning === 'no_price_data' ? 'Aucune donnée prix — monitoring impossible' : t.price_warning === 'no_entry_price' ? 'Prix d\'entrée manquant' : 'Prix live indisponible'}
|
||||
title={t.price_warning === 'no_price_data' ? 'No price data — monitoring unavailable' : t.price_warning === 'no_entry_price' ? 'Missing entry price' : 'Live price unavailable'}
|
||||
>
|
||||
⚠ {t.price_warning === 'no_price_data' ? 'NO DATA' : t.price_warning === 'no_entry_price' ? 'NO ENTRY' : 'NO LIVE'}
|
||||
</span>
|
||||
@@ -1167,7 +1167,7 @@ function TradeMtmSection({ days }: { days: number }) {
|
||||
{t.maturity.emoji} {t.maturity.label}
|
||||
</span>
|
||||
<span className="text-[9px] text-slate-600 font-mono">
|
||||
{t.days_held ?? 0}j / {t.horizon_days ?? 90}j ({t.maturity.ratio_pct}%)
|
||||
{t.days_held ?? 0}d / {t.horizon_days ?? 90}d ({t.maturity.ratio_pct}%)
|
||||
</span>
|
||||
<div className="w-16 h-1 bg-slate-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
@@ -1183,7 +1183,7 @@ function TradeMtmSection({ days }: { days: number }) {
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-slate-700 text-[11px]">
|
||||
{t.days_held != null ? `${t.days_held}j` : '—'}
|
||||
{t.days_held != null ? `${t.days_held}d` : '—'}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
@@ -1219,7 +1219,7 @@ function TradeMtmSection({ days }: { days: number }) {
|
||||
? 'text-amber-400 bg-amber-900/30 hover:bg-amber-900/50'
|
||||
: 'text-slate-700 hover:text-amber-400 hover:bg-amber-900/20'
|
||||
)}
|
||||
title="Fermer le trade"
|
||||
title="Close trade"
|
||||
>
|
||||
<Lock className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
@@ -1265,7 +1265,7 @@ function GeoHistorySection({ days }: { days: number }) {
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide">
|
||||
{history.length} alertes · {days} derniers jours
|
||||
{history.length} alerts · last {days} days
|
||||
</div>
|
||||
<button onClick={() => refetch()} disabled={isFetching}
|
||||
className="text-slate-600 hover:text-slate-400 disabled:opacity-40">
|
||||
@@ -1278,7 +1278,7 @@ function GeoHistorySection({ days }: { days: number }) {
|
||||
) : history.length === 0 ? (
|
||||
<div className="card text-center py-10 text-slate-600 text-sm">
|
||||
<AlertTriangle className="w-8 h-8 mx-auto mb-2 opacity-20" />
|
||||
Aucune alerte — scorer des patterns pour commencer le suivi
|
||||
No alerts — score patterns to start tracking
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
@@ -1348,7 +1348,7 @@ function CyclesSection() {
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide">
|
||||
{runs.length} cycles · {cs?.enabled ? `auto ${cs.interval_hours}h` : 'manuel uniquement'}
|
||||
{runs.length} cycles · {cs?.enabled ? `auto ${cs.interval_hours}h` : 'manual only'}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
@@ -1356,7 +1356,7 @@ function CyclesSection() {
|
||||
disabled={triggering}
|
||||
className="flex items-center gap-1 text-xs border border-blue-500/40 text-blue-400 hover:bg-blue-900/20 px-2.5 py-1 rounded disabled:opacity-40">
|
||||
<Zap className={clsx('w-3 h-3', triggering && 'animate-pulse')} />
|
||||
{triggering ? 'Lancement...' : 'Lancer un cycle'}
|
||||
{triggering ? 'Launching...' : 'Run a cycle'}
|
||||
</button>
|
||||
<button onClick={() => refetch()} disabled={isFetching}
|
||||
className="text-slate-600 hover:text-slate-400 disabled:opacity-40">
|
||||
@@ -1370,7 +1370,7 @@ function CyclesSection() {
|
||||
) : runs.length === 0 ? (
|
||||
<div className="card text-center py-10 text-slate-600 text-sm">
|
||||
<Zap className="w-8 h-8 mx-auto mb-2 opacity-20" />
|
||||
Aucun cycle — activer l'auto-cycle dans Configuration ou lancer manuellement
|
||||
No cycles — enable auto-cycle in Configuration or run manually
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
@@ -1392,16 +1392,16 @@ function CyclesSection() {
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs text-slate-300 font-semibold">{ts} UTC</span>
|
||||
<span className={clsx('badge text-[10px]', run.trigger === 'manual' ? 'badge-blue' : 'badge-purple')}>
|
||||
{run.trigger === 'manual' ? 'Manuel' : `Auto ${cs?.interval_hours ?? ''}h`}
|
||||
{run.trigger === 'manual' ? 'Manual' : `Auto ${cs?.interval_hours ?? ''}h`}
|
||||
</span>
|
||||
{run.dominant_regime && <ScenarioBadge dominant={run.dominant_regime} />}
|
||||
{duration != null && <span className="text-[10px] text-slate-600">{duration}s</span>}
|
||||
</div>
|
||||
<div className="flex gap-3 mt-1 text-[11px] text-slate-500 flex-wrap">
|
||||
<span>💡 {run.patterns_suggested} suggérés</span>
|
||||
<span>✚ {run.patterns_added} ajoutés</span>
|
||||
<span>🎯 {run.patterns_scored} scorés</span>
|
||||
{run.geo_score != null && <span>🌐 Géo {run.geo_score}/100</span>}
|
||||
<span>💡 {run.patterns_suggested} suggested</span>
|
||||
<span>✚ {run.patterns_added} added</span>
|
||||
<span>🎯 {run.patterns_scored} scored</span>
|
||||
{run.geo_score != null && <span>🌐 Geo {run.geo_score}/100</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1409,7 +1409,7 @@ function CyclesSection() {
|
||||
{commentary && (
|
||||
<div className="bg-blue-900/10 border border-blue-700/20 rounded p-3 space-y-2">
|
||||
<div className="flex items-center gap-1.5 text-[10px] text-blue-400 font-semibold uppercase tracking-wide">
|
||||
<Brain className="w-3 h-3" /> Commentaire IA
|
||||
<Brain className="w-3 h-3" /> AI Comment
|
||||
</div>
|
||||
<p className="text-xs text-slate-300 leading-relaxed">{commentary.commentary}</p>
|
||||
{commentary.key_risk && (
|
||||
@@ -1419,13 +1419,13 @@ function CyclesSection() {
|
||||
)}
|
||||
{commentary.top_pattern && (
|
||||
<div className="text-[11px] text-emerald-400/80">
|
||||
🎯 Pattern clé : <span className="font-semibold">{commentary.top_pattern}</span>
|
||||
🎯 Key pattern: <span className="font-semibold">{commentary.top_pattern}</span>
|
||||
</div>
|
||||
)}
|
||||
{commentary.lessons_from_report ? (
|
||||
<div className="flex items-center gap-1.5 text-[10px] text-purple-400/80 bg-purple-900/10 border border-purple-700/20 rounded px-2 py-1">
|
||||
<BookOpen className="w-3 h-3 shrink-0" />
|
||||
Leçons du rapport du {commentary.lessons_from_report} intégrées dans ce cycle
|
||||
Lessons from the {commentary.lessons_from_report} report integrated in this cycle
|
||||
{commentary.lessons_headline && (
|
||||
<span className="text-slate-600 ml-1">· {commentary.lessons_headline}</span>
|
||||
)}
|
||||
@@ -1433,14 +1433,14 @@ function CyclesSection() {
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5 text-[10px] text-slate-700 italic">
|
||||
<BookOpen className="w-3 h-3 shrink-0" />
|
||||
Aucun rapport de performance disponible — générer un rapport dans "Rapport IA"
|
||||
No performance report available — generate a report in "AI Report"
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!commentary && ok && (
|
||||
<div className="text-[11px] text-slate-700 italic">Aucun commentaire IA généré pour ce cycle</div>
|
||||
<div className="text-[11px] text-slate-700 italic">No AI comment generated for this cycle</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
@@ -1476,8 +1476,8 @@ function SimPortfolioRiskPanel() {
|
||||
return (
|
||||
<div className="card text-center py-8 text-slate-500">
|
||||
<PieChart className="w-8 h-8 mx-auto mb-2 opacity-20" />
|
||||
<div className="text-sm">Aucune position ouverte dans le portefeuille simulé</div>
|
||||
<div className="text-xs mt-1">Les trades logués apparaîtront ici après le prochain cycle</div>
|
||||
<div className="text-sm">No open positions in the simulated portfolio</div>
|
||||
<div className="text-xs mt-1">Logged trades will appear here after the next cycle</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1497,11 +1497,11 @@ function SimPortfolioRiskPanel() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<PieChart className="w-4 h-4 text-blue-400" />
|
||||
<span className="text-sm font-semibold text-white">Portefeuille Simulé — Analyse de Risque</span>
|
||||
<span className="text-xs text-slate-500">{risk.open_count} positions ouvertes</span>
|
||||
<span className="text-sm font-semibold text-white">Simulated Portfolio — Risk Analysis</span>
|
||||
<span className="text-xs text-slate-500">{risk.open_count} open positions</span>
|
||||
{dangers.length > 0 && (
|
||||
<span className="inline-flex items-center gap-1 text-[10px] font-bold bg-red-900/30 border border-red-600/40 text-red-400 rounded px-1.5 py-0.5">
|
||||
<ShieldAlert className="w-3 h-3" /> {dangers.length} CONFLIT{dangers.length > 1 ? 'S' : ''}
|
||||
<ShieldAlert className="w-3 h-3" /> {dangers.length} CONFLICT{dangers.length > 1 ? 'S' : ''}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -1512,7 +1512,7 @@ function SimPortfolioRiskPanel() {
|
||||
|
||||
{/* Asset class concentration bars */}
|
||||
<div className="card">
|
||||
<div className="text-xs text-slate-500 mb-3 font-semibold uppercase tracking-wide">Répartition par classe d'actif</div>
|
||||
<div className="text-xs text-slate-500 mb-3 font-semibold uppercase tracking-wide">Breakdown by asset class</div>
|
||||
<div className="space-y-2">
|
||||
{Object.entries(concentration)
|
||||
.sort(([, a]: any, [, b]: any) => b.pct - a.pct)
|
||||
@@ -1563,7 +1563,7 @@ function SimPortfolioRiskPanel() {
|
||||
<ShieldAlert className="w-4 h-4 text-red-400 shrink-0 mt-0.5" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs font-semibold text-red-300 mb-1">
|
||||
Conflit directionnel — {c.underlying}
|
||||
Directional conflict — {c.underlying}
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
{c.trades.map((t: any) => (
|
||||
@@ -1589,7 +1589,7 @@ function SimPortfolioRiskPanel() {
|
||||
{warnings.length > 0 && (
|
||||
<div className="card border-amber-700/30 bg-amber-900/10">
|
||||
<div className="text-xs font-semibold text-amber-300 mb-1.5 flex items-center gap-1">
|
||||
<AlertTriangle className="w-3.5 h-3.5" /> Alertes de concentration
|
||||
<AlertTriangle className="w-3.5 h-3.5" /> Concentration alerts
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{warnings.map((a: any, i: number) => (
|
||||
@@ -1609,7 +1609,7 @@ function SimPortfolioRiskPanel() {
|
||||
<div className="card border-blue-700/30 bg-blue-900/10">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-xs font-semibold text-blue-300 flex items-center gap-1.5">
|
||||
<Brain className="w-3.5 h-3.5" /> Recommandations IA — Moniteur de portefeuille
|
||||
<Brain className="w-3.5 h-3.5" /> AI Recommendations — Portfolio Monitor
|
||||
</div>
|
||||
{aiTs && (
|
||||
<span className="text-[10px] text-slate-600">{aiTs.slice(0, 16).replace('T', ' ')}</span>
|
||||
@@ -1650,7 +1650,7 @@ function SimPortfolioRiskPanel() {
|
||||
{alerts.length === 0 && (
|
||||
<div className="card border-emerald-700/30 bg-emerald-900/10 text-center py-3">
|
||||
<CheckCircle className="w-5 h-5 text-emerald-400 mx-auto mb-1" />
|
||||
<div className="text-xs text-emerald-300">Portefeuille équilibré — aucun conflit ni sur-concentration détectés</div>
|
||||
<div className="text-xs text-emerald-300">Balanced portfolio — no conflicts or over-concentration detected</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -1658,13 +1658,13 @@ function SimPortfolioRiskPanel() {
|
||||
}
|
||||
|
||||
const TABS = [
|
||||
{ key: 'cycles', label: 'Cycles IA', icon: Zap },
|
||||
{ key: 'macro', label: 'Régimes Macro', icon: Activity },
|
||||
{ key: 'ideas', label: 'Idées de trade', icon: Target },
|
||||
{ key: 'mtm', label: 'Ouverts', icon: TrendingUp },
|
||||
{ key: 'closed', label: 'Fermés', icon: Lock },
|
||||
{ key: 'geo', label: 'Alertes Géo', icon: AlertTriangle },
|
||||
{ key: 'skipped', label: 'Non loggés', icon: XCircle },
|
||||
{ key: 'cycles', label: 'AI Cycles', icon: Zap },
|
||||
{ key: 'macro', label: 'Macro Regimes', icon: Activity },
|
||||
{ key: 'ideas', label: 'Trade Ideas', icon: Target },
|
||||
{ key: 'mtm', label: 'Open', icon: TrendingUp },
|
||||
{ key: 'closed', label: 'Closed', icon: Lock },
|
||||
{ key: 'geo', label: 'Geo Alerts', icon: AlertTriangle },
|
||||
{ key: 'skipped', label: 'Not logged', icon: XCircle },
|
||||
] as const
|
||||
|
||||
function SkippedTradesSection({ days }: { days: number }) {
|
||||
@@ -1706,7 +1706,7 @@ function SkippedTradesSection({ days }: { days: number }) {
|
||||
return (
|
||||
<div className="card text-center py-8 text-slate-500 text-sm">
|
||||
<XCircle className="w-6 h-6 mx-auto mb-2 text-slate-600" />
|
||||
Aucune suggestion non loggée sur {days}j — tous les trades scorés passent au moins un profil de risque.
|
||||
No unlogged suggestions in the last {days}d — all scored trades match at least one risk profile.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1716,9 +1716,9 @@ function SkippedTradesSection({ days }: { days: number }) {
|
||||
<div className="card">
|
||||
<div className="section-title flex items-center gap-1 mb-3">
|
||||
<XCircle className="w-3 h-3 text-amber-400" />
|
||||
Trades suggérés non loggés — {trades.length}/{allTrades.length} sur {days}j
|
||||
Suggested trades not logged — {trades.length}/{allTrades.length} over {days}d
|
||||
<span className="ml-2 text-slate-600 font-normal text-xs">
|
||||
(score ou gain insuffisant pour tout profil actif)
|
||||
(score or gain insufficient for any active profile)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1743,7 +1743,7 @@ function SkippedTradesSection({ days }: { days: number }) {
|
||||
<button onClick={() => setReasonFilter('all')}
|
||||
className={clsx('px-2 py-1 rounded text-xs transition-colors',
|
||||
reasonFilter === 'all' ? 'bg-slate-600 text-white' : 'text-slate-400 hover:text-slate-200')}>
|
||||
Tous
|
||||
All
|
||||
</button>
|
||||
{allReasons.map(r => (
|
||||
<button key={r} onClick={() => setReasonFilter(r)}
|
||||
@@ -1757,7 +1757,7 @@ function SkippedTradesSection({ days }: { days: number }) {
|
||||
</div>
|
||||
|
||||
{trades.length === 0 ? (
|
||||
<div className="text-center py-6 text-slate-600 text-sm">Aucun résultat pour ces filtres</div>
|
||||
<div className="text-center py-6 text-slate-600 text-sm">No results for these filters</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto rounded-lg border border-slate-700/40">
|
||||
<table className="w-full text-xs">
|
||||
@@ -1765,11 +1765,11 @@ function SkippedTradesSection({ days }: { days: number }) {
|
||||
<tr className="border-b border-slate-700/60 text-slate-600 text-[10px] uppercase tracking-wide">
|
||||
<th className="pl-3 py-2 text-left">Pattern</th>
|
||||
<th className="px-2 py-2 text-left">Ticker</th>
|
||||
<th className="px-2 py-2 text-left">Stratégie</th>
|
||||
<th className="px-2 py-2 text-left">Strategy</th>
|
||||
<th className="px-2 py-2 text-right">Score</th>
|
||||
<th className="px-2 py-2 text-right">Gain attendu</th>
|
||||
<th className="px-2 py-2 text-left">Classe</th>
|
||||
<th className="pr-3 py-2 text-left">Raison du skip</th>
|
||||
<th className="px-2 py-2 text-right">Expected gain</th>
|
||||
<th className="px-2 py-2 text-left">Class</th>
|
||||
<th className="pr-3 py-2 text-left">Skip reason</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800/40">
|
||||
@@ -1827,7 +1827,7 @@ export default function JournalDeBord() {
|
||||
setResetting(true)
|
||||
try {
|
||||
await api.delete('/journal/reset')
|
||||
setResetMsg('Journal réinitialisé')
|
||||
setResetMsg('Journal reset')
|
||||
setConfirmReset(false)
|
||||
// Invalidate all journal queries
|
||||
await qc.invalidateQueries({ queryKey: ['journal-summary'] })
|
||||
@@ -1837,7 +1837,7 @@ export default function JournalDeBord() {
|
||||
await qc.invalidateQueries({ queryKey: ['cycle-history'] })
|
||||
setTimeout(() => setResetMsg(''), 3000)
|
||||
} catch {
|
||||
setResetMsg('Erreur lors du reset')
|
||||
setResetMsg('Reset error')
|
||||
setTimeout(() => setResetMsg(''), 3000)
|
||||
} finally {
|
||||
setResetting(false)
|
||||
@@ -1850,10 +1850,10 @@ export default function JournalDeBord() {
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-white flex items-center gap-2">
|
||||
<BookOpen className="w-5 h-5 text-blue-400" /> Journal de Bord
|
||||
<BookOpen className="w-5 h-5 text-blue-400" /> Trading Journal
|
||||
</h1>
|
||||
<p className="text-xs text-slate-500 mt-0.5">
|
||||
Historique des régimes macro · Mark-to-market des trades · Évolution du risque géopolitique
|
||||
Macro regime history · Trade mark-to-market · Geopolitical risk evolution
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1866,7 +1866,7 @@ export default function JournalDeBord() {
|
||||
</span>
|
||||
)}
|
||||
{confirmReset && !resetting && (
|
||||
<span className="text-xs text-amber-400">Confirmer ?</span>
|
||||
<span className="text-xs text-amber-400">Confirm?</span>
|
||||
)}
|
||||
<button
|
||||
onClick={confirmReset ? handleReset : () => setConfirmReset(true)}
|
||||
@@ -1879,7 +1879,7 @@ export default function JournalDeBord() {
|
||||
: 'border-slate-700/40 text-slate-600 hover:text-slate-400 hover:border-slate-600'
|
||||
)}>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
{resetting ? 'Réinit...' : confirmReset ? 'Effacer tout' : 'Reset journal'}
|
||||
{resetting ? 'Resetting...' : confirmReset ? 'Clear all' : 'Reset journal'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1891,7 +1891,7 @@ export default function JournalDeBord() {
|
||||
'bg-blue-600 text-white': days === d,
|
||||
'text-slate-400 hover:text-slate-200': days !== d,
|
||||
})}>
|
||||
{d}j
|
||||
{d}d
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -1903,14 +1903,14 @@ export default function JournalDeBord() {
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<div className="card text-center">
|
||||
<div className="text-2xl font-bold text-white">{s.macro_snapshots ?? 0}</div>
|
||||
<div className="text-xs text-slate-600 mt-0.5">snapshots macro</div>
|
||||
<div className="text-xs text-slate-600 mt-0.5">macro snapshots</div>
|
||||
{s.current_dominant && (
|
||||
<div className="mt-1"><ScenarioBadge dominant={s.current_dominant} /></div>
|
||||
)}
|
||||
</div>
|
||||
<div className="card text-center">
|
||||
<div className="text-2xl font-bold text-white">{s.regime_transitions?.length ?? 0}</div>
|
||||
<div className="text-xs text-slate-600 mt-0.5">transitions de régime</div>
|
||||
<div className="text-xs text-slate-600 mt-0.5">regime transitions</div>
|
||||
{s.regime_transitions?.length > 0 && (
|
||||
<div className="text-[10px] text-amber-400 mt-1">
|
||||
↕ {s.regime_transitions[0].from} → {s.regime_transitions[0].to}
|
||||
@@ -1922,23 +1922,23 @@ export default function JournalDeBord() {
|
||||
(s.avg_geo_score ?? 0) >= 70 ? 'text-red-400' : (s.avg_geo_score ?? 0) >= 40 ? 'text-orange-400' : 'text-emerald-400')}>
|
||||
{s.avg_geo_score != null ? s.avg_geo_score.toFixed(0) : '—'}
|
||||
</div>
|
||||
<div className="text-xs text-slate-600 mt-0.5">score géo moyen</div>
|
||||
<div className="text-xs text-slate-600 mt-0.5">avg geo score</div>
|
||||
{s.max_geo_score != null && (
|
||||
<div className="text-[10px] text-slate-600 mt-1">max {s.max_geo_score}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="card text-center">
|
||||
<div className="text-2xl font-bold text-white">{s.trade_entries_logged ?? 0}</div>
|
||||
<div className="text-xs text-slate-600 mt-0.5">trades logués</div>
|
||||
<div className="text-xs text-slate-600 mt-0.5">logged trades</div>
|
||||
</div>
|
||||
{riskAlertCount > 0 && (
|
||||
<div className="card text-center border-red-700/40 bg-red-900/10 col-span-full">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<ShieldAlert className="w-4 h-4 text-red-400" />
|
||||
<span className="text-sm font-semibold text-red-300">
|
||||
{riskAlertCount} conflit{riskAlertCount > 1 ? 's' : ''} directionnel{riskAlertCount > 1 ? 's' : ''} détecté{riskAlertCount > 1 ? 's' : ''}
|
||||
{riskAlertCount} directional conflict{riskAlertCount > 1 ? 's' : ''} detected
|
||||
</span>
|
||||
<span className="text-xs text-slate-500">— voir Risk Dashboard → Simulé</span>
|
||||
<span className="text-xs text-slate-500">— see Risk Dashboard → Simulated</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -18,12 +18,12 @@ const SCENARIO_ORDER = [
|
||||
] as const
|
||||
|
||||
const BLOC_INFO: Record<string, { label: string; emoji: string; description: string }> = {
|
||||
liquidite: { label: 'Liquidité mondiale', emoji: '💧', description: 'Carburant des marchés — DXY↓ = conditions mondiales détendues ; pente = signal récession/reprise' },
|
||||
credit: { label: 'Crédit & Stress', emoji: '⚡', description: 'Thermomètre systémique — HYG (HY, obligataires détectent avant les actions), LQD (IG), VIX' },
|
||||
energie: { label: 'Énergie', emoji: '⛽', description: 'Premier moteur d\'inflation — Brent↑↑ = stagflation/choc ; Brent↓ = désinflationniste' },
|
||||
metaux: { label: 'Métaux stratégiques', emoji: '🥇', description: 'Cuivre = "Dr Copper" (croissance mondiale) ; Or = refuge/taux réels ; ratio Or/Cu = biais craint vs expansion' },
|
||||
croissance: { label: 'Croissance mondiale', emoji: '📊', description: 'Cycle réel — S&P, Russell 2000 (breadth risk-on), XLI (proxy ISM mfg), Baltic Dry (secondaire)' },
|
||||
derive: { label: 'Indicateurs dérivés', emoji: '🔗', description: 'Pente 10Y–3M (récession) · Or/Cu (peur vs expansion) · S&P vs 200j · Russell vs S&P (breadth)' },
|
||||
liquidite: { label: 'Global Liquidity', emoji: '💧', description: 'Market fuel — DXY↓ = loose global conditions; yield curve = recession/recovery signal' },
|
||||
credit: { label: 'Credit & Stress', emoji: '⚡', description: 'Systemic thermometer — HYG (HY, bonds lead equities), LQD (IG), VIX' },
|
||||
energie: { label: 'Energy', emoji: '⛽', description: 'Primary inflation driver — Brent↑↑ = stagflation/shock; Brent↓ = disinflationary' },
|
||||
metaux: { label: 'Strategic Metals', emoji: '🥇', description: 'Copper = "Dr Copper" (global growth); Gold = haven/real rates; Gold/Cu ratio = fear vs expansion bias' },
|
||||
croissance: { label: 'Global Growth', emoji: '📊', description: 'Real cycle — S&P, Russell 2000 (breadth risk-on), XLI (ISM mfg proxy), Baltic Dry (secondary)' },
|
||||
derive: { label: 'Derived Indicators', emoji: '🔗', description: 'Slope 10Y–3M (recession) · Gold/Cu (fear vs expansion) · S&P vs 200d · Russell vs S&P (breadth)' },
|
||||
}
|
||||
|
||||
const ASSET_BIAS_COLORS: Record<string, string> = {
|
||||
@@ -31,12 +31,12 @@ const ASSET_BIAS_COLORS: Record<string, string> = {
|
||||
'bearish': '#f97316', 'bearish+': '#ef4444', 'defensive': '#f59e0b',
|
||||
}
|
||||
const ASSET_BIAS_LABELS: Record<string, string> = {
|
||||
'bullish+': '★★ Très favorable', 'bullish': '★ Favorable', 'neutral': '→ Neutre',
|
||||
'bearish': '✗ Défavorable', 'bearish+': '✗✗ Contre', 'defensive': '⚠ Défensif',
|
||||
'bullish+': '★★ Very Favorable', 'bullish': '★ Favorable', 'neutral': '→ Neutral',
|
||||
'bearish': '✗ Unfavorable', 'bearish+': '✗✗ Against', 'defensive': '⚠ Defensive',
|
||||
}
|
||||
const ASSET_CLASS_LABELS: Record<string, string> = {
|
||||
energy: 'Énergie ⛽', metals: 'Métaux 🥇', agriculture: 'Agriculture 🌾',
|
||||
indices: 'Indices 📊', equities: 'Actions 📈', forex: 'Forex 💱',
|
||||
energy: 'Energy ⛽', metals: 'Metals 🥇', agriculture: 'Agriculture 🌾',
|
||||
indices: 'Indices 📊', equities: 'Equities 📈', forex: 'Forex 💱',
|
||||
}
|
||||
|
||||
function GaugeRow({ g }: { g: MacroGauge }) {
|
||||
@@ -153,7 +153,7 @@ export default function MacroRegime() {
|
||||
setSavedNarration(entry)
|
||||
localStorage.setItem('geo-options:macro-narration', JSON.stringify(entry))
|
||||
} catch {
|
||||
const entry = { text: 'Erreur lors de la génération — vérifier la clé OpenAI.', at: new Date().toISOString() }
|
||||
const entry = { text: 'Error during generation — please check your OpenAI key.', at: new Date().toISOString() }
|
||||
setSavedNarration(entry)
|
||||
} finally {
|
||||
setLoadingNarration(false)
|
||||
@@ -166,10 +166,10 @@ export default function MacroRegime() {
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-white flex items-center gap-2">
|
||||
<Activity className="w-5 h-5 text-blue-400" /> Régime Macro
|
||||
<Activity className="w-5 h-5 text-blue-400" /> Macro Regime
|
||||
</h1>
|
||||
<p className="text-xs text-slate-500 mt-0.5">
|
||||
30 compteurs institutionnels · Détection de régime · Compatibilité scénarios
|
||||
30 institutional gauges · Regime detection · Scenario compatibility
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -184,14 +184,14 @@ export default function MacroRegime() {
|
||||
className="flex items-center gap-1.5 text-xs text-slate-400 hover:text-white border border-slate-700 rounded px-2.5 py-1.5 transition-colors hover:border-slate-500 disabled:opacity-40"
|
||||
>
|
||||
<RefreshCw className={clsx('w-3.5 h-3.5', isFetching && 'animate-spin')} />
|
||||
Actualiser
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="card flex items-center justify-center py-12 text-slate-600 text-sm">
|
||||
<RefreshCw className="w-4 h-4 animate-spin mr-2" /> Chargement des compteurs macro...
|
||||
<RefreshCw className="w-4 h-4 animate-spin mr-2" /> Loading macro gauges...
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -201,7 +201,7 @@ export default function MacroRegime() {
|
||||
style={{ borderColor: `${dominantMeta.color}44`, background: `${dominantMeta.color}0d` }}>
|
||||
<div className="text-4xl">{dominantMeta.emoji}</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-xs text-slate-500 uppercase tracking-widest mb-0.5">Scénario dominant</div>
|
||||
<div className="text-xs text-slate-500 uppercase tracking-widest mb-0.5">Dominant Scenario</div>
|
||||
<div className="text-xl font-bold" style={{ color: dominantMeta.color }}>{dominantMeta.label}</div>
|
||||
<div className="text-xs text-slate-500 mt-1 flex flex-wrap gap-1">
|
||||
{(reasons[dominant] ?? []).map((r, i) => (
|
||||
@@ -216,16 +216,16 @@ export default function MacroRegime() {
|
||||
<div className="text-3xl font-bold font-mono" style={{ color: dominantMeta.color }}>
|
||||
{scores[dominant]}%
|
||||
</div>
|
||||
<div className="text-xs text-slate-600">score de régime</div>
|
||||
<div className="text-xs text-slate-600">regime score</div>
|
||||
{regimeStability > 0 && (
|
||||
<div className="text-[10px] mt-1 px-1.5 py-0.5 rounded-full text-center"
|
||||
style={{ background: `${dominantMeta.color}22`, color: dominantMeta.color }}>
|
||||
stable {regimeStability}j
|
||||
stable {regimeStability}d
|
||||
</div>
|
||||
)}
|
||||
{historyDays > 0 && (
|
||||
<div className="text-[10px] text-slate-700 mt-0.5 text-center">
|
||||
{historyDays}j d'historique
|
||||
{historyDays}d history
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -236,7 +236,7 @@ export default function MacroRegime() {
|
||||
{/* Left: Gauge blocs */}
|
||||
<div className="col-span-2 space-y-3">
|
||||
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide">
|
||||
Compteurs par bloc ({Object.values(gauges).length} indicateurs)
|
||||
Gauges by bloc ({Object.values(gauges).length} indicators)
|
||||
</div>
|
||||
{['liquidite', 'credit', 'energie', 'metaux', 'croissance', 'derive'].map(bloc => (
|
||||
<BlocCard key={bloc} id={bloc} gauges={byBloc[bloc] ?? []} />
|
||||
@@ -248,7 +248,7 @@ export default function MacroRegime() {
|
||||
{/* Scenario ranking */}
|
||||
<div className="card">
|
||||
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-3">
|
||||
Classement des scénarios
|
||||
Scenario Ranking
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{ranked.map(([key, score]) => {
|
||||
@@ -291,7 +291,7 @@ export default function MacroRegime() {
|
||||
{dominant !== 'incertain' && Object.keys(dominantBias).length > 0 && (
|
||||
<div className="card">
|
||||
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-3">
|
||||
Classes d'actifs · {dominantMeta.emoji} {dominantMeta.label}
|
||||
Asset Classes · {dominantMeta.emoji} {dominantMeta.label}
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
{Object.entries(dominantBias).map(([cls, bias]) => {
|
||||
@@ -312,7 +312,7 @@ export default function MacroRegime() {
|
||||
<div className="card">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide flex items-center gap-1.5">
|
||||
<Brain className="w-3.5 h-3.5 text-blue-400" /> Analyse IA
|
||||
<Brain className="w-3.5 h-3.5 text-blue-400" /> AI Analysis
|
||||
</div>
|
||||
{savedNarration?.at && (
|
||||
<span className="text-[10px] text-slate-700">
|
||||
@@ -326,7 +326,7 @@ export default function MacroRegime() {
|
||||
<p className="text-xs text-slate-300 leading-relaxed mb-3">{savedNarration.text}</p>
|
||||
) : (
|
||||
<p className="text-xs text-slate-600 leading-relaxed mb-3">
|
||||
GPT-4o peut interpréter le régime actuel, identifier les incohérences entre compteurs et proposer des biais de trading.
|
||||
GPT-4o can interpret the current regime, identify inconsistencies between gauges and suggest trading biases.
|
||||
</p>
|
||||
)}
|
||||
{aiStatus?.enabled ? (
|
||||
@@ -336,25 +336,25 @@ export default function MacroRegime() {
|
||||
className="w-full text-xs bg-blue-600/20 hover:bg-blue-600/40 border border-blue-500/30 hover:border-blue-500/60 text-blue-300 rounded py-1.5 transition-all disabled:opacity-40 flex items-center justify-center gap-1.5"
|
||||
>
|
||||
{loadingNarration ? (
|
||||
<><RefreshCw className="w-3 h-3 animate-spin" /> Analyse en cours...</>
|
||||
<><RefreshCw className="w-3 h-3 animate-spin" /> Analysis in progress...</>
|
||||
) : (
|
||||
<><Brain className="w-3 h-3" /> {savedNarration ? 'Ré-analyser' : 'Demander à l\'IA'}</>
|
||||
<><Brain className="w-3 h-3" /> {savedNarration ? 'Re-analyze' : 'Ask AI'}</>
|
||||
)}
|
||||
</button>
|
||||
) : (
|
||||
<div className="text-xs text-slate-700 text-center">OpenAI non configuré</div>
|
||||
<div className="text-xs text-slate-700 text-center">OpenAI not configured</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Reading order reminder */}
|
||||
<div className="card bg-dark-700/30">
|
||||
<div className="text-xs font-semibold text-slate-600 mb-2">Ordre de lecture (5 min)</div>
|
||||
<div className="text-xs font-semibold text-slate-600 mb-2">Reading order (5 min)</div>
|
||||
<div className="space-y-0.5 text-[10px] text-slate-600">
|
||||
<div>1. <span className="text-slate-500">Liquidité</span> → Bilan Fed, DXY, taux réels</div>
|
||||
<div>2. <span className="text-slate-500">Crédit</span> → HY spreads, VIX, MOVE</div>
|
||||
<div>3. <span className="text-slate-500">Énergie</span> → Brent, gaz naturel</div>
|
||||
<div>4. <span className="text-slate-500">Métaux</span> → Or/Cuivre ratio</div>
|
||||
<div>5. <span className="text-slate-500">Synthèse</span> → Scénario dominant</div>
|
||||
<div>1. <span className="text-slate-500">Liquidity</span> → Fed balance sheet, DXY, real rates</div>
|
||||
<div>2. <span className="text-slate-500">Credit</span> → HY spreads, VIX, MOVE</div>
|
||||
<div>3. <span className="text-slate-500">Energy</span> → Brent, natural gas</div>
|
||||
<div>4. <span className="text-slate-500">Metals</span> → Gold/Copper ratio</div>
|
||||
<div>5. <span className="text-slate-500">Synthesis</span> → Dominant scenario</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,11 +8,11 @@ import { TrendingUp, TrendingDown, BarChart2, RefreshCw, Search } from 'lucide-r
|
||||
|
||||
|
||||
const ASSET_CLASSES: { key: AssetClass; label: string; emoji: string }[] = [
|
||||
{ key: 'energy', label: 'Énergie', emoji: '⛽' },
|
||||
{ key: 'metals', label: 'Métaux', emoji: '🥇' },
|
||||
{ key: 'energy', label: 'Energy', emoji: '⛽' },
|
||||
{ key: 'metals', label: 'Metals', emoji: '🥇' },
|
||||
{ key: 'agriculture', label: 'Agriculture', emoji: '🌾' },
|
||||
{ key: 'indices', label: 'Indices', emoji: '📊' },
|
||||
{ key: 'equities', label: 'Actions', emoji: '📈' },
|
||||
{ key: 'equities', label: 'Equities', emoji: '📈' },
|
||||
{ key: 'forex', label: 'Forex', emoji: '💱' },
|
||||
]
|
||||
|
||||
@@ -56,8 +56,8 @@ const PERIOD_OPTIONS = [
|
||||
{ label: '1M', value: '1mo' },
|
||||
{ label: '3M', value: '3mo' },
|
||||
{ label: '6M', value: '6mo' },
|
||||
{ label: '1A', value: '1y' },
|
||||
{ label: '2A', value: '2y' },
|
||||
{ label: '1Y', value: '1y' },
|
||||
{ label: '2Y', value: '2y' },
|
||||
]
|
||||
|
||||
function PriceChart({ symbol, name }: { symbol: string; name: string }) {
|
||||
@@ -127,7 +127,7 @@ function PriceChart({ symbol, name }: { symbol: string; name: string }) {
|
||||
<Tooltip
|
||||
contentStyle={{ background: '#0f1623', border: '1px solid #1e2d4d', fontSize: 11 }}
|
||||
labelStyle={{ color: '#94a3b8' }}
|
||||
formatter={(v: number) => [v.toFixed(4), 'Prix']}
|
||||
formatter={(v: number) => [v.toFixed(4), 'Price']}
|
||||
/>
|
||||
<Area
|
||||
type="monotone" dataKey="close"
|
||||
@@ -139,7 +139,7 @@ function PriceChart({ symbol, name }: { symbol: string; name: string }) {
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="h-48 flex items-center justify-center text-slate-600 text-xs">
|
||||
Aucune donnée disponible
|
||||
No data available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -192,17 +192,17 @@ export default function Markets() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-white flex items-center gap-2">
|
||||
<BarChart2 className="w-5 h-5 text-blue-400" /> Marchés & Prix
|
||||
<BarChart2 className="w-5 h-5 text-blue-400" /> Markets & Prices
|
||||
</h1>
|
||||
<p className="text-xs text-slate-500 mt-0.5">
|
||||
Données en temps réel · Volatilité implicite · Opportunités options
|
||||
Real-time data · Implied volatility · Options opportunities
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => refetch()}
|
||||
className="flex items-center gap-1.5 text-xs text-slate-400 hover:text-slate-200 transition-colors"
|
||||
>
|
||||
<RefreshCw className="w-3.5 h-3.5" /> Actualiser
|
||||
<RefreshCw className="w-3.5 h-3.5" /> Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -227,7 +227,7 @@ export default function Markets() {
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Escape') setSearchQuery('') }}
|
||||
placeholder="Rechercher un ticker..."
|
||||
placeholder="Search ticker..."
|
||||
className="bg-dark-700 border border-slate-700 rounded pl-8 pr-3 py-1.5 text-sm text-white placeholder-slate-600 focus:outline-none focus:border-blue-500 w-52"
|
||||
/>
|
||||
{searchResults.length > 0 && (
|
||||
@@ -282,7 +282,7 @@ export default function Markets() {
|
||||
))
|
||||
) : (
|
||||
<div className="card text-slate-500 text-xs text-center py-6">
|
||||
Démarrer le backend pour charger les prix
|
||||
Start the backend to load prices
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -295,8 +295,8 @@ export default function Markets() {
|
||||
<div className="card h-full flex items-center justify-center text-slate-600 text-sm">
|
||||
<div className="text-center">
|
||||
<BarChart2 className="w-8 h-8 mx-auto mb-2 opacity-30" />
|
||||
<div>Sélectionner un instrument</div>
|
||||
<div className="text-xs mt-1">pour voir le graphique de prix</div>
|
||||
<div>Select an instrument</div>
|
||||
<div className="text-xs mt-1">to view the price chart</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -305,16 +305,16 @@ export default function Markets() {
|
||||
|
||||
{/* Market overview table */}
|
||||
<div className="card">
|
||||
<div className="section-title">Vue d'ensemble — Tous marchés</div>
|
||||
<div className="section-title">Overview — All Markets</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-slate-600 border-b border-slate-700/40">
|
||||
<th className="text-left pb-2">Instrument</th>
|
||||
<th className="text-left pb-2">Classe</th>
|
||||
<th className="text-right pb-2">Prix</th>
|
||||
<th className="text-right pb-2">Var. 1j</th>
|
||||
<th className="text-right pb-2">Vol. IV (est.)</th>
|
||||
<th className="text-left pb-2">Class</th>
|
||||
<th className="text-right pb-2">Price</th>
|
||||
<th className="text-right pb-2">1d Chg</th>
|
||||
<th className="text-right pb-2">IV Vol (est.)</th>
|
||||
<th className="text-right pb-2">Signal</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -337,9 +337,9 @@ export default function Markets() {
|
||||
{q.iv ? `${(q.iv * 100).toFixed(1)}%` : '—'}
|
||||
</td>
|
||||
<td className="py-1.5 text-right">
|
||||
{q.change_pct > 1.5 ? <span className="badge-green badge">▲ Haussier</span>
|
||||
: q.change_pct < -1.5 ? <span className="badge-red badge">▼ Baissier</span>
|
||||
: <span className="badge badge-blue">→ Neutre</span>}
|
||||
{q.change_pct > 1.5 ? <span className="badge-green badge">▲ Bullish</span>
|
||||
: q.change_pct < -1.5 ? <span className="badge-red badge">▼ Bearish</span>
|
||||
: <span className="badge badge-blue">→ Neutral</span>}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
|
||||
@@ -25,9 +25,9 @@ function ivRankBg(rank: number | null | undefined): string {
|
||||
|
||||
function ivSignalLabel(rank: number | null | undefined) {
|
||||
if (rank == null) return null
|
||||
if (rank >= 80) return { text: 'Vendre de la vol', icon: <TrendingDown className="w-3 h-3" />, cls: 'text-red-400' }
|
||||
if (rank < 20) return { text: 'Acheter de la vol', icon: <TrendingUp className="w-3 h-3" />, cls: 'text-blue-400' }
|
||||
return { text: 'Neutre', icon: <Minus className="w-3 h-3" />, cls: 'text-slate-500' }
|
||||
if (rank >= 80) return { text: 'Sell vol', icon: <TrendingDown className="w-3 h-3" />, cls: 'text-red-400' }
|
||||
if (rank < 20) return { text: 'Buy vol', icon: <TrendingUp className="w-3 h-3" />, cls: 'text-blue-400' }
|
||||
return { text: 'Neutral', icon: <Minus className="w-3 h-3" />, cls: 'text-slate-500' }
|
||||
}
|
||||
|
||||
function StructureBadge({ structure }: { structure: string | null | undefined }) {
|
||||
@@ -93,7 +93,7 @@ function TickerDetail({ ticker }: { ticker: string }) {
|
||||
<div>
|
||||
<div className="text-[9px] text-slate-500 uppercase tracking-wide font-semibold mb-2">Term Structure</div>
|
||||
<div className="space-y-1.5">
|
||||
{([['30j', ts.iv_30d], ['60j', ts.iv_60d], ['90j', ts.iv_90d], ['180j', ts.iv_180d]] as [string, number | undefined][]).map(([label, iv]) =>
|
||||
{([['30d', ts.iv_30d], ['60d', ts.iv_60d], ['90d', ts.iv_90d], ['180d', ts.iv_180d]] as [string, number | undefined][]).map(([label, iv]) =>
|
||||
iv ? (
|
||||
<div key={label} className="flex items-center gap-2">
|
||||
<span className="text-[9px] text-slate-600 w-7 shrink-0">{label}</span>
|
||||
@@ -130,7 +130,7 @@ function TickerDetail({ ticker }: { ticker: string }) {
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-[9px] text-slate-700 italic">Données insuffisantes</div>
|
||||
<div className="text-[9px] text-slate-700 italic">Insufficient data</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -150,7 +150,7 @@ function TickerDetail({ ticker }: { ticker: string }) {
|
||||
{flow.gamma_bias && <div className="text-[9px] text-amber-500/70">{flow.gamma_bias}</div>}
|
||||
{flow.unusual_strikes?.length > 0 && (
|
||||
<div className="mt-1 pt-1 border-t border-slate-700/30">
|
||||
<div className="text-[8px] text-slate-600 mb-0.5">Strikes inhabituels</div>
|
||||
<div className="text-[8px] text-slate-600 mb-0.5">Unusual strikes</div>
|
||||
{flow.unusual_strikes.slice(0, 2).map((s: any) => (
|
||||
<div key={s.strike} className="text-[8px] font-mono text-slate-500">
|
||||
{s.strike} {s.type.toUpperCase()} OI={s.total_oi.toLocaleString()}
|
||||
@@ -161,19 +161,19 @@ function TickerDetail({ ticker }: { ticker: string }) {
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-[9px] text-slate-700 italic">Pas d'OI disponible</div>
|
||||
<div className="text-[9px] text-slate-700 italic">No OI available</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* History sparkline */}
|
||||
{history.length > 4 && (
|
||||
<div className="col-span-3 flex items-center gap-3 pt-1 border-t border-slate-700/20">
|
||||
<span className="text-[9px] text-slate-600 shrink-0">IV 90j</span>
|
||||
<span className="text-[9px] text-slate-600 shrink-0">IV 90d</span>
|
||||
<IvSparkline history={history} />
|
||||
<div className="text-[9px] text-slate-600">
|
||||
min <span className="text-slate-400">{snap.iv_min_52w_pct}%</span>
|
||||
{' · '}max <span className="text-slate-400">{snap.iv_max_52w_pct}%</span>
|
||||
{' · '}{snap.history_days}j d'historique
|
||||
{' · '}{snap.history_days}d of history
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -203,7 +203,7 @@ function WatchlistRow({ item }: { item: any }) {
|
||||
<span className={clsx('text-sm font-bold font-mono', ivRankColor(item.iv_rank))}>
|
||||
{item.iv_current_pct != null ? `${item.iv_source === 'history' ? '~' : ''}${item.iv_current_pct}%` : '—'}
|
||||
</span>
|
||||
<div className="text-[8px] text-slate-600">{item.iv_source === 'history' ? 'IV estimée' : 'IV actuelle'}</div>
|
||||
<div className="text-[8px] text-slate-600">{item.iv_source === 'history' ? 'Estimated IV' : 'Current IV'}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0 space-y-1">
|
||||
@@ -230,7 +230,7 @@ function WatchlistRow({ item }: { item: any }) {
|
||||
</div>
|
||||
)}
|
||||
{item.history_days > 0 && (
|
||||
<div className="text-[8px] text-slate-500">{item.history_days}j historique</div>
|
||||
<div className="text-[8px] text-slate-500">{item.history_days}d history</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -286,15 +286,15 @@ function WatchlistManager() {
|
||||
<div className="card space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<List className="w-4 h-4 text-slate-400" />
|
||||
<span className="text-sm font-semibold text-slate-300">Gestion de la Watchlist IV</span>
|
||||
<span className="ml-auto text-xs text-slate-600">{active.length} tickers actifs</span>
|
||||
<span className="text-sm font-semibold text-slate-300">IV Watchlist Manager</span>
|
||||
<span className="ml-auto text-xs text-slate-600">{active.length} active tickers</span>
|
||||
</div>
|
||||
|
||||
{/* Add ticker */}
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
className="flex-1 bg-slate-800 border border-slate-700 rounded px-3 py-1.5 text-xs text-slate-300 placeholder:text-slate-600"
|
||||
placeholder="Ajouter ticker (ex: AAPL, FXI, EEM…)"
|
||||
placeholder="Add ticker (e.g. AAPL, FXI, EEM…)"
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value.toUpperCase())}
|
||||
onKeyDown={e => e.key === 'Enter' && handleAdd()}
|
||||
@@ -305,10 +305,10 @@ function WatchlistManager() {
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-blue-600/20 hover:bg-blue-600/30 border border-blue-600/40 text-blue-400 rounded disabled:opacity-40 transition-all"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
{adding ? 'Ajout...' : 'Ajouter'}
|
||||
{adding ? 'Adding...' : 'Add'}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-600">Le système bootstrappe automatiquement 1 an d'historique IV pour chaque nouveau ticker ajouté.</p>
|
||||
<p className="text-[10px] text-slate-600">The system automatically bootstraps 1 year of IV history for each new ticker added.</p>
|
||||
|
||||
{/* Active tickers */}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
@@ -331,12 +331,12 @@ function WatchlistManager() {
|
||||
<div className="text-[9px] text-slate-700 flex gap-3">
|
||||
<span className="text-slate-800 border-b border-slate-700">■</span> Builtins
|
||||
<span className="text-blue-700 border-b border-blue-700">■</span> Auto (cycle)
|
||||
<span className="text-emerald-700 border-b border-emerald-700">■</span> Manuel
|
||||
<span className="text-emerald-700 border-b border-emerald-700">■</span> Manual
|
||||
</div>
|
||||
|
||||
{inactive.length > 0 && (
|
||||
<button onClick={() => setShowInactive(!showInactive)} className="text-[10px] text-slate-600 hover:text-slate-400">
|
||||
{showInactive ? 'Masquer' : `Voir ${inactive.length} ticker(s) désactivés`}
|
||||
{showInactive ? 'Hide' : `Show ${inactive.length} disabled ticker(s)`}
|
||||
</button>
|
||||
)}
|
||||
{showInactive && inactive.map(e => (
|
||||
@@ -365,10 +365,10 @@ export default function OptionsLab() {
|
||||
setBootstrapMsg(null)
|
||||
try {
|
||||
await api.post('/options-vol/bootstrap-history')
|
||||
setBootstrapMsg('Bootstrap lancé (~60s) — actualise dans 1 minute pour voir les IV Rank mis à jour.')
|
||||
setBootstrapMsg('Bootstrap started (~60s) — refresh in 1 minute to see updated IV Ranks.')
|
||||
setTimeout(() => refetch(), 70_000)
|
||||
} catch {
|
||||
setBootstrapMsg('Erreur lors du bootstrap.')
|
||||
setBootstrapMsg('Error during bootstrap.')
|
||||
} finally {
|
||||
setBootstrapping(false)
|
||||
}
|
||||
@@ -381,14 +381,14 @@ export default function OptionsLab() {
|
||||
<div className="p-6 space-y-5">
|
||||
{needsBootstrap && !bootstrapMsg && (
|
||||
<div className="flex items-center justify-between gap-3 px-4 py-3 rounded border border-amber-700/40 bg-amber-900/10 text-xs text-amber-300">
|
||||
<span>⚠️ Historique IV insuffisant — IV Rank bloqué à 50%. Bootstrappe 1 an de données réalisées pour calibrer le Rank.</span>
|
||||
<span>⚠️ Insufficient IV history — IV Rank stuck at 50%. Bootstrap 1 year of realized data to calibrate the Rank.</span>
|
||||
<button
|
||||
onClick={handleBootstrap}
|
||||
disabled={bootstrapping}
|
||||
className="flex items-center gap-1.5 text-xs bg-amber-700/30 hover:bg-amber-700/50 border border-amber-600/50 text-amber-200 px-3 py-1.5 rounded transition-all whitespace-nowrap disabled:opacity-50"
|
||||
>
|
||||
<Database className={clsx('w-3.5 h-3.5', bootstrapping && 'animate-pulse')} />
|
||||
{bootstrapping ? 'Chargement...' : 'Initialiser historique'}
|
||||
{bootstrapping ? 'Loading...' : 'Initialize history'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -413,7 +413,7 @@ export default function OptionsLab() {
|
||||
className="flex items-center gap-1.5 text-xs border border-slate-600 text-slate-400 hover:text-slate-200 hover:border-slate-500 px-3 py-1.5 rounded transition-all disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={clsx('w-3.5 h-3.5', isFetching && 'animate-spin')} />
|
||||
{isFetching ? 'Chargement...' : 'Actualiser'}
|
||||
{isFetching ? 'Loading...' : 'Refresh'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -422,22 +422,22 @@ export default function OptionsLab() {
|
||||
<div className="card bg-red-900/10 border-red-700/20 text-xs">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<TrendingDown className="w-3.5 h-3.5 text-red-400" />
|
||||
<span className="font-semibold text-red-400">IV Rank > 80% — Vendre de la vol</span>
|
||||
<span className="font-semibold text-red-400">IV Rank > 80% — Sell vol</span>
|
||||
</div>
|
||||
<div className="text-slate-500 leading-snug">Credit spreads, iron condors, covered calls. La prime est élevée → avantage au vendeur.</div>
|
||||
<div className="text-slate-500 leading-snug">Credit spreads, iron condors, covered calls. Premium is high → seller has the edge.</div>
|
||||
</div>
|
||||
<div className="card bg-blue-900/10 border-blue-700/20 text-xs">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<TrendingUp className="w-3.5 h-3.5 text-blue-400" />
|
||||
<span className="font-semibold text-blue-400">IV Rank < 20% — Acheter de la convexité</span>
|
||||
<span className="font-semibold text-blue-400">IV Rank < 20% — Buy convexity</span>
|
||||
</div>
|
||||
<div className="text-slate-500 leading-snug">Long calls, long puts, straddles, strangles. La prime est cheap → bon moment pour acheter du gamma.</div>
|
||||
<div className="text-slate-500 leading-snug">Long calls, long puts, straddles, strangles. Premium is cheap → good time to buy gamma.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-600 -mt-2 px-1">
|
||||
<strong className="text-slate-500">Contango</strong> = IV courte < longue (marché calme, bon pour vendre du court terme) ·
|
||||
<strong className="text-slate-500"> Backwardation</strong> = IV courte > longue (stress, bon pour acheter de la protection courte) ·
|
||||
<strong className="text-slate-500"> Skew +</strong> = puts plus chers que calls (biais baissier institutionnel)
|
||||
<strong className="text-slate-500">Contango</strong> = short IV < long IV (calm market, good for selling short term) ·
|
||||
<strong className="text-slate-500"> Backwardation</strong> = short IV > long IV (stress, good for buying short protection) ·
|
||||
<strong className="text-slate-500"> Skew +</strong> = puts more expensive than calls (institutional bearish bias)
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
@@ -447,14 +447,14 @@ export default function OptionsLab() {
|
||||
) : items.length === 0 ? (
|
||||
<div className="card text-center py-12 text-slate-600">
|
||||
<Activity className="w-8 h-8 mx-auto mb-3 opacity-30" />
|
||||
<div className="font-semibold mb-1 text-slate-500">Aucune donnée IV disponible</div>
|
||||
<div className="font-semibold mb-1 text-slate-500">No IV data available</div>
|
||||
<div className="text-xs text-slate-600">
|
||||
Les ETFs US sont requis (SPY, QQQ, GLD...).
|
||||
<br />L'IV Rank nécessite de l'historique — les données s'accumulent à chaque actualisation.
|
||||
US ETFs are required (SPY, QQQ, GLD...).
|
||||
<br />IV Rank requires history — data accumulates with each refresh.
|
||||
</div>
|
||||
<button onClick={() => refetch()} disabled={isFetching}
|
||||
className="mt-4 text-xs bg-blue-600 hover:bg-blue-500 text-white px-4 py-1.5 rounded disabled:opacity-50">
|
||||
Lancer la collecte
|
||||
Start collection
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -462,7 +462,7 @@ export default function OptionsLab() {
|
||||
{sellVol.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-red-400 uppercase tracking-wide mb-2 flex items-center gap-1.5">
|
||||
<TrendingDown className="w-3.5 h-3.5" /> Vendre de la vol — IVR > 80% ({sellVol.length})
|
||||
<TrendingDown className="w-3.5 h-3.5" /> Sell vol — IVR > 80% ({sellVol.length})
|
||||
</div>
|
||||
<div className="space-y-1.5">{sellVol.map(item => <WatchlistRow key={item.ticker} item={item} />)}</div>
|
||||
</div>
|
||||
@@ -470,7 +470,7 @@ export default function OptionsLab() {
|
||||
{buyVol.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-blue-400 uppercase tracking-wide mb-2 flex items-center gap-1.5">
|
||||
<TrendingUp className="w-3.5 h-3.5" /> Acheter de la vol — IVR < 20% ({buyVol.length})
|
||||
<TrendingUp className="w-3.5 h-3.5" /> Buy vol — IVR < 20% ({buyVol.length})
|
||||
</div>
|
||||
<div className="space-y-1.5">{buyVol.map(item => <WatchlistRow key={item.ticker} item={item} />)}</div>
|
||||
</div>
|
||||
@@ -478,14 +478,14 @@ export default function OptionsLab() {
|
||||
{neutral.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 flex items-center gap-1.5">
|
||||
<Minus className="w-3.5 h-3.5" /> Zone neutre — IVR 20–80% ({neutral.length})
|
||||
<Minus className="w-3.5 h-3.5" /> Neutral zone — IVR 20–80% ({neutral.length})
|
||||
</div>
|
||||
<div className="space-y-1.5">{neutral.map(item => <WatchlistRow key={item.ticker} item={item} />)}</div>
|
||||
</div>
|
||||
)}
|
||||
{noData.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs text-slate-500 uppercase tracking-wide mb-2">Sans historique ({noData.length})</div>
|
||||
<div className="text-xs text-slate-500 uppercase tracking-wide mb-2">No history ({noData.length})</div>
|
||||
<div className="space-y-1.5">{noData.map(item => <WatchlistRow key={item.ticker} item={item} />)}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -16,14 +16,14 @@ function jaccard(a: string[], b: string[]): number {
|
||||
const TRIGGERS = ['military', 'sanctions', 'elections', 'natural_disaster', 'health_crisis', 'resource_scarcity', 'trade_war', 'energy', 'political_speech', 'financial_crisis']
|
||||
const ASSET_CLASSES = ['energy', 'metals', 'agriculture', 'equities', 'indices', 'forex', 'rates']
|
||||
const ASSET_CLASS_LABELS: Record<string, string> = {
|
||||
energy: '⚡ Énergie', metals: '🥇 Métaux', agriculture: '🌾 Agri',
|
||||
equities: '📈 Actions', indices: '📊 Indices', forex: '💱 Forex', rates: '📉 Taux',
|
||||
energy: '⚡ Energy', metals: '🥇 Metals', agriculture: '🌾 Agri',
|
||||
equities: '📈 Equities', indices: '📊 Indices', forex: '💱 Forex', rates: '📉 Rates',
|
||||
}
|
||||
const TRIGGER_LABELS: Record<string, string> = {
|
||||
military: '⚔️ Militaire', sanctions: '🚫 Sanctions', elections: '🗳️ Élections',
|
||||
natural_disaster: '🌪️ Catastrophe', health_crisis: '🏥 Santé',
|
||||
resource_scarcity: '⚠️ Ressources', trade_war: '🤝 Commerce',
|
||||
energy: '⚡ Énergie', political_speech: '🎙️ Discours', financial_crisis: '💸 Finance',
|
||||
military: '⚔️ Military', sanctions: '🚫 Sanctions', elections: '🗳️ Elections',
|
||||
natural_disaster: '🌪️ Disaster', health_crisis: '🏥 Health',
|
||||
resource_scarcity: '⚠️ Resources', trade_war: '🤝 Trade',
|
||||
energy: '⚡ Energy', political_speech: '🎙️ Speech', financial_crisis: '💸 Finance',
|
||||
}
|
||||
|
||||
const EMPTY_PATTERN = {
|
||||
@@ -35,7 +35,7 @@ const EMPTY_PATTERN = {
|
||||
|
||||
function QualityBadge({ score }: { score: number }) {
|
||||
const color = score >= 75 ? 'badge-green' : score >= 50 ? 'badge-yellow' : 'badge-red'
|
||||
const label = score >= 75 ? 'Excellent' : score >= 50 ? 'Bon' : 'Faible'
|
||||
const label = score >= 75 ? 'Excellent' : score >= 50 ? 'Good' : 'Weak'
|
||||
return <span className={clsx('badge', color)}>{score}/100 — {label}</span>
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
|
||||
<div className="flex items-center gap-2 mb-0.5 flex-wrap">
|
||||
<div className="text-sm font-semibold text-white">{p.name}</div>
|
||||
{isCustom && <span className="badge badge-purple text-xs">Custom</span>}
|
||||
{!isActive && <span className="badge badge-red text-xs">Désactivé</span>}
|
||||
{!isActive && <span className="badge badge-red text-xs">Disabled</span>}
|
||||
{p.ai_quality_score && <QualityBadge score={p.ai_quality_score} />}
|
||||
<ReliabilityBadge rel={reliability} />
|
||||
</div>
|
||||
@@ -83,15 +83,15 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
|
||||
<div className={clsx('text-sm font-bold', p.expected_move_pct > 0 ? 'positive' : 'negative')}>
|
||||
{p.expected_move_pct > 0 ? '+' : ''}{p.expected_move_pct}%
|
||||
</div>
|
||||
<div className="text-xs text-slate-600">{p.horizon_days}j</div>
|
||||
<div className="text-xs text-slate-600">{p.horizon_days}d</div>
|
||||
</div>
|
||||
<button onClick={onEdit} title="Modifier" className="text-slate-500 hover:text-blue-400 transition-colors">
|
||||
<button onClick={onEdit} title="Edit" className="text-slate-500 hover:text-blue-400 transition-colors">
|
||||
<Edit3 className="w-4 h-4" />
|
||||
</button>
|
||||
<button onClick={onToggle} title={isActive ? 'Désactiver' : 'Activer'} className={clsx('transition-colors', isActive ? 'text-slate-500 hover:text-yellow-400' : 'text-yellow-500 hover:text-yellow-300')}>
|
||||
<button onClick={onToggle} title={isActive ? 'Disable' : 'Enable'} className={clsx('transition-colors', isActive ? 'text-slate-500 hover:text-yellow-400' : 'text-yellow-500 hover:text-yellow-300')}>
|
||||
{isActive ? <Eye className="w-4 h-4" /> : <EyeOff className="w-4 h-4" />}
|
||||
</button>
|
||||
<button onClick={onDelete} title="Supprimer" className="text-slate-500 hover:text-red-400 transition-colors">
|
||||
<button onClick={onDelete} title="Delete" className="text-slate-500 hover:text-red-400 transition-colors">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -101,7 +101,7 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
|
||||
<div className="flex flex-wrap gap-1 mb-2">
|
||||
{similarTo.map((s, i) => (
|
||||
<span key={i} className="inline-flex items-center gap-1 text-[10px] bg-amber-900/30 border border-amber-600/30 text-amber-400 rounded px-1.5 py-0.5">
|
||||
⚠ Similaire à <span className="font-semibold max-w-[140px] truncate">{s.name}</span>
|
||||
⚠ Similar to <span className="font-semibold max-w-[140px] truncate">{s.name}</span>
|
||||
<span className="font-mono">{Math.round(s.similarity * 100)}%</span>
|
||||
</span>
|
||||
))}
|
||||
@@ -115,7 +115,7 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
|
||||
<span className="badge badge-blue text-xs">{p.asset_class}</span>
|
||||
{aiScore != null ? (
|
||||
<span className={clsx('badge text-xs font-bold', aiScore >= 50 ? 'badge-green' : aiScore >= 25 ? 'badge-yellow' : 'badge-red')}>
|
||||
{aiScore}/100 IA
|
||||
{aiScore}/100 AI
|
||||
</span>
|
||||
) : (
|
||||
<span className="badge badge-purple text-xs">{Math.round(p.probability * 100)}% prob.</span>
|
||||
@@ -123,14 +123,14 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
|
||||
</div>
|
||||
|
||||
<button onClick={() => setExpanded(!expanded)} className="text-xs text-slate-600 hover:text-slate-400">
|
||||
{expanded ? '▲ Réduire' : '▼ Voir détails'}
|
||||
{expanded ? '▲ Collapse' : '▼ See details'}
|
||||
</button>
|
||||
|
||||
{expanded && (
|
||||
<div className="mt-3 space-y-2">
|
||||
{p.keywords?.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs text-slate-600 mb-1">Mots-clés de détection</div>
|
||||
<div className="text-xs text-slate-600 mb-1">Detection keywords</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{p.keywords.map((kw: string) => (
|
||||
<span key={kw} className="text-xs bg-dark-700 text-slate-400 px-1.5 py-0.5 rounded">#{kw}</span>
|
||||
@@ -140,7 +140,7 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
|
||||
)}
|
||||
{p.historical_instances?.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs text-slate-600 mb-1">Instances historiques</div>
|
||||
<div className="text-xs text-slate-600 mb-1">Historical instances</div>
|
||||
{p.historical_instances.map((h: any, i: number) => (
|
||||
<div key={i} className="card-sm mb-1">
|
||||
<span className="text-xs text-slate-500">{h.date}</span>
|
||||
@@ -151,7 +151,7 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
|
||||
)}
|
||||
{p.suggested_trades?.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs text-slate-600 mb-1">Trades suggérés</div>
|
||||
<div className="text-xs text-slate-600 mb-1">Suggested trades</div>
|
||||
{p.suggested_trades.map((t: any, i: number) => (
|
||||
<div key={i} className="flex items-center gap-2 text-xs mb-1 flex-wrap">
|
||||
<span className="badge badge-green">{t.strategy}</span>
|
||||
@@ -165,11 +165,11 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
|
||||
{(p.counter_thesis || p.invalidation_trigger) && (
|
||||
<div className="card-sm border-red-700/30 bg-red-900/10">
|
||||
<div className="text-xs text-red-400 font-semibold mb-1 flex items-center gap-1">
|
||||
<ShieldAlert className="w-3 h-3" /> Contre-thèse & Invalidation
|
||||
<ShieldAlert className="w-3 h-3" /> Counter-thesis & Invalidation
|
||||
</div>
|
||||
{p.counter_thesis && (
|
||||
<div className="text-xs text-slate-300 mb-1">
|
||||
<span className="text-slate-500">Contre-thèse :</span> {p.counter_thesis}
|
||||
<span className="text-slate-500">Counter-thesis:</span> {p.counter_thesis}
|
||||
</div>
|
||||
)}
|
||||
{p.invalidation_trigger && (
|
||||
@@ -184,7 +184,7 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
|
||||
)}
|
||||
{p.ai_evaluation && Object.keys(p.ai_evaluation).length > 0 && (
|
||||
<div className="card-sm border-blue-700/30">
|
||||
<div className="text-xs text-blue-400 font-semibold mb-1">Évaluation IA</div>
|
||||
<div className="text-xs text-blue-400 font-semibold mb-1">AI Evaluation</div>
|
||||
{p.ai_evaluation.strengths?.length > 0 && (
|
||||
<div className="text-xs text-emerald-400 mb-1">✓ {p.ai_evaluation.strengths[0]}</div>
|
||||
)}
|
||||
@@ -233,10 +233,10 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
|
||||
<div className="flex items-center justify-between p-4 border-b border-slate-700/30">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-white flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4 text-blue-400" /> Suggérer des patterns par l'IA
|
||||
<Sparkles className="w-4 h-4 text-blue-400" /> Suggest patterns with AI
|
||||
</h2>
|
||||
<p className="text-xs text-slate-500 mt-0.5">
|
||||
GPT-4o analyse l'actualité géo, les prix, le calendrier et le <span className="text-blue-400">régime macro actuel</span> pour proposer des patterns cohérents avec le scénario dominant
|
||||
GPT-4o analyzes geo news, prices, calendar and the <span className="text-blue-400">current macro regime</span> to propose patterns aligned with the dominant scenario
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={onClose} className="text-slate-500 hover:text-white"><X className="w-5 h-5" /></button>
|
||||
@@ -248,11 +248,11 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
|
||||
<div className="text-center py-12">
|
||||
<Sparkles className="w-10 h-10 text-blue-400/40 mx-auto mb-3" />
|
||||
<p className="text-sm text-slate-400 mb-4">
|
||||
L'IA va analyser les news géopolitiques actuelles, les mouvements de prix et le calendrier économique pour proposer des patterns pertinents <em>aujourd'hui</em>.
|
||||
The AI will analyze current geopolitical news, price movements, and the economic calendar to propose patterns relevant <em>today</em>.
|
||||
</p>
|
||||
<button onClick={() => suggest()}
|
||||
className="bg-blue-600 hover:bg-blue-500 text-white px-6 py-2 rounded text-sm font-semibold">
|
||||
Lancer l'analyse (GPT-4o)
|
||||
Run analysis (GPT-4o)
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -260,7 +260,7 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
|
||||
{isPending && (
|
||||
<div className="text-center py-12">
|
||||
<div className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto mb-3"></div>
|
||||
<p className="text-sm text-slate-400">GPT-4o analyse le contexte du moment…</p>
|
||||
<p className="text-sm text-slate-400">GPT-4o analyzing current context…</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -277,7 +277,7 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
|
||||
<span className={clsx('badge text-xs font-bold', score >= 50 ? 'badge-green' : score >= 25 ? 'badge-yellow' : 'badge-red')}>
|
||||
{score}/100 IA
|
||||
</span>
|
||||
<span className="text-xs text-slate-500">{p.horizon_days}j · {p.expected_move_pct > 0 ? '+' : ''}{p.expected_move_pct}%</span>
|
||||
<span className="text-xs text-slate-500">{p.horizon_days}d · {p.expected_move_pct > 0 ? '+' : ''}{p.expected_move_pct}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@@ -287,7 +287,7 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
|
||||
'bg-emerald-700/30 border border-emerald-700/40 text-emerald-400 cursor-default': saved.has(idx),
|
||||
'bg-blue-600 hover:bg-blue-500 text-white': !saved.has(idx),
|
||||
})}>
|
||||
{saved.has(idx) ? <><Check className="w-3 h-3" /> Sauvegardé</> : <><Save className="w-3 h-3" /> Sauvegarder</>}
|
||||
{saved.has(idx) ? <><Check className="w-3 h-3" /> Saved</> : <><Save className="w-3 h-3" /> Save</>}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -295,7 +295,7 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
|
||||
<div className="flex flex-wrap gap-1 mb-2">
|
||||
{similars.map((s, i) => (
|
||||
<span key={i} className="inline-flex items-center gap-1 text-[10px] bg-amber-900/30 border border-amber-600/30 text-amber-400 rounded px-1.5 py-0.5">
|
||||
⚠ Similaire à <span className="font-semibold max-w-[140px] truncate">{s.name}</span>
|
||||
⚠ Similar to <span className="font-semibold max-w-[140px] truncate">{s.name}</span>
|
||||
<span className="font-mono">{Math.round(s.similarity * 100)}%</span>
|
||||
</span>
|
||||
))}
|
||||
@@ -312,7 +312,7 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
|
||||
{(p.counter_thesis || p.invalidation_trigger) && (
|
||||
<div className="mb-2 text-[11px] bg-red-900/10 border border-red-700/20 rounded px-2 py-1.5 space-y-0.5">
|
||||
{p.counter_thesis && (
|
||||
<div className="text-slate-300"><span className="text-red-400/80">⚠ Contre-thèse :</span> {p.counter_thesis}</div>
|
||||
<div className="text-slate-300"><span className="text-red-400/80">⚠ Counter-thesis:</span> {p.counter_thesis}</div>
|
||||
)}
|
||||
{p.invalidation_trigger && (
|
||||
<div className="text-orange-300/70"><span className="text-slate-500">Trigger :</span> {p.invalidation_trigger}{p.invalidation_probability != null ? ` (${Math.round(p.invalidation_probability * 100)}%)` : ''}</div>
|
||||
@@ -336,7 +336,7 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
|
||||
|
||||
{data && suggestions.length === 0 && (
|
||||
<div className="card text-center py-8 text-slate-500 text-sm">
|
||||
Aucun pattern suggéré. Réessayer.
|
||||
No patterns suggested. Try again.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -345,18 +345,18 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
|
||||
<div className="p-4 border-t border-slate-700/30 flex justify-between items-center">
|
||||
{suggestions.length > 0 && (
|
||||
<span className="text-xs text-slate-500">
|
||||
{saved.size}/{suggestions.length} sauvegardé{saved.size > 1 ? 's' : ''}
|
||||
{saved.size}/{suggestions.length} saved
|
||||
</span>
|
||||
)}
|
||||
<div className="flex gap-2 ml-auto">
|
||||
{data && suggestions.length > 0 && (
|
||||
<button onClick={() => suggest()} disabled={isPending}
|
||||
className="flex items-center gap-1 border border-slate-700 text-slate-400 hover:text-slate-200 px-3 py-1.5 rounded text-xs">
|
||||
<RotateCcw className="w-3 h-3" /> Régénérer
|
||||
<RotateCcw className="w-3 h-3" /> Regenerate
|
||||
</button>
|
||||
)}
|
||||
<button onClick={onClose} className="border border-slate-700 text-slate-400 hover:text-slate-200 px-4 py-1.5 rounded text-xs">
|
||||
Fermer
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -431,18 +431,18 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
|
||||
{/* AI Suggest from context */}
|
||||
{aiStatus?.enabled && (
|
||||
<div className="card border-blue-500/30">
|
||||
<div className="section-title flex items-center gap-1"><Brain className="w-3 h-3 text-blue-400" /> Générer depuis un contexte (IA)</div>
|
||||
<div className="section-title flex items-center gap-1"><Brain className="w-3 h-3 text-blue-400" /> Generate from context (AI)</div>
|
||||
<div className="flex gap-2">
|
||||
<textarea
|
||||
value={suggestInput}
|
||||
onChange={e => setSuggestInput(e.target.value)}
|
||||
placeholder="Décris le contexte géopolitique... ex: 'Quand la Chine impose des restrictions sur les terres rares, que se passe-t-il sur le marché des semi-conducteurs et des métaux ?'"
|
||||
placeholder="Describe the geopolitical context... e.g. 'When China imposes restrictions on rare earths, what happens to semiconductor and metals markets?'"
|
||||
rows={2}
|
||||
className="flex-1 bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500 resize-none"
|
||||
/>
|
||||
<button onClick={suggestFromContext} disabled={suggestLoading || !suggestInput}
|
||||
className="shrink-0 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-3 rounded text-xs">
|
||||
{suggestLoading ? '...' : '⚡ Générer'}
|
||||
{suggestLoading ? '...' : '⚡ Generate'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -451,22 +451,22 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
|
||||
{/* Form fields */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="col-span-2">
|
||||
<label className="text-xs text-slate-500 mb-1 block">Nom du pattern</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Pattern name</label>
|
||||
<input value={form.name} onChange={e => set('name', e.target.value)}
|
||||
placeholder="Ex: Chine tensions Taiwan → Semi-conducteurs spike"
|
||||
placeholder="E.g.: China Taiwan tensions → Semiconductors spike"
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500" />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="text-xs text-slate-500 mb-1 block">Description</label>
|
||||
<textarea value={form.description} onChange={e => set('description', e.target.value)}
|
||||
rows={2} placeholder="Mécanisme géopolitique → impact marché..."
|
||||
rows={2} placeholder="Geopolitical mechanism → market impact..."
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500 resize-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Triggers */}
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-2 block">Déclencheurs</label>
|
||||
<label className="text-xs text-slate-500 mb-2 block">Triggers</label>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{TRIGGERS.map(t => (
|
||||
<button key={t} onClick={() => toggleTrigger(t)}
|
||||
@@ -482,11 +482,11 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
|
||||
|
||||
{/* Keywords */}
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Mots-clés de détection (anglais)</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Detection keywords (English)</label>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<input value={kwInput} onChange={e => setKwInput(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && addKeyword()}
|
||||
placeholder="Ajouter un mot-clé..."
|
||||
placeholder="Add a keyword..."
|
||||
className="flex-1 bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500" />
|
||||
<button onClick={addKeyword} className="bg-dark-600 border border-slate-700 hover:border-slate-500 text-slate-300 px-3 rounded text-xs">+</button>
|
||||
</div>
|
||||
@@ -504,48 +504,48 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
|
||||
{/* Metrics */}
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Classe d'actif</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Asset class</label>
|
||||
<select value={form.asset_class} onChange={e => set('asset_class', e.target.value)}
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500">
|
||||
{ASSET_CLASSES.map(a => <option key={a}>{a}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Move attendu (%)</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Expected move (%)</label>
|
||||
<input type="number" value={form.expected_move_pct} onChange={e => set('expected_move_pct', Number(e.target.value))}
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Probabilité (0-1)</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Probability (0-1)</label>
|
||||
<input type="number" step="0.05" min="0" max="1" value={form.probability} onChange={e => set('probability', Number(e.target.value))}
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Horizon (jours)</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Horizon (days)</label>
|
||||
<input type="number" value={form.horizon_days} onChange={e => set('horizon_days', Number(e.target.value))}
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contre-thèse & Invalidation */}
|
||||
{/* Counter-thesis & Invalidation */}
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block flex items-center gap-1">
|
||||
<ShieldAlert className="w-3 h-3 text-red-400" /> Contre-thèse (scénario adverse principal)
|
||||
<ShieldAlert className="w-3 h-3 text-red-400" /> Counter-thesis (main adverse scenario)
|
||||
</label>
|
||||
<textarea value={form.counter_thesis || ''} onChange={e => set('counter_thesis', e.target.value)}
|
||||
rows={2} placeholder="Ex: accord de paix inattendu, données inflation sous 3%, pivot Fed…"
|
||||
rows={2} placeholder="E.g.: unexpected peace deal, inflation below 3%, Fed pivot…"
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-red-500/50 resize-none" />
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="col-span-2">
|
||||
<label className="text-xs text-slate-500 mb-1 block">Trigger d'invalidation (événement précis & mesurable)</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Invalidation trigger (precise & measurable event)</label>
|
||||
<input value={form.invalidation_trigger || ''} onChange={e => set('invalidation_trigger', e.target.value)}
|
||||
placeholder="Ex: prix pétrole < 70$/b 3j consécutifs"
|
||||
placeholder="E.g.: oil price < $70/b for 3 consecutive days"
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-red-500/50" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Prob. invalidation (0-1)</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Invalidation prob. (0-1)</label>
|
||||
<input type="number" step="0.05" min="0" max="1" value={form.invalidation_probability ?? ''}
|
||||
onChange={e => set('invalidation_probability', e.target.value ? Number(e.target.value) : undefined)}
|
||||
placeholder="0.20"
|
||||
@@ -559,19 +559,19 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
|
||||
<div className="card border-blue-700/40">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-sm font-semibold text-blue-400 flex items-center gap-1">
|
||||
<Brain className="w-4 h-4" /> Évaluation IA
|
||||
<Brain className="w-4 h-4" /> AI Evaluation
|
||||
</div>
|
||||
<QualityBadge score={aiResult.quality_score || 0} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 text-xs">
|
||||
<div>
|
||||
<div className="text-slate-500 mb-1 font-semibold">✓ Points forts</div>
|
||||
<div className="text-slate-500 mb-1 font-semibold">✓ Strengths</div>
|
||||
{aiResult.strengths?.map((s: string, i: number) => (
|
||||
<div key={i} className="text-emerald-400 mb-0.5">• {s}</div>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-slate-500 mb-1 font-semibold">⚠ Faiblesses</div>
|
||||
<div className="text-slate-500 mb-1 font-semibold">⚠ Weaknesses</div>
|
||||
{aiResult.weaknesses?.map((w: string, i: number) => (
|
||||
<div key={i} className="text-orange-400 mb-0.5">• {w}</div>
|
||||
))}
|
||||
@@ -579,7 +579,7 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
|
||||
</div>
|
||||
{aiResult.suggested_improvements?.additional_keywords?.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<div className="text-xs text-slate-500 mb-1">Mots-clés suggérés par l'IA</div>
|
||||
<div className="text-xs text-slate-500 mb-1">Keywords suggested by AI</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{aiResult.suggested_improvements.additional_keywords.map((kw: string) => (
|
||||
<button key={kw} onClick={() => set('keywords', [...form.keywords, kw])}
|
||||
@@ -597,7 +597,7 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
|
||||
)}
|
||||
{aiResult.counter_scenarios?.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<div className="text-xs text-slate-500 mb-1">Scénarios d'invalidation</div>
|
||||
<div className="text-xs text-slate-500 mb-1">Invalidation scenarios</div>
|
||||
{aiResult.counter_scenarios.map((s: string, i: number) => (
|
||||
<div key={i} className="text-xs text-red-400 mb-0.5">✗ {s}</div>
|
||||
))}
|
||||
@@ -611,15 +611,15 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
|
||||
<button onClick={evaluateWithAI} disabled={aiLoading || !form.name}
|
||||
className="flex items-center gap-1.5 border border-blue-500/50 text-blue-400 hover:bg-blue-900/20 px-3 py-1.5 rounded text-sm transition-colors disabled:opacity-40">
|
||||
<Brain className="w-3.5 h-3.5" />
|
||||
{aiLoading ? 'Évaluation IA...' : 'Évaluer avec IA'}
|
||||
{aiLoading ? 'AI Evaluation...' : 'Evaluate with AI'}
|
||||
</button>
|
||||
)}
|
||||
<button onClick={onCancel} className="flex items-center gap-1.5 border border-slate-700 text-slate-400 hover:text-slate-200 px-3 py-1.5 rounded text-sm">
|
||||
<RotateCcw className="w-3.5 h-3.5" /> Annuler
|
||||
<RotateCcw className="w-3.5 h-3.5" /> Cancel
|
||||
</button>
|
||||
<button onClick={() => onSave(form)} disabled={!form.name || form.triggers.length === 0}
|
||||
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-1.5 rounded text-sm font-semibold ml-auto">
|
||||
<Save className="w-3.5 h-3.5" /> Sauvegarder le pattern
|
||||
<Save className="w-3.5 h-3.5" /> Save pattern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -725,23 +725,23 @@ export default function PatternEditor() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-white flex items-center gap-2">
|
||||
<Zap className="w-5 h-5 text-blue-400" /> Patterns Géopolitiques
|
||||
<Zap className="w-5 h-5 text-blue-400" /> Geopolitical Patterns
|
||||
</h1>
|
||||
<p className="text-xs text-slate-500 mt-0.5">
|
||||
{patterns?.length ?? 0} patterns ·{' '}
|
||||
{patterns?.filter((p: any) => p.source === 'custom').length ?? 0} personnalisés
|
||||
{patterns?.filter((p: any) => p.source === 'custom').length ?? 0} custom
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{aiStatus?.enabled && (
|
||||
<button onClick={() => setShowAiSuggest(true)}
|
||||
className="flex items-center gap-1.5 border border-blue-500/50 text-blue-400 hover:bg-blue-900/20 px-3 py-1.5 rounded text-sm font-semibold transition-colors">
|
||||
<Sparkles className="w-4 h-4" /> Suggérer par l'IA
|
||||
<Sparkles className="w-4 h-4" /> Suggest with AI
|
||||
</button>
|
||||
)}
|
||||
<button onClick={() => { setCreating(true); setEditing(null) }}
|
||||
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 text-white px-3 py-1.5 rounded text-sm font-semibold">
|
||||
<Plus className="w-4 h-4" /> Créer un pattern
|
||||
<Plus className="w-4 h-4" /> Create pattern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -749,7 +749,7 @@ export default function PatternEditor() {
|
||||
{(creating || editing) && (
|
||||
<div className="card border-blue-500/30">
|
||||
<div className="text-sm font-semibold text-white mb-4">
|
||||
{editing ? `Modifier: ${editing.name}` : 'Nouveau pattern géopolitique'}
|
||||
{editing ? `Edit: ${editing.name}` : 'New geopolitical pattern'}
|
||||
</div>
|
||||
<PatternForm
|
||||
initial={editing}
|
||||
@@ -763,9 +763,9 @@ export default function PatternEditor() {
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex gap-1 bg-dark-700 p-1 rounded">
|
||||
{[
|
||||
{ key: 'all', label: `Tous (${patterns?.length ?? 0})` },
|
||||
{ key: 'builtin', label: `Intégrés (${patterns?.filter((p: any) => p.source === 'builtin').length ?? 0})` },
|
||||
{ key: 'custom', label: `Mes patterns (${patterns?.filter((p: any) => p.source === 'custom').length ?? 0})` },
|
||||
{ key: 'all', label: `All (${patterns?.length ?? 0})` },
|
||||
{ key: 'builtin', label: `Built-in (${patterns?.filter((p: any) => p.source === 'builtin').length ?? 0})` },
|
||||
{ key: 'custom', label: `My patterns (${patterns?.filter((p: any) => p.source === 'custom').length ?? 0})` },
|
||||
].map(({ key, label }) => (
|
||||
<button key={key} onClick={() => setTab(key as any)}
|
||||
className={clsx('px-3 py-1.5 rounded text-sm', {
|
||||
@@ -799,9 +799,9 @@ export default function PatternEditor() {
|
||||
{/* Period filter */}
|
||||
<div className="flex gap-1 bg-dark-700 p-1 rounded">
|
||||
{([
|
||||
{ key: null, label: 'Tout' },
|
||||
{ key: 7, label: '7j' },
|
||||
{ key: 30, label: '30j' },
|
||||
{ key: null, label: 'All' },
|
||||
{ key: 7, label: '7d' },
|
||||
{ key: 30, label: '30d' },
|
||||
] as const).map(({ key, label }) => (
|
||||
<button key={String(key)} onClick={() => setFilterDays(key)}
|
||||
className={clsx('px-2 py-1 rounded text-xs transition-colors', {
|
||||
@@ -834,7 +834,7 @@ export default function PatternEditor() {
|
||||
'bg-slate-600/40 border-slate-500/60 text-white': filterAsset === 'all',
|
||||
'border-slate-700/40 text-slate-500 hover:border-slate-500 hover:text-slate-300': filterAsset !== 'all',
|
||||
})}>
|
||||
Tous types
|
||||
All types
|
||||
</button>
|
||||
{ASSET_CLASSES.map(ac => (
|
||||
<button key={ac} onClick={() => setFilterAsset(filterAsset === ac ? 'all' : ac)}
|
||||
@@ -847,7 +847,7 @@ export default function PatternEditor() {
|
||||
))}
|
||||
{(filterAsset !== 'all' || filterDays !== null) && (
|
||||
<span className="text-xs text-slate-600 ml-1">
|
||||
{displayPatterns.length} résultat{displayPatterns.length !== 1 ? 's' : ''}
|
||||
{displayPatterns.length} result{displayPatterns.length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -874,12 +874,12 @@ export default function PatternEditor() {
|
||||
<Zap className="w-8 h-8 mx-auto mb-2 opacity-20" />
|
||||
<div>
|
||||
{filterAsset !== 'all'
|
||||
? `Aucun pattern dans "${ASSET_CLASS_LABELS[filterAsset]}"`
|
||||
? `No patterns in "${ASSET_CLASS_LABELS[filterAsset]}"`
|
||||
: filterDays !== null
|
||||
? `Aucun pattern dans les ${filterDays} derniers jours`
|
||||
: 'Aucun pattern personnalisé'}
|
||||
? `No patterns in the last ${filterDays} days`
|
||||
: 'No custom patterns'}
|
||||
</div>
|
||||
<div className="text-xs mt-1">Créer un pattern avec l'aide de l'IA</div>
|
||||
<div className="text-xs mt-1">Create a pattern with AI assistance</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -86,7 +86,7 @@ function AddPositionModal({ prefill, onClose }: AddModalProps) {
|
||||
onSuccess: onClose,
|
||||
onError: (err: unknown) => {
|
||||
const detail = (err as { response?: { data?: { detail?: string } } })?.response?.data?.detail
|
||||
setAddError(detail || 'Erreur lors de l\'ajout de la position')
|
||||
setAddError(detail || 'Error adding position')
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -96,38 +96,38 @@ function AddPositionModal({ prefill, onClose }: AddModalProps) {
|
||||
<div className="card w-full max-w-lg max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-base font-bold text-white flex items-center gap-2">
|
||||
<Plus className="w-4 h-4 text-blue-400" /> Ajouter au portefeuille
|
||||
<Plus className="w-4 h-4 text-blue-400" /> Add to portfolio
|
||||
</h2>
|
||||
<button onClick={onClose} className="text-slate-500 hover:text-slate-200"><X className="w-4 h-4" /></button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Titre</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Title</label>
|
||||
<input value={form.title} onChange={e => set('title', e.target.value)}
|
||||
placeholder="Ex: Long Call GLD Q3 2026"
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">
|
||||
Sous-jacent <span className="text-slate-600">(ticker Yahoo Finance)</span>
|
||||
Underlying <span className="text-slate-600">(Yahoo Finance ticker)</span>
|
||||
</label>
|
||||
<input value={form.underlying} onChange={e => { set('underlying', e.target.value.toUpperCase()); setAddError(null) }}
|
||||
placeholder="^GSPC, GLD, CL=F..."
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500" />
|
||||
<div className="text-xs text-slate-600 mt-0.5">S&P 500: ^GSPC · Or: GC=F · WTI: CL=F · GLD · USO</div>
|
||||
<div className="text-xs text-slate-600 mt-0.5">S&P 500: ^GSPC · Gold: GC=F · WTI: CL=F · GLD · USO</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Stratégie</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Strategy</label>
|
||||
<select value={form.strategy} onChange={e => set('strategy', e.target.value)}
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500">
|
||||
{STRATEGIES.map(s => <option key={s}>{s}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Classe d'actif</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Asset class</label>
|
||||
<select value={form.asset_class} onChange={e => set('asset_class', e.target.value)}
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500">
|
||||
{ASSET_CLASSES.map(s => <option key={s}>{s}</option>)}
|
||||
@@ -138,7 +138,7 @@ function AddPositionModal({ prefill, onClose }: AddModalProps) {
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Strike</label>
|
||||
<input type="number" value={form.strike} onChange={e => set('strike', e.target.value)}
|
||||
placeholder="Prix d'exercice"
|
||||
placeholder="Exercise price"
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
@@ -150,45 +150,45 @@ function AddPositionModal({ prefill, onClose }: AddModalProps) {
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Prime payée</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Premium paid</label>
|
||||
<input type="number" value={form.premium} onChange={e => set('premium', e.target.value)}
|
||||
placeholder="Prix option"
|
||||
placeholder="Option price"
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Capital investi (€)</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Capital invested (€)</label>
|
||||
<input type="number" value={form.capital_invested} onChange={e => set('capital_invested', Number(e.target.value))}
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Durée (jours)</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Duration (days)</label>
|
||||
<input type="number" value={form.expiry_days} onChange={e => set('expiry_days', Number(e.target.value))}
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Déclencheur géopolitique</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Geopolitical trigger</label>
|
||||
<input value={form.geo_trigger} onChange={e => set('geo_trigger', e.target.value)}
|
||||
placeholder="Ex: Middle East escalation → oil spike"
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Raisonnement</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Rationale</label>
|
||||
<textarea value={form.rationale} onChange={e => set('rationale', e.target.value)}
|
||||
rows={2} placeholder="Pourquoi ce trade..."
|
||||
rows={2} placeholder="Why this trade..."
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500 resize-none" />
|
||||
</div>
|
||||
{isStraddle && (
|
||||
<div className="bg-blue-900/30 border border-blue-700/30 rounded p-2 text-xs text-blue-300">
|
||||
Straddle : 2 legs seront créés automatiquement (CALL + PUT au même strike)
|
||||
Straddle: 2 legs will be created automatically (CALL + PUT at the same strike)
|
||||
</div>
|
||||
)}
|
||||
<div className="bg-dark-700 rounded p-2 text-xs text-slate-500">
|
||||
<div className="font-semibold text-slate-400 mb-1">Frais IB simulés</div>
|
||||
<div>Options: $0.65/contrat (min $1.00) à l'entrée + sortie</div>
|
||||
<div>1 contrat = <span className="text-white">$0.65</span> · 5 contrats = <span className="text-white">$3.25</span></div>
|
||||
<div className="font-semibold text-slate-400 mb-1">Simulated IB fees</div>
|
||||
<div>Options: $0.65/contract (min $1.00) at entry + exit</div>
|
||||
<div>1 contract = <span className="text-white">$0.65</span> · 5 contracts = <span className="text-white">$3.25</span></div>
|
||||
</div>
|
||||
{addError && (
|
||||
<div className="bg-red-900/40 border border-red-700/40 rounded p-2 text-xs text-red-300">
|
||||
@@ -197,7 +197,7 @@ function AddPositionModal({ prefill, onClose }: AddModalProps) {
|
||||
)}
|
||||
<button onClick={submit} disabled={isPending || !form.underlying}
|
||||
className="w-full bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white rounded py-2 text-sm font-semibold">
|
||||
{isPending ? 'Ajout...' : '+ Ajouter au portefeuille'}
|
||||
{isPending ? 'Adding...' : '+ Add to portfolio'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -233,7 +233,7 @@ function PositionCard({ pos }: { pos: Record<string, any> }) {
|
||||
<button
|
||||
onClick={() => navigate(`/markets?symbol=${pos.underlying}`)}
|
||||
className="text-xs text-blue-400 hover:text-blue-300 flex items-center gap-0.5 transition-colors"
|
||||
title="Voir dans les marchés"
|
||||
title="View in markets"
|
||||
>
|
||||
{pos.underlying} <ExternalLink className="w-2.5 h-2.5" />
|
||||
</button>
|
||||
@@ -253,7 +253,7 @@ function PositionCard({ pos }: { pos: Record<string, any> }) {
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={() => deletePos(pos.id)} disabled={deleting}
|
||||
className="text-slate-600 hover:text-red-400 transition-colors" title="Supprimer">
|
||||
className="text-slate-600 hover:text-red-400 transition-colors" title="Delete">
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -262,29 +262,29 @@ function PositionCard({ pos }: { pos: Record<string, any> }) {
|
||||
{/* KPI grid */}
|
||||
<div className="grid grid-cols-4 gap-2 mb-3">
|
||||
<div className="card-sm text-center">
|
||||
<div className="text-xs text-slate-600">{hasPremium ? 'Prime payée' : 'Capital investi'}</div>
|
||||
<div className="text-xs text-slate-600">{hasPremium ? 'Premium paid' : 'Capital invested'}</div>
|
||||
<div className="text-xs text-white font-mono mt-0.5">{entryRef.toFixed(2)}€</div>
|
||||
</div>
|
||||
<div className="card-sm text-center">
|
||||
<div className="text-xs text-slate-600">Valeur BS actuelle</div>
|
||||
<div className="text-xs text-slate-600">Current BS value</div>
|
||||
<div className="text-xs text-white font-mono mt-0.5">
|
||||
{pos.current_value != null ? `${pos.current_value.toFixed(2)}€` : '—'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-sm text-center">
|
||||
<div className="text-xs text-slate-600">Spot sous-jacent</div>
|
||||
<div className="text-xs text-slate-600">Underlying spot</div>
|
||||
<div className="text-xs text-white font-mono mt-0.5">
|
||||
{pos.current_underlying != null ? `$${pos.current_underlying}` : '—'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-sm text-center">
|
||||
<div className="text-xs text-slate-600">Jours restants</div>
|
||||
<div className="text-xs text-slate-600">Days remaining</div>
|
||||
<div className={clsx('text-xs font-mono mt-0.5', {
|
||||
'text-red-400': (pos.days_remaining ?? 99) < 14,
|
||||
'text-yellow-400': (pos.days_remaining ?? 99) < 30,
|
||||
'text-white': (pos.days_remaining ?? 99) >= 30,
|
||||
})}>
|
||||
{pos.days_remaining != null ? `${pos.days_remaining}j` : '—'}
|
||||
{pos.days_remaining != null ? `${pos.days_remaining}d` : '—'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -294,9 +294,9 @@ function PositionCard({ pos }: { pos: Record<string, any> }) {
|
||||
<div className="flex items-center gap-4 text-xs mb-2">
|
||||
<span className="text-slate-600">Greeks:</span>
|
||||
<span>Δ <span className="text-slate-300 font-mono">{pos.greeks.net_delta?.toFixed(3)}</span></span>
|
||||
<span>Θ <span className="text-red-400 font-mono">{pos.greeks.net_theta?.toFixed(3)}</span>/j</span>
|
||||
<span>Θ <span className="text-red-400 font-mono">{pos.greeks.net_theta?.toFixed(3)}</span>/d</span>
|
||||
<span>ν <span className="text-blue-400 font-mono">{pos.greeks.net_vega?.toFixed(3)}</span></span>
|
||||
<span className="ml-auto text-slate-600">Frais IB: <span className="text-orange-400">{(pos.ib_fees_entry || 0).toFixed(2)}€</span></span>
|
||||
<span className="ml-auto text-slate-600">IB fees: <span className="text-orange-400">{(pos.ib_fees_entry || 0).toFixed(2)}€</span></span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -307,7 +307,7 @@ function PositionCard({ pos }: { pos: Record<string, any> }) {
|
||||
className="flex items-center gap-1 text-xs text-slate-600 hover:text-slate-400 mb-2 transition-colors"
|
||||
>
|
||||
{showDetails ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
|
||||
Détail de la simulation Black-Scholes
|
||||
Black-Scholes simulation detail
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -316,14 +316,14 @@ function PositionCard({ pos }: { pos: Record<string, any> }) {
|
||||
<div className="mb-3 bg-dark-700/40 rounded p-2.5 text-xs border border-slate-700/30">
|
||||
<div className="flex flex-wrap items-center gap-3 mb-2 text-slate-500 border-b border-slate-700/30 pb-1.5">
|
||||
{pos.entry_underlying_price != null && (
|
||||
<span>Spot entrée: <span className="text-slate-300 font-mono">${Number(pos.entry_underlying_price).toFixed(2)}</span></span>
|
||||
<span>Entry spot: <span className="text-slate-300 font-mono">${Number(pos.entry_underlying_price).toFixed(2)}</span></span>
|
||||
)}
|
||||
{pos.sigma_used != null && (
|
||||
<span>σ (IV histo): <span className="text-slate-300 font-mono">{(pos.sigma_used * 100).toFixed(1)}%</span></span>
|
||||
<span>σ (hist. IV): <span className="text-slate-300 font-mono">{(pos.sigma_used * 100).toFixed(1)}%</span></span>
|
||||
)}
|
||||
<span>r: <span className="text-slate-300">5%</span></span>
|
||||
{pos.expiry_days != null && (
|
||||
<span>Durée initiale: <span className="text-slate-300">{pos.expiry_days}j</span></span>
|
||||
<span>Initial duration: <span className="text-slate-300">{pos.expiry_days}d</span></span>
|
||||
)}
|
||||
</div>
|
||||
<table className="w-full mb-2">
|
||||
@@ -331,9 +331,9 @@ function PositionCard({ pos }: { pos: Record<string, any> }) {
|
||||
<tr className="text-slate-600">
|
||||
<th className="text-left pb-1 font-normal">Leg</th>
|
||||
<th className="text-left pb-1 font-normal">Strike</th>
|
||||
<th className="text-right pb-1 font-normal">Qté</th>
|
||||
<th className="text-right pb-1 font-normal">Prime/contrat</th>
|
||||
<th className="text-right pb-1 font-normal">Coût total</th>
|
||||
<th className="text-right pb-1 font-normal">Qty</th>
|
||||
<th className="text-right pb-1 font-normal">Premium/contract</th>
|
||||
<th className="text-right pb-1 font-normal">Total cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -370,7 +370,7 @@ function PositionCard({ pos }: { pos: Record<string, any> }) {
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="text-slate-600 italic">
|
||||
Black-Scholes · 1 contrat = 100 actions · Prix calculé au moment de l'ajout (spot, IV historique, r=5%)
|
||||
Black-Scholes · 1 contract = 100 shares · Price computed at entry time (spot, historical IV, r=5%)
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -403,29 +403,29 @@ function PositionCard({ pos }: { pos: Record<string, any> }) {
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => setShowClose(!showClose)}
|
||||
className="flex-1 text-xs border border-slate-700 hover:border-emerald-600 text-slate-400 hover:text-emerald-400 rounded py-1.5 transition-colors">
|
||||
{showClose ? 'Annuler' : '💰 Clôturer la position'}
|
||||
{showClose ? 'Cancel' : '💰 Close position'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showClose && (
|
||||
<div className="mt-2 space-y-1">
|
||||
<div className="text-xs text-slate-500">Valeur de revente (€) — ex: {pos.current_value?.toFixed(2)}</div>
|
||||
<div className="text-xs text-slate-500">Resale value (€) — e.g.: {pos.current_value?.toFixed(2)}</div>
|
||||
<div className="flex gap-2">
|
||||
<input type="number" value={closeVal} onChange={e => setCloseVal(e.target.value)}
|
||||
placeholder={`Valeur actuelle: ~${pos.current_value?.toFixed(2)}€`}
|
||||
placeholder={`Current value: ~${pos.current_value?.toFixed(2)}€`}
|
||||
className="flex-1 bg-dark-700 border border-slate-700 rounded px-2 py-1 text-sm text-white focus:outline-none focus:border-blue-500" />
|
||||
<button
|
||||
onClick={() => closePos({ id: pos.id, close_value: parseFloat(closeVal) || 0 },
|
||||
{ onSuccess: () => setShowClose(false) })}
|
||||
disabled={closing || !closeVal}
|
||||
className="bg-emerald-700 hover:bg-emerald-600 disabled:opacity-40 text-white px-3 rounded text-xs font-semibold">
|
||||
{closing ? '...' : 'Confirmer'}
|
||||
{closing ? '...' : 'Confirm'}
|
||||
</button>
|
||||
</div>
|
||||
{closeVal && (
|
||||
<div className={clsx('text-xs', parseFloat(closeVal) - entryRef - (pos.ib_fees_entry||0) - 1 >= 0 ? 'positive' : 'negative')}>
|
||||
P&L net IB: {(parseFloat(closeVal) - entryRef - (pos.ib_fees_entry||0) - 1).toFixed(2)}€
|
||||
(frais sortie: ~$1.00)
|
||||
Net IB P&L: {(parseFloat(closeVal) - entryRef - (pos.ib_fees_entry||0) - 1).toFixed(2)}€
|
||||
(exit fees: ~$1.00)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -449,7 +449,7 @@ function ClosedRow({ pos, fees, pnl }: { pos: Record<string, any>; fees: number;
|
||||
<td className="py-1.5 text-slate-500">{pos.close_date?.slice(0, 10)}</td>
|
||||
<td className="py-1.5 pl-2">
|
||||
<button onClick={() => deletePos(pos.id)} disabled={isPending}
|
||||
className="opacity-0 group-hover:opacity-100 text-slate-600 hover:text-red-400 transition-all" title="Supprimer">
|
||||
className="opacity-0 group-hover:opacity-100 text-slate-600 hover:text-red-400 transition-all" title="Delete">
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</td>
|
||||
@@ -470,19 +470,19 @@ export default function Portfolio() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-white flex items-center gap-2">
|
||||
<DollarSign className="w-5 h-5 text-blue-400" /> Portefeuille
|
||||
<DollarSign className="w-5 h-5 text-blue-400" /> Portfolio
|
||||
</h1>
|
||||
<p className="text-xs text-slate-500 mt-0.5">
|
||||
Suivi temps réel · Mark-to-market Black-Scholes · Frais IB simulés
|
||||
Real-time tracking · Mark-to-market Black-Scholes · Simulated IB fees
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => refetch()} className="flex items-center gap-1 text-xs text-slate-400 hover:text-slate-200">
|
||||
<RefreshCw className="w-3.5 h-3.5" /> Actualiser
|
||||
<RefreshCw className="w-3.5 h-3.5" /> Refresh
|
||||
</button>
|
||||
<button onClick={() => setShowModal(true)}
|
||||
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 text-white px-3 py-1.5 rounded text-sm font-semibold">
|
||||
<Plus className="w-4 h-4" /> Nouvelle position
|
||||
<Plus className="w-4 h-4" /> New position
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -491,11 +491,11 @@ export default function Portfolio() {
|
||||
{summary && (
|
||||
<div className="grid grid-cols-5 gap-3">
|
||||
{[
|
||||
{ label: 'Positions ouvertes', value: summary.open_positions, color: 'text-white' },
|
||||
{ label: 'Capital engagé', value: `${summary.total_invested}€`, color: 'text-white' },
|
||||
{ label: 'P&L latent', value: `${summary.unrealized_pnl >= 0 ? '+' : ''}${summary.unrealized_pnl}€`, color: summary.unrealized_pnl >= 0 ? 'text-emerald-400' : 'text-red-400' },
|
||||
{ label: 'P&L réalisé', value: `${summary.realized_pnl >= 0 ? '+' : ''}${summary.realized_pnl}€`, color: summary.realized_pnl >= 0 ? 'text-emerald-400' : 'text-red-400' },
|
||||
{ label: 'Frais IB total', value: `${summary.total_fees_paid}€`, color: 'text-orange-400' },
|
||||
{ label: 'Open positions', value: summary.open_positions, color: 'text-white' },
|
||||
{ label: 'Capital deployed', value: `${summary.total_invested}€`, color: 'text-white' },
|
||||
{ label: 'Unrealized P&L', value: `${summary.unrealized_pnl >= 0 ? '+' : ''}${summary.unrealized_pnl}€`, color: summary.unrealized_pnl >= 0 ? 'text-emerald-400' : 'text-red-400' },
|
||||
{ label: 'Realized P&L', value: `${summary.realized_pnl >= 0 ? '+' : ''}${summary.realized_pnl}€`, color: summary.realized_pnl >= 0 ? 'text-emerald-400' : 'text-red-400' },
|
||||
{ label: 'Total IB fees', value: `${summary.total_fees_paid}€`, color: 'text-orange-400' },
|
||||
].map(({ label, value, color }) => (
|
||||
<div key={label} className="card-sm text-center">
|
||||
<div className="stat-label">{label}</div>
|
||||
@@ -508,9 +508,9 @@ export default function Portfolio() {
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-1 bg-dark-700 p-1 rounded w-fit">
|
||||
{[
|
||||
{ key: 'open', label: `Positions ouvertes (${positions?.length ?? 0})` },
|
||||
{ key: 'closed', label: `Clôturées (${closed?.length ?? 0})` },
|
||||
{ key: 'history', label: 'Courbe P&L' },
|
||||
{ key: 'open', label: `Open positions (${positions?.length ?? 0})` },
|
||||
{ key: 'closed', label: `Closed (${closed?.length ?? 0})` },
|
||||
{ key: 'history', label: 'P&L curve' },
|
||||
].map(({ key, label }) => (
|
||||
<button key={key} onClick={() => setTab(key as any)}
|
||||
className={clsx('px-3 py-1.5 rounded text-sm transition-colors', {
|
||||
@@ -538,8 +538,8 @@ export default function Portfolio() {
|
||||
) : (
|
||||
<div className="card text-center py-12 text-slate-500">
|
||||
<DollarSign className="w-10 h-10 mx-auto mb-3 opacity-20" />
|
||||
<div>Aucune position ouverte</div>
|
||||
<div className="text-xs mt-1">Ajouter un trade depuis les idées ou manuellement</div>
|
||||
<div>No open positions</div>
|
||||
<div className="text-xs mt-1">Add a trade from ideas or manually</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -552,12 +552,12 @@ export default function Portfolio() {
|
||||
<thead>
|
||||
<tr className="text-slate-600 border-b border-slate-700/40">
|
||||
<th className="text-left pb-2">Position</th>
|
||||
<th className="text-left pb-2">Stratégie</th>
|
||||
<th className="text-right pb-2">Investi</th>
|
||||
<th className="text-right pb-2">Clôture</th>
|
||||
<th className="text-right pb-2">Frais IB</th>
|
||||
<th className="text-right pb-2">P&L net</th>
|
||||
<th className="text-left pb-2">Date clôture</th>
|
||||
<th className="text-left pb-2">Strategy</th>
|
||||
<th className="text-right pb-2">Invested</th>
|
||||
<th className="text-right pb-2">Close</th>
|
||||
<th className="text-right pb-2">IB fees</th>
|
||||
<th className="text-right pb-2">Net P&L</th>
|
||||
<th className="text-left pb-2">Close date</th>
|
||||
<th className="pb-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -570,7 +570,7 @@ export default function Portfolio() {
|
||||
)
|
||||
})}
|
||||
{(!closed || closed.length === 0) && (
|
||||
<tr><td colSpan={8} className="py-8 text-center text-slate-600">Aucun trade clôturé</td></tr>
|
||||
<tr><td colSpan={8} className="py-8 text-center text-slate-600">No closed trades</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -580,7 +580,7 @@ export default function Portfolio() {
|
||||
{/* P&L history */}
|
||||
{tab === 'history' && (
|
||||
<div className="card">
|
||||
<div className="section-title">Courbe de P&L cumulé (trades réalisés)</div>
|
||||
<div className="section-title">Cumulative P&L curve (realized trades)</div>
|
||||
{pnlHistory && pnlHistory.length > 0 ? (
|
||||
<>
|
||||
<ResponsiveContainer width="100%" height={250}>
|
||||
@@ -596,7 +596,7 @@ export default function Portfolio() {
|
||||
<YAxis tick={{ fill: '#475569', fontSize: 9 }} tickLine={false} axisLine={false}
|
||||
tickFormatter={v => `${v.toFixed(0)}€`} />
|
||||
<Tooltip contentStyle={{ background: '#0f1623', border: '1px solid #1e2d4d', fontSize: 11 }}
|
||||
formatter={(v: number) => [`${v.toFixed(2)}€`, 'P&L cumulé']} />
|
||||
formatter={(v: number) => [`${v.toFixed(2)}€`, 'Cumulative P&L']} />
|
||||
<ReferenceLine y={0} stroke="#475569" strokeDasharray="4 4" />
|
||||
<Area type="monotone" dataKey="cumulative" stroke="#10b981" fill="url(#pnl-grad)" strokeWidth={2} dot={false} />
|
||||
</AreaChart>
|
||||
@@ -615,7 +615,7 @@ export default function Portfolio() {
|
||||
</>
|
||||
) : (
|
||||
<div className="h-40 flex items-center justify-center text-slate-600 text-sm">
|
||||
Clôturer des positions pour voir la courbe P&L
|
||||
Close positions to see the P&L curve
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -140,13 +140,13 @@ function DiffView({ diffData }: { diffData: any }) {
|
||||
{pd.positions_opened > 0 && (
|
||||
<div className="text-center">
|
||||
<div className="text-emerald-400 font-bold text-lg">+{pd.positions_opened}</div>
|
||||
<div className="text-[10px] text-slate-500">ouvertes</div>
|
||||
<div className="text-[10px] text-slate-500">opened</div>
|
||||
</div>
|
||||
)}
|
||||
{pd.positions_closed > 0 && (
|
||||
<div className="text-center">
|
||||
<div className="text-red-400 font-bold text-lg">-{pd.positions_closed}</div>
|
||||
<div className="text-[10px] text-slate-500">fermées</div>
|
||||
<div className="text-[10px] text-slate-500">closed</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -159,7 +159,7 @@ function DiffView({ diffData }: { diffData: any }) {
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Plus className="w-4 h-4 text-emerald-400" />
|
||||
<h3 className="text-sm font-semibold text-emerald-400">
|
||||
Nouvelles positions ({new_positions.length})
|
||||
New positions ({new_positions.length})
|
||||
</h3>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
@@ -170,7 +170,7 @@ function DiffView({ diffData }: { diffData: any }) {
|
||||
<span className="text-slate-400 ml-2">{p.strategy}</span>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<span className="text-slate-500">Entrée: {p.entry_price?.toFixed(2) ?? '—'}</span>
|
||||
<span className="text-slate-500">Entry: {p.entry_price?.toFixed(2) ?? '—'}</span>
|
||||
<span className={clsx('font-semibold', (p.pnl_pct ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{fmt(p.pnl_pct)}
|
||||
</span>
|
||||
@@ -187,7 +187,7 @@ function DiffView({ diffData }: { diffData: any }) {
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Minus className="w-4 h-4 text-red-400" />
|
||||
<h3 className="text-sm font-semibold text-red-400">
|
||||
Positions fermées ({closed_positions.length})
|
||||
Closed positions ({closed_positions.length})
|
||||
</h3>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
@@ -198,7 +198,7 @@ function DiffView({ diffData }: { diffData: any }) {
|
||||
<span className="text-slate-500 ml-2">{p.strategy}</span>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<span className="text-slate-500">Dernière val.: {fmt(p.pnl_pct)}</span>
|
||||
<span className="text-slate-500">Last val.: {fmt(p.pnl_pct)}</span>
|
||||
<span className={clsx('font-semibold', (p.pnl_pct ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{fmtEur(p.pnl_eur)}
|
||||
</span>
|
||||
@@ -213,7 +213,7 @@ function DiffView({ diffData }: { diffData: any }) {
|
||||
{changed_positions.length > 0 && (
|
||||
<div className="bg-dark-800 border border-slate-700/40 rounded-xl p-4">
|
||||
<h3 className="text-sm font-semibold text-white mb-3">
|
||||
Évolution des positions ({changed_positions.length})
|
||||
Position changes ({changed_positions.length})
|
||||
</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
@@ -265,7 +265,7 @@ function SnapshotDetail({ snapId }: { snapId: number }) {
|
||||
staleTime: 300_000,
|
||||
})
|
||||
|
||||
if (isLoading) return <div className="text-slate-600 text-sm p-4">Chargement…</div>
|
||||
if (isLoading) return <div className="text-slate-600 text-sm p-4">Loading…</div>
|
||||
if (!data) return null
|
||||
|
||||
const trades: any[] = data.trades_snapshot ?? []
|
||||
@@ -277,7 +277,7 @@ function SnapshotDetail({ snapId }: { snapId: number }) {
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-white">{fmtDate(data.snapped_at)} UTC</h3>
|
||||
{macro?.dominant && (
|
||||
<span className="text-xs text-slate-500 capitalize">Régime : {macro.dominant}</span>
|
||||
<span className="text-xs text-slate-500 capitalize">Regime: {macro.dominant}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-4 text-xs">
|
||||
@@ -294,7 +294,7 @@ function SnapshotDetail({ snapId }: { snapId: number }) {
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-500">{data.n_open} ouvertes · {data.n_closed} fermées</span>
|
||||
<span className="text-slate-500">{data.n_open} open · {data.n_closed} closed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -304,9 +304,9 @@ function SnapshotDetail({ snapId }: { snapId: number }) {
|
||||
<thead>
|
||||
<tr className="text-slate-500 border-b border-slate-700/40">
|
||||
<th className="text-left pb-2 font-medium">Ticker</th>
|
||||
<th className="text-left pb-2 font-medium">Stratégie</th>
|
||||
<th className="text-right pb-2 font-medium">Entrée</th>
|
||||
<th className="text-right pb-2 font-medium">Prix actuel</th>
|
||||
<th className="text-left pb-2 font-medium">Strategy</th>
|
||||
<th className="text-right pb-2 font-medium">Entry</th>
|
||||
<th className="text-right pb-2 font-medium">Current price</th>
|
||||
<th className="text-right pb-2 font-medium">PnL %</th>
|
||||
<th className="text-right pb-2 font-medium">PnL €</th>
|
||||
</tr>
|
||||
@@ -329,7 +329,7 @@ function SnapshotDetail({ snapId }: { snapId: number }) {
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<div className="text-slate-600 text-sm">Aucune position dans ce snapshot</div>
|
||||
<div className="text-slate-600 text-sm">No positions in this snapshot</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
@@ -397,11 +397,11 @@ export default function PositionHistory() {
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<History className="w-6 h-6 text-violet-400" />
|
||||
<h1 className="text-xl font-bold text-white">Historique des Positions</h1>
|
||||
<h1 className="text-xl font-bold text-white">Position History</h1>
|
||||
</div>
|
||||
<p className="text-slate-400 text-sm">
|
||||
{snapshots.length} snapshot{snapshots.length !== 1 ? 's' : ''} PnL enregistré{snapshots.length !== 1 ? 's' : ''}
|
||||
{snapshots.length > 0 && ` · de ${fmtDate(snapshots[snapshots.length - 1]?.snapped_at)} à ${fmtDate(snapshots[0]?.snapped_at)}`}
|
||||
{snapshots.length} PnL snapshot{snapshots.length !== 1 ? 's' : ''} recorded
|
||||
{snapshots.length > 0 && ` · from ${fmtDate(snapshots[snapshots.length - 1]?.snapped_at)} to ${fmtDate(snapshots[0]?.snapped_at)}`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
@@ -412,7 +412,7 @@ export default function PositionHistory() {
|
||||
className={clsx('px-3 py-1.5 rounded text-xs font-semibold transition-colors',
|
||||
viewMode === 'detail' ? 'bg-slate-600 text-white' : 'bg-dark-700 text-slate-400 hover:text-white')}
|
||||
>
|
||||
Détail
|
||||
Detail
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('diff')}
|
||||
@@ -428,12 +428,12 @@ export default function PositionHistory() {
|
||||
onClick={() => { setSelectedA(null); setSelectedB(null); setViewMode('detail') }}
|
||||
className="px-3 py-1.5 rounded text-xs text-slate-500 hover:text-slate-300 bg-dark-700"
|
||||
>
|
||||
Effacer sélection
|
||||
Clear selection
|
||||
</button>
|
||||
)}
|
||||
<button onClick={() => refetch()}
|
||||
className="flex items-center gap-1 px-3 py-1.5 rounded text-xs bg-dark-700 text-slate-400 hover:text-white">
|
||||
<RefreshCw className="w-3.5 h-3.5" /> Actualiser
|
||||
<RefreshCw className="w-3.5 h-3.5" /> Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -448,24 +448,24 @@ export default function PositionHistory() {
|
||||
{/* Selection banner */}
|
||||
{(selectedA || selectedB) && (
|
||||
<div className="flex items-center gap-3 bg-dark-800 border border-slate-700/40 rounded-xl px-4 py-3 text-xs flex-wrap">
|
||||
<span className="text-slate-500">Sélection diff :</span>
|
||||
<span className="text-slate-500">Diff selection:</span>
|
||||
{selectedA ? (
|
||||
<span className="flex items-center gap-1.5 bg-blue-900/30 text-blue-400 border border-blue-700/40 px-2.5 py-1 rounded">
|
||||
<span className="font-bold">A</span>
|
||||
{fmtDate(snapshots.find(s => s.id === selectedA)?.snapped_at ?? '')}
|
||||
</span>
|
||||
) : <span className="text-slate-600">A : non sélectionné</span>}
|
||||
) : <span className="text-slate-600">A: not selected</span>}
|
||||
<ArrowRight className="w-3.5 h-3.5 text-slate-600" />
|
||||
{selectedB ? (
|
||||
<span className="flex items-center gap-1.5 bg-violet-900/30 text-violet-400 border border-violet-700/40 px-2.5 py-1 rounded">
|
||||
<span className="font-bold">B</span>
|
||||
{fmtDate(snapshots.find(s => s.id === selectedB)?.snapped_at ?? '')}
|
||||
</span>
|
||||
) : <span className="text-slate-600">B : non sélectionné</span>}
|
||||
) : <span className="text-slate-600">B: not selected</span>}
|
||||
{canDiff && (
|
||||
<button onClick={() => setViewMode('diff')}
|
||||
className="ml-auto bg-violet-600 hover:bg-violet-500 text-white px-3 py-1 rounded text-xs font-semibold">
|
||||
Voir le diff
|
||||
View diff
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -475,10 +475,10 @@ export default function PositionHistory() {
|
||||
{!isLoading && snapshots.length === 0 && (
|
||||
<div className="text-center py-20">
|
||||
<History className="w-14 h-14 text-slate-700 mx-auto mb-4" />
|
||||
<div className="text-slate-400 font-semibold mb-2">Aucun snapshot PnL disponible</div>
|
||||
<div className="text-slate-400 font-semibold mb-2">No PnL snapshots available</div>
|
||||
<p className="text-slate-600 text-sm max-w-md mx-auto">
|
||||
Activez le Scheduler PnL dans la Configuration ou déclenchez un snapshot manuellement
|
||||
depuis la page Configuration → Auto-Cycle & Logging.
|
||||
Enable the PnL Scheduler in Configuration or trigger a snapshot manually
|
||||
from the Configuration → Auto-Cycle & Logging page.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -491,7 +491,7 @@ export default function PositionHistory() {
|
||||
{/* Mini sparkline */}
|
||||
{sparkData.length > 2 && (
|
||||
<div className="bg-dark-800 border border-slate-700/40 rounded-xl p-3">
|
||||
<div className="text-xs text-slate-500 mb-2">PnL portfolio dans le temps</div>
|
||||
<div className="text-xs text-slate-500 mb-2">Portfolio PnL over time</div>
|
||||
<ResponsiveContainer width="100%" height={80}>
|
||||
<LineChart data={sparkData} margin={{ top: 4, right: 4, left: -30, bottom: 0 }}>
|
||||
<XAxis dataKey="date" hide />
|
||||
@@ -507,7 +507,7 @@ export default function PositionHistory() {
|
||||
{/* Snapshot list */}
|
||||
<div className="bg-dark-800 border border-slate-700/40 rounded-xl p-3 space-y-1.5 max-h-[calc(100vh-320px)] overflow-y-auto">
|
||||
<div className="text-[10px] text-slate-600 uppercase tracking-wider px-1 mb-2">
|
||||
{snapshots.length} snapshots — cliquez pour détail · A/B pour diff
|
||||
{snapshots.length} snapshots — click for detail · A/B to diff
|
||||
</div>
|
||||
{snapshots.map(s => (
|
||||
<div key={s.id} onClick={() => { setActiveDetail(s.id); setViewMode('detail') }}>
|
||||
@@ -527,7 +527,7 @@ export default function PositionHistory() {
|
||||
<div className="lg:col-span-2">
|
||||
{viewMode === 'diff' && canDiff && (
|
||||
diffLoading
|
||||
? <div className="text-slate-500 text-sm p-4">Calcul du diff…</div>
|
||||
? <div className="text-slate-500 text-sm p-4">Computing diff…</div>
|
||||
: diffError
|
||||
? <div className="text-red-400 text-sm p-4">{String(diffError)}</div>
|
||||
: diffData ? <DiffView diffData={diffData} />
|
||||
@@ -542,8 +542,8 @@ export default function PositionHistory() {
|
||||
<div className="flex flex-col items-center justify-center py-20 text-center">
|
||||
<TrendingUp className="w-12 h-12 text-slate-700 mb-4" />
|
||||
<div className="text-slate-500 text-sm">
|
||||
Cliquez sur un snapshot pour voir le détail<br />
|
||||
ou sélectionnez A et B pour comparer deux valuations
|
||||
Click a snapshot to see the detail<br />
|
||||
or select A and B to compare two valuations
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -552,7 +552,7 @@ export default function PositionHistory() {
|
||||
<div className="flex flex-col items-center justify-center py-20 text-center">
|
||||
<TrendingDown className="w-12 h-12 text-slate-700 mb-4" />
|
||||
<div className="text-slate-500 text-sm">
|
||||
Sélectionnez deux snapshots (A et B) depuis la liste pour voir le diff
|
||||
Select two snapshots (A and B) from the list to view the diff
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -9,14 +9,14 @@ import {
|
||||
|
||||
const SCENARIO_META: Record<string, { label: string; color: string; emoji: string }> = {
|
||||
goldilocks: { label: 'Goldilocks', color: '#22c55e', emoji: '🌟' },
|
||||
desinflation: { label: 'Désinflation', color: '#06b6d4', emoji: '❄️' },
|
||||
desinflation: { label: 'Disinflation', color: '#06b6d4', emoji: '❄️' },
|
||||
soft_landing: { label: 'Soft Landing', color: '#3b82f6', emoji: '🛬' },
|
||||
reflation: { label: 'Reflation', color: '#f97316', emoji: '🔥' },
|
||||
stagflation: { label: 'Stagflation', color: '#eab308', emoji: '⚡' },
|
||||
inflation_shock: { label: 'Inflation Shock', color: '#ef4444', emoji: '💥' },
|
||||
recession: { label: 'Récession', color: '#8b5cf6', emoji: '📉' },
|
||||
crise_liquidite: { label: 'Crise Liquidité', color: '#ec4899', emoji: '🚨' },
|
||||
incertain: { label: 'Incertain', color: '#64748b', emoji: '❓' },
|
||||
recession: { label: 'Recession', color: '#8b5cf6', emoji: '📉' },
|
||||
crise_liquidite: { label: 'Liquidity Crisis', color: '#ec4899', emoji: '🚨' },
|
||||
incertain: { label: 'Uncertain', color: '#64748b', emoji: '❓' },
|
||||
}
|
||||
|
||||
function RegimeBadge({ dominant }: { dominant?: string }) {
|
||||
@@ -107,7 +107,7 @@ function DeltaRow({ emoji, label, count, items, colorClass }: {
|
||||
<div className="text-slate-600 text-[10px] mt-0.5 line-clamp-2">{item.description}</div>
|
||||
)}
|
||||
{item.triggers?.length > 0 && (
|
||||
<div className="text-slate-700 text-[10px]">Déclencheurs: {item.triggers.join(', ')}</div>
|
||||
<div className="text-slate-700 text-[10px]">Triggers: {item.triggers.join(', ')}</div>
|
||||
)}
|
||||
{item.pnl_pct != null && (
|
||||
<span className={clsx('font-mono text-[10px] ml-1', item.pnl_pct >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
@@ -157,18 +157,18 @@ export default function RapportCycle() {
|
||||
{/* Left panel — history list */}
|
||||
<div className="w-56 shrink-0">
|
||||
<div className="section-title flex items-center gap-1 mb-3">
|
||||
<BookOpen className="w-3.5 h-3.5" /> Historique
|
||||
<BookOpen className="w-3.5 h-3.5" /> History
|
||||
</div>
|
||||
{listLoading ? (
|
||||
<div className="text-xs text-slate-600">Chargement…</div>
|
||||
<div className="text-xs text-slate-600">Loading…</div>
|
||||
) : reports.length === 0 ? (
|
||||
<div className="text-xs text-slate-600">Aucun rapport. Le premier sera généré au prochain cycle.</div>
|
||||
<div className="text-xs text-slate-600">No reports. The first will be generated at the next cycle.</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{reports.map((r: any) => {
|
||||
const dt = r.generated_at ? new Date(r.generated_at.endsWith('Z') ? r.generated_at : r.generated_at + 'Z') : null
|
||||
const dateStr = dt
|
||||
? dt.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
|
||||
? dt.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
|
||||
: '—'
|
||||
const isActive = selectedRunId === r.run_id || (!selectedRunId && r === reports[0])
|
||||
const m = SCENARIO_META[r.macro_dominant] ?? SCENARIO_META.incertain
|
||||
@@ -204,13 +204,13 @@ export default function RapportCycle() {
|
||||
<div className="flex-1 min-w-0">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center gap-2 text-slate-500 text-sm mt-12 justify-center">
|
||||
<RefreshCw className="w-4 h-4 animate-spin" /> Chargement…
|
||||
<RefreshCw className="w-4 h-4 animate-spin" /> Loading…
|
||||
</div>
|
||||
) : !report ? (
|
||||
<div className="card text-center py-12">
|
||||
<Brain className="w-10 h-10 text-slate-700 mx-auto mb-3" />
|
||||
<div className="text-slate-500 text-sm mb-1">Aucun rapport de cycle disponible</div>
|
||||
<div className="text-xs text-slate-600">Les rapports sont générés automatiquement à la fin de chaque cycle.</div>
|
||||
<div className="text-slate-500 text-sm mb-1">No cycle report available</div>
|
||||
<div className="text-xs text-slate-600">Reports are generated automatically at the end of each cycle.</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -221,18 +221,18 @@ export default function RapportCycle() {
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<RegimeBadge dominant={report.macro_dominant} />
|
||||
<span className="text-xs text-slate-500 font-mono">
|
||||
Géo {report.geo_score ?? '—'}/100
|
||||
Geo {report.geo_score ?? '—'}/100
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-600 font-mono">
|
||||
Cycle du {report.generated_at
|
||||
Cycle of {report.generated_at
|
||||
? new Date(report.generated_at.endsWith('Z') ? report.generated_at : report.generated_at + 'Z')
|
||||
.toLocaleString('fr-FR')
|
||||
.toLocaleString('en-GB')
|
||||
: '—'}
|
||||
{report.prev_cycle_at && (
|
||||
<span className="ml-2 text-slate-700">
|
||||
· vs cycle du {new Date(report.prev_cycle_at.endsWith('Z') ? report.prev_cycle_at : report.prev_cycle_at + 'Z')
|
||||
.toLocaleDateString('fr-FR')}
|
||||
· vs cycle of {new Date(report.prev_cycle_at.endsWith('Z') ? report.prev_cycle_at : report.prev_cycle_at + 'Z')
|
||||
.toLocaleDateString('en-GB')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -260,7 +260,7 @@ export default function RapportCycle() {
|
||||
<div className="grid grid-cols-2 gap-3 mt-2">
|
||||
{report.pnl_summary && Object.keys(report.pnl_summary).length > 0 && (
|
||||
<div className="bg-dark-700/50 rounded px-3 py-2">
|
||||
<div className="text-[9px] text-slate-600 mb-1 uppercase tracking-wide">PnL au moment du cycle</div>
|
||||
<div className="text-[9px] text-slate-600 mb-1 uppercase tracking-wide">PnL at cycle time</div>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className={clsx('text-lg font-bold font-mono',
|
||||
(report.pnl_summary.total_pnl_pct ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
@@ -276,13 +276,13 @@ export default function RapportCycle() {
|
||||
)}
|
||||
</div>
|
||||
<div className="text-[9px] text-slate-600 mt-0.5">
|
||||
{report.pnl_summary.n_open ?? 0} ouvertes · {report.pnl_summary.n_closed ?? 0} fermées
|
||||
{report.pnl_summary.n_open ?? 0} open · {report.pnl_summary.n_closed ?? 0} closed
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{report.var_summary && Object.keys(report.var_summary).length > 0 && (
|
||||
<div className="bg-dark-700/50 rounded px-3 py-2">
|
||||
<div className="text-[9px] text-slate-600 mb-1 uppercase tracking-wide">VaR 95% au moment du cycle</div>
|
||||
<div className="text-[9px] text-slate-600 mb-1 uppercase tracking-wide">VaR 95% at cycle time</div>
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
{[
|
||||
{ label: 'Hist.', val: report.var_summary.hist_var_1d_pct, color: 'text-blue-400' },
|
||||
@@ -303,15 +303,15 @@ export default function RapportCycle() {
|
||||
</div>
|
||||
|
||||
{/* Delta section */}
|
||||
<Section icon={<GitCompare className="w-4 h-4" />} title="Changements du cycle">
|
||||
<Section icon={<GitCompare className="w-4 h-4" />} title="Cycle changes">
|
||||
<DeltaRow
|
||||
emoji="⭐" label="patterns ajoutés"
|
||||
emoji="⭐" label="patterns added"
|
||||
count={report.patterns_added ?? 0}
|
||||
items={report.patterns_added_list ?? []}
|
||||
colorClass="text-blue-400"
|
||||
/>
|
||||
<DeltaRow
|
||||
emoji="📥" label="trades loggés"
|
||||
emoji="📥" label="trades logged"
|
||||
count={report.trades_logged ?? 0}
|
||||
items={(report.trades_logged_list ?? []).map((t: any) => ({
|
||||
underlying: t.underlying, strategy: t.strategy,
|
||||
@@ -320,7 +320,7 @@ export default function RapportCycle() {
|
||||
colorClass="text-emerald-400"
|
||||
/>
|
||||
<DeltaRow
|
||||
emoji="✅" label="trades fermés depuis le cycle précédent"
|
||||
emoji="✅" label="trades closed since last cycle"
|
||||
count={report.trades_closed ?? 0}
|
||||
items={(report.trades_closed_list ?? []).map((t: any) => ({
|
||||
underlying: t.underlying, strategy: t.strategy,
|
||||
@@ -331,7 +331,7 @@ export default function RapportCycle() {
|
||||
{/* Top scored */}
|
||||
{(report.top_scored ?? []).length > 0 && (
|
||||
<div className="mt-3 pt-3 border-t border-slate-700/30">
|
||||
<div className="text-xs text-slate-500 mb-2">Top patterns scorés ce cycle</div>
|
||||
<div className="text-xs text-slate-500 mb-2">Top scored patterns this cycle</div>
|
||||
<div className="space-y-1.5">
|
||||
{(report.top_scored as any[]).map((p: any, i: number) => (
|
||||
<div key={i} className="flex items-start gap-2 text-xs">
|
||||
@@ -355,7 +355,7 @@ export default function RapportCycle() {
|
||||
|
||||
{/* Context narrative */}
|
||||
{report.context_narrative?.narrative && (
|
||||
<Section icon={<Brain className="w-4 h-4" />} title="Raisonnement IA — ce cycle">
|
||||
<Section icon={<Brain className="w-4 h-4" />} title="AI reasoning — this cycle">
|
||||
<p className="text-sm text-slate-300 leading-relaxed mb-4">
|
||||
{report.context_narrative.narrative}
|
||||
</p>
|
||||
@@ -363,7 +363,7 @@ export default function RapportCycle() {
|
||||
{(report.context_narrative.context_driven ?? []).length > 0 && (
|
||||
<div>
|
||||
<div className="text-[10px] text-emerald-400 font-semibold uppercase tracking-wide mb-2">
|
||||
📡 Du contexte transmis
|
||||
📡 From transmitted context
|
||||
</div>
|
||||
<ul className="space-y-1">
|
||||
{(report.context_narrative.context_driven as string[]).map((s, i) => (
|
||||
@@ -377,7 +377,7 @@ export default function RapportCycle() {
|
||||
{(report.context_narrative.general_knowledge ?? []).length > 0 && (
|
||||
<div>
|
||||
<div className="text-[10px] text-blue-400 font-semibold uppercase tracking-wide mb-2">
|
||||
🧠 Connaissance générale
|
||||
🧠 General knowledge
|
||||
</div>
|
||||
<ul className="space-y-1">
|
||||
{(report.context_narrative.general_knowledge as string[]).map((s, i) => (
|
||||
@@ -391,7 +391,7 @@ export default function RapportCycle() {
|
||||
</div>
|
||||
{(report.context_narrative.key_signals ?? []).length > 0 && (
|
||||
<div className="mt-3 pt-3 border-t border-slate-700/30">
|
||||
<div className="text-[10px] text-slate-500 mb-2">Signaux déterminants</div>
|
||||
<div className="text-[10px] text-slate-500 mb-2">Determining signals</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{(report.context_narrative.key_signals as string[]).map((s, i) => (
|
||||
<span key={i} className="text-[10px] bg-slate-800 text-slate-300 border border-slate-700/50 px-2 py-0.5 rounded">
|
||||
@@ -404,7 +404,7 @@ export default function RapportCycle() {
|
||||
{/* Context log */}
|
||||
{report.context_narrative.context_log && (
|
||||
<div className="mt-3 pt-3 border-t border-slate-700/30">
|
||||
<div className="text-[10px] text-slate-500 mb-2">Log contexte — état transmis à l'IA</div>
|
||||
<div className="text-[10px] text-slate-500 mb-2">Context log — state transmitted to AI</div>
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-[10px]">
|
||||
{Object.entries(report.context_narrative.context_log.key_gauges ?? {}).map(([k, v]: [string, any]) => (
|
||||
<div key={k} className="flex items-center gap-1.5">
|
||||
@@ -415,7 +415,7 @@ export default function RapportCycle() {
|
||||
</div>
|
||||
{(report.context_narrative.context_log.dominant_news ?? []).length > 0 && (
|
||||
<div className="mt-2">
|
||||
<div className="text-[9px] text-slate-700 mb-1">News transmises (top impact)</div>
|
||||
<div className="text-[9px] text-slate-700 mb-1">Transmitted news (top impact)</div>
|
||||
{(report.context_narrative.context_log.dominant_news as string[]).map((n, i) => (
|
||||
<div key={i} className="text-[9px] text-slate-600 truncate">· {n}</div>
|
||||
))}
|
||||
@@ -428,7 +428,7 @@ export default function RapportCycle() {
|
||||
|
||||
{/* Cycle commentary */}
|
||||
{report.commentary?.commentary && (
|
||||
<Section icon={<Layers className="w-4 h-4" />} title="Analyse macro du cycle">
|
||||
<Section icon={<Layers className="w-4 h-4" />} title="Cycle macro analysis">
|
||||
<p className="text-sm text-slate-300 leading-relaxed mb-3">{report.commentary.commentary}</p>
|
||||
{report.commentary.key_risk && (
|
||||
<div className="flex items-start gap-2 text-xs text-orange-400 bg-orange-900/10 border border-orange-800/20 rounded px-3 py-2">
|
||||
@@ -438,7 +438,7 @@ export default function RapportCycle() {
|
||||
)}
|
||||
{report.commentary.top_pattern && (
|
||||
<div className="mt-2 text-xs text-slate-500">
|
||||
Pattern le plus pertinent : <span className="text-blue-400 font-medium">{report.commentary.top_pattern}</span>
|
||||
Most relevant pattern: <span className="text-blue-400 font-medium">{report.commentary.top_pattern}</span>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
@@ -446,7 +446,7 @@ export default function RapportCycle() {
|
||||
|
||||
{/* Options Technical Agent */}
|
||||
{report.options_technical && (report.options_technical.assessments?.length > 0 || report.options_technical.global_assessment) && (
|
||||
<Section icon={<BarChart2 className="w-4 h-4" />} title="Validation Technique Options">
|
||||
<Section icon={<BarChart2 className="w-4 h-4" />} title="Options Technical Validation">
|
||||
{/* Global score + summary */}
|
||||
<div className="flex items-start gap-3 mb-4">
|
||||
{report.options_technical.global_score != null && (
|
||||
@@ -548,7 +548,7 @@ export default function RapportCycle() {
|
||||
<div className="flex items-start gap-3 mt-1.5 pt-1.5 border-t border-slate-700/20 text-[10px]">
|
||||
{a.optimal_strategy && a.optimal_strategy !== a.strategy && (
|
||||
<span className="text-slate-500">
|
||||
Stratégie optimale: <span className="text-blue-400 font-medium">{a.optimal_strategy}</span>
|
||||
Optimal strategy: <span className="text-blue-400 font-medium">{a.optimal_strategy}</span>
|
||||
</span>
|
||||
)}
|
||||
{a.when_to_enter && (
|
||||
@@ -563,7 +563,7 @@ export default function RapportCycle() {
|
||||
|
||||
{/* Risk / portfolio monitor */}
|
||||
{report.portfolio_monitor && (
|
||||
<Section icon={<ShieldAlert className="w-4 h-4" />} title="Risque & Alertes portefeuille">
|
||||
<Section icon={<ShieldAlert className="w-4 h-4" />} title="Risk & Portfolio alerts">
|
||||
<p className="text-sm text-slate-400 mb-3">{report.portfolio_monitor.assessment}</p>
|
||||
{(report.portfolio_monitor.actions ?? []).length > 0 && (
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -17,7 +17,7 @@ function ConcentrationGauge({ label, pct, threshold = 50 }: { label: string; pct
|
||||
<div className={clsx('h-full rounded-full transition-all', color)} style={{ width: `${Math.min(pct, 100)}%` }} />
|
||||
</div>
|
||||
{pct > threshold && (
|
||||
<div className="text-[10px] text-red-400 mt-0.5">⚠ Saturé (>{threshold}%)</div>
|
||||
<div className="text-[10px] text-red-400 mt-0.5">⚠ Saturated (>{threshold}%)</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
@@ -28,7 +28,7 @@ function EquityCurve({ timeline }: { timeline: any[] }) {
|
||||
if (!timeline || timeline.length < 2) {
|
||||
return (
|
||||
<div className="h-24 flex items-center justify-center text-slate-600 text-xs">
|
||||
Pas encore de données de P&L (nécessite des trades matures)
|
||||
No P&L data yet (requires mature trades)
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -119,7 +119,7 @@ function RecommendationCard({ rec }: { rec: any }) {
|
||||
'text-emerald-400': isOk,
|
||||
})}>
|
||||
{isOk ? <CheckCircle className="w-4 h-4" /> : <AlertTriangle className="w-4 h-4" />}
|
||||
Recommandation Risk Committee
|
||||
Risk Committee Recommendation
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
{(rec.messages ?? []).map((msg: string, i: number) => (
|
||||
@@ -150,8 +150,8 @@ function SimRiskPanel() {
|
||||
if (!risk || risk.open_count === 0) return (
|
||||
<div className="card text-center py-10 text-slate-500">
|
||||
<PieChart className="w-8 h-8 mx-auto mb-2 opacity-20" />
|
||||
<div className="text-sm">Aucune position ouverte dans le portefeuille simulé</div>
|
||||
<div className="text-xs mt-1">Les trades logués apparaîtront ici après le prochain cycle IA</div>
|
||||
<div className="text-sm">No open positions in the simulated portfolio</div>
|
||||
<div className="text-xs mt-1">Logged trades will appear here after the next AI cycle</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -169,25 +169,25 @@ function SimRiskPanel() {
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||
<div className="card text-center">
|
||||
<div className="text-2xl font-bold text-white font-mono">{risk.open_count}</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Positions simulées</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Simulated positions</div>
|
||||
</div>
|
||||
<div className="card text-center">
|
||||
<div className={clsx('text-2xl font-bold font-mono', dangers.length > 0 ? 'text-red-400' : 'text-emerald-400')}>
|
||||
{dangers.length}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Conflits directionnels</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Directional conflicts</div>
|
||||
</div>
|
||||
<div className="card text-center">
|
||||
<div className={clsx('text-2xl font-bold font-mono', warnings.length > 0 ? 'text-amber-400' : 'text-emerald-400')}>
|
||||
{warnings.length}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Alertes concentration</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Concentration alerts</div>
|
||||
</div>
|
||||
<div className="card text-center">
|
||||
<div className="text-2xl font-bold font-mono text-slate-300">
|
||||
{Object.keys(concentration).filter(ac => ac !== 'unknown').length}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Classes d'actifs</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Asset classes</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -197,7 +197,7 @@ function SimRiskPanel() {
|
||||
<div className="card">
|
||||
<div className="text-sm font-semibold text-white mb-3 flex items-center justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<GitBranch className="w-4 h-4 text-blue-400" /> Répartition par classe d'actif
|
||||
<GitBranch className="w-4 h-4 text-blue-400" /> Allocation by asset class
|
||||
</span>
|
||||
<button onClick={() => refetch()} className="text-slate-600 hover:text-slate-400">
|
||||
<RefreshCw className="w-3.5 h-3.5" />
|
||||
@@ -243,14 +243,14 @@ function SimRiskPanel() {
|
||||
{conflicts.length === 0 && warnings.length === 0 ? (
|
||||
<div className="card border-emerald-700/30 bg-emerald-900/10 flex items-center gap-3 py-4">
|
||||
<CheckCircle className="w-5 h-5 text-emerald-400 shrink-0" />
|
||||
<div className="text-xs text-emerald-300">Portefeuille équilibré — aucun conflit ni sur-concentration détectés</div>
|
||||
<div className="text-xs text-emerald-300">Balanced portfolio — no conflicts or over-concentration detected</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{conflicts.map((c: any, i: number) => (
|
||||
<div key={i} className="card border-red-700/40 bg-red-900/10">
|
||||
<div className="text-xs font-semibold text-red-300 mb-2 flex items-center gap-1">
|
||||
<ShieldAlert className="w-3.5 h-3.5" /> Conflit directionnel — {c.underlying}
|
||||
<ShieldAlert className="w-3.5 h-3.5" /> Directional conflict — {c.underlying}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{c.trades.map((t: any) => (
|
||||
@@ -271,7 +271,7 @@ function SimRiskPanel() {
|
||||
{warnings.length > 0 && (
|
||||
<div className="card border-amber-700/30 bg-amber-900/10">
|
||||
<div className="text-xs font-semibold text-amber-300 mb-2 flex items-center gap-1">
|
||||
<AlertTriangle className="w-3.5 h-3.5" /> Alertes de concentration
|
||||
<AlertTriangle className="w-3.5 h-3.5" /> Concentration alerts
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{warnings.map((a: any, i: number) => (
|
||||
@@ -290,7 +290,7 @@ function SimRiskPanel() {
|
||||
<div className="card border-blue-700/30 bg-blue-900/10">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-xs font-semibold text-blue-300 flex items-center gap-1.5">
|
||||
<Brain className="w-3.5 h-3.5" /> Recommandations IA — Moniteur de portefeuille simulé
|
||||
<Brain className="w-3.5 h-3.5" /> AI Recommendations — Simulated portfolio monitor
|
||||
</div>
|
||||
{aiTs && <span className="text-[10px] text-slate-600">{aiTs.slice(0, 16).replace('T', ' ')}</span>}
|
||||
</div>
|
||||
@@ -349,8 +349,8 @@ export default function RiskDashboard() {
|
||||
</h1>
|
||||
<p className="text-xs text-slate-500 mt-0.5">
|
||||
{mode === 'real'
|
||||
? 'Concentration · Clusters · Corrélations · Position Sizing — Portefeuille IBKR'
|
||||
: 'Conflits directionnels · Concentration · Recommandations IA — Trades loggés'}
|
||||
? 'Concentration · Clusters · Correlations · Position Sizing — IBKR Portfolio'
|
||||
: 'Directional conflicts · Concentration · AI Recommendations — Logged trades'}
|
||||
</p>
|
||||
</div>
|
||||
{/* Mode toggle */}
|
||||
@@ -360,14 +360,14 @@ export default function RiskDashboard() {
|
||||
'bg-blue-600 text-white': mode === 'real',
|
||||
'text-slate-400 hover:text-slate-200': mode !== 'real',
|
||||
})}>
|
||||
Portefeuille Réel
|
||||
Real Portfolio
|
||||
</button>
|
||||
<button onClick={() => setMode('sim')}
|
||||
className={clsx('flex items-center gap-1.5 px-3 py-1.5 rounded text-sm font-semibold transition-colors', {
|
||||
'bg-blue-600 text-white': mode === 'sim',
|
||||
'text-slate-400 hover:text-slate-200': mode !== 'sim',
|
||||
})}>
|
||||
Simulé (loggés)
|
||||
Simulated (logged)
|
||||
{simConflicts > 0 && (
|
||||
<span className="text-[10px] font-bold bg-red-500 text-white rounded-full w-4 h-4 flex items-center justify-center">
|
||||
{simConflicts}
|
||||
@@ -387,14 +387,14 @@ export default function RiskDashboard() {
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
<div className="card text-center">
|
||||
<div className="text-2xl font-bold text-white font-mono">{d.open_trades ?? 0}</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Positions ouvertes</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Open positions</div>
|
||||
</div>
|
||||
<div className="card text-center">
|
||||
<div className={clsx('text-2xl font-bold font-mono',
|
||||
d.diversification_score >= 60 ? 'text-emerald-400'
|
||||
: d.diversification_score >= 35 ? 'text-amber-400' : 'text-red-400'
|
||||
)}>{d.diversification_score ?? '—'}%</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Score diversification</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Diversification score</div>
|
||||
<div className="text-[10px] text-slate-600">N effectif = {d.effective_n_positions ?? '—'}</div>
|
||||
</div>
|
||||
<div className="card text-center">
|
||||
@@ -402,7 +402,7 @@ export default function RiskDashboard() {
|
||||
(d.expected_drawdown_pct ?? 0) < 15 ? 'text-emerald-400'
|
||||
: (d.expected_drawdown_pct ?? 0) < 25 ? 'text-amber-400' : 'text-red-400'
|
||||
)}>{d.expected_drawdown_pct ?? '—'}%</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Drawdown attendu</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Expected drawdown</div>
|
||||
</div>
|
||||
<div className="card text-center">
|
||||
<div className={clsx('text-2xl font-bold font-mono',
|
||||
@@ -410,7 +410,7 @@ export default function RiskDashboard() {
|
||||
)}>
|
||||
{d.saturated_factors?.length ?? 0}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Facteurs saturés</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Saturated factors</div>
|
||||
{d.saturated_factors?.length > 0 && (
|
||||
<div className="text-[10px] text-red-400">{d.saturated_factors.join(', ')}</div>
|
||||
)}
|
||||
@@ -439,10 +439,10 @@ export default function RiskDashboard() {
|
||||
{/* Exposure by class */}
|
||||
<div className="card">
|
||||
<div className="text-sm font-semibold text-white mb-3 flex items-center gap-2">
|
||||
<GitBranch className="w-4 h-4 text-blue-400" /> Exposition par classe d'actif
|
||||
<GitBranch className="w-4 h-4 text-blue-400" /> Exposure by asset class
|
||||
</div>
|
||||
{Object.keys(d.exposure_by_class ?? {}).length === 0 ? (
|
||||
<div className="text-xs text-slate-600 text-center py-4">Aucune position ouverte</div>
|
||||
<div className="text-xs text-slate-600 text-center py-4">No open positions</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{Object.entries(d.exposure_by_class ?? {})
|
||||
@@ -457,10 +457,10 @@ export default function RiskDashboard() {
|
||||
{/* Risk factors */}
|
||||
<div className="card">
|
||||
<div className="text-sm font-semibold text-white mb-3 flex items-center gap-2">
|
||||
<Activity className="w-4 h-4 text-orange-400" /> Exposition par facteur de risque
|
||||
<Activity className="w-4 h-4 text-orange-400" /> Exposure by risk factor
|
||||
</div>
|
||||
{(d.risk_clusters ?? []).length === 0 ? (
|
||||
<div className="text-xs text-slate-600 text-center py-4">Aucune position ouverte</div>
|
||||
<div className="text-xs text-slate-600 text-center py-4">No open positions</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{(d.risk_clusters ?? []).map((c: any) => (
|
||||
@@ -475,14 +475,14 @@ export default function RiskDashboard() {
|
||||
<div className="card">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="text-sm font-semibold text-white flex items-center gap-2">
|
||||
<TrendingUp className="w-4 h-4 text-blue-400" /> Courbe P&L cumulé (trades matures)
|
||||
<TrendingUp className="w-4 h-4 text-blue-400" /> Cumulative P&L curve (mature trades)
|
||||
</div>
|
||||
<select value={tlDays} onChange={e => setTlDays(Number(e.target.value))}
|
||||
className="bg-dark-700 border border-slate-700 rounded px-2 py-0.5 text-xs text-slate-300">
|
||||
<option value={30}>30j</option>
|
||||
<option value={90}>90j</option>
|
||||
<option value={180}>180j</option>
|
||||
<option value={365}>1 an</option>
|
||||
<option value={30}>30d</option>
|
||||
<option value={90}>90d</option>
|
||||
<option value={180}>180d</option>
|
||||
<option value={365}>1 yr</option>
|
||||
</select>
|
||||
</div>
|
||||
<EquityCurve timeline={tlData?.timeline ?? []} />
|
||||
@@ -491,11 +491,11 @@ export default function RiskDashboard() {
|
||||
{/* Correlation pairs */}
|
||||
<div className="card">
|
||||
<div className="text-sm font-semibold text-white mb-3">
|
||||
Corrélations entre patterns (P&L historique)
|
||||
Pattern correlations (historical P&L)
|
||||
</div>
|
||||
{pairs.length === 0 ? (
|
||||
<div className="text-xs text-slate-600 text-center py-4">
|
||||
Nécessite ≥3 trades matures par pattern pour calculer les corrélations
|
||||
Requires ≥3 mature trades per pattern to calculate correlations
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
@@ -503,13 +503,13 @@ export default function RiskDashboard() {
|
||||
<CorrelationPairRow key={i} pair={pair} />
|
||||
))}
|
||||
{pairs.length > 10 && (
|
||||
<div className="text-xs text-slate-600 text-center pt-1">{pairs.length - 10} autres paires…</div>
|
||||
<div className="text-xs text-slate-600 text-center pt-1">{pairs.length - 10} more pairs…</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{pairs.length > 0 && (
|
||||
<div className="text-[10px] text-slate-600 mt-2">
|
||||
Corrélation > 0.7 = risque concentré — ces patterns partagent le même facteur sous-jacent
|
||||
Correlation > 0.7 = concentrated risk — these patterns share the same underlying factor
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -14,9 +14,9 @@ import {
|
||||
// ─── Status badge ─────────────────────────────────────────────────────────────
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
const map: Record<string, { icon: React.ReactNode; color: string; label: string }> = {
|
||||
active: { icon: <CheckCircle className="w-3 h-3" />, color: 'text-emerald-400 bg-emerald-400/10 border-emerald-400/20', label: 'Validé' },
|
||||
active: { icon: <CheckCircle className="w-3 h-3" />, color: 'text-emerald-400 bg-emerald-400/10 border-emerald-400/20', label: 'Validated' },
|
||||
tentative: { icon: <HelpCircle className="w-3 h-3" />, color: 'text-amber-400 bg-amber-400/10 border-amber-400/20', label: 'Tentative' },
|
||||
invalidated: { icon: <XCircle className="w-3 h-3" />, color: 'text-rose-400 bg-rose-400/10 border-rose-400/20', label: 'Invalidé' },
|
||||
invalidated: { icon: <XCircle className="w-3 h-3" />, color: 'text-rose-400 bg-rose-400/10 border-rose-400/20', label: 'Invalidated' },
|
||||
}
|
||||
const s = map[status] || map['tentative']
|
||||
return (
|
||||
@@ -61,10 +61,10 @@ function KbEntry({ entry, onStatusChange, onDelete }: {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (confirm(`Supprimer "${entry.title}" ?`)) onDelete(entry.id)
|
||||
if (confirm(`Delete "${entry.title}"?`)) onDelete(entry.id)
|
||||
}}
|
||||
className="opacity-0 group-hover:opacity-100 p-1 rounded text-slate-600 hover:text-red-400 hover:bg-red-400/10 transition-all mt-0.5"
|
||||
title="Supprimer cette entrée"
|
||||
title="Delete this entry"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
@@ -90,12 +90,12 @@ function KbEntry({ entry, onStatusChange, onDelete }: {
|
||||
: 'border-slate-700 text-slate-500 hover:border-slate-500 hover:text-slate-300'
|
||||
}`}
|
||||
>
|
||||
{s === 'active' ? 'Valider' : s === 'tentative' ? 'Tentative' : 'Invalider'}
|
||||
{s === 'active' ? 'Validate' : s === 'tentative' ? 'Tentative' : 'Invalidate'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-600">
|
||||
Vu le {entry.first_seen_at?.slice(0, 10)} · Confirmé {entry.confirmation_count}×
|
||||
First seen {entry.first_seen_at?.slice(0, 10)} · Confirmed {entry.confirmation_count}×
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -105,11 +105,11 @@ function KbEntry({ entry, onStatusChange, onDelete }: {
|
||||
|
||||
// ─── Category section ─────────────────────────────────────────────────────────
|
||||
const CAT_META: Record<string, { icon: React.ReactNode; color: string; label: string }> = {
|
||||
régimes: { icon: <Activity className="w-4 h-4" />, color: 'text-blue-400', label: 'Régimes macro' },
|
||||
régimes: { icon: <Activity className="w-4 h-4" />, color: 'text-blue-400', label: 'Macro regimes' },
|
||||
patterns: { icon: <BarChart2 className="w-4 h-4" />, color: 'text-purple-400', label: 'Patterns' },
|
||||
erreurs: { icon: <AlertTriangle className="w-4 h-4" />, color: 'text-rose-400', label: 'Erreurs récurrentes' },
|
||||
corrélations: { icon: <TrendingUp className="w-4 h-4" />, color: 'text-emerald-400', label: 'Corrélations' },
|
||||
général: { icon: <BookOpen className="w-4 h-4" />, color: 'text-slate-400', label: 'Général' },
|
||||
erreurs: { icon: <AlertTriangle className="w-4 h-4" />, color: 'text-rose-400', label: 'Recurring errors' },
|
||||
corrélations: { icon: <TrendingUp className="w-4 h-4" />, color: 'text-emerald-400', label: 'Correlations' },
|
||||
général: { icon: <BookOpen className="w-4 h-4" />, color: 'text-slate-400', label: 'General' },
|
||||
}
|
||||
|
||||
function CategorySection({
|
||||
@@ -132,7 +132,7 @@ function CategorySection({
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={meta.color}>{meta.icon}</span>
|
||||
<span className="text-sm font-medium text-slate-200">{meta.label}</span>
|
||||
<span className="text-xs text-slate-500">{active}/{entries.length} actifs</span>
|
||||
<span className="text-xs text-slate-500">{active}/{entries.length} active</span>
|
||||
</div>
|
||||
{open ? <ChevronUp className="w-4 h-4 text-slate-500" /> : <ChevronDown className="w-4 h-4 text-slate-500" />}
|
||||
</button>
|
||||
@@ -236,14 +236,14 @@ export default function SuperContexte() {
|
||||
<Brain className="w-6 h-6 text-violet-400" />
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-slate-100">Super Contexte</h1>
|
||||
<p className="text-xs text-slate-500">Base de raisonnement évolutive — cerveau du système</p>
|
||||
<p className="text-xs text-slate-500">Evolving reasoning base — system brain</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{currentState && (
|
||||
<div className="text-right text-xs text-slate-500">
|
||||
<div>v{currentState.version} · {currentState.created_at?.slice(0, 16)}</div>
|
||||
<div>{currentState.reports_used} rapports · {currentState.trades_analyzed} trades analysés</div>
|
||||
<div>{currentState.reports_used} reports · {currentState.trades_analyzed} trades analyzed</div>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
@@ -262,7 +262,7 @@ export default function SuperContexte() {
|
||||
) : (
|
||||
<Brain className="w-4 h-4" />
|
||||
)}
|
||||
{synthesize.isPending ? 'Synthèse en cours…' : 'Lancer synthèse GPT-4o'}
|
||||
{synthesize.isPending ? 'Synthesis in progress…' : 'Run GPT-4o synthesis'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -277,7 +277,7 @@ export default function SuperContexte() {
|
||||
onClick={() => setSelectedHistoryId(null)}
|
||||
className={`w-full text-left px-3 py-2 rounded-lg text-xs transition-colors ${!selectedHistoryId ? 'bg-violet-600/20 border border-violet-500/30 text-violet-300' : 'border border-slate-700/50 text-slate-400 hover:border-slate-600'}`}
|
||||
>
|
||||
Dernière version
|
||||
Latest version
|
||||
</button>
|
||||
{history.map((h: any) => (
|
||||
<div
|
||||
@@ -290,18 +290,18 @@ export default function SuperContexte() {
|
||||
>
|
||||
<div className="font-medium">v{h.version}</div>
|
||||
<div className="text-slate-500">{h.created_at?.slice(0, 16)}</div>
|
||||
<div className="text-slate-600">{h.reports_used} rapports · {h.trades_analyzed} trades</div>
|
||||
<div className="text-slate-600">{h.reports_used} reports · {h.trades_analyzed} trades</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (confirm(`Supprimer la version v${h.version} du Super Contexte ?`)) {
|
||||
if (confirm(`Delete Super Context version v${h.version}?`)) {
|
||||
if (selectedHistoryId === h.id) setSelectedHistoryId(null)
|
||||
deleteState.mutate(h.id)
|
||||
}
|
||||
}}
|
||||
className="absolute top-1.5 right-1.5 p-1 rounded opacity-0 group-hover:opacity-100 text-slate-600 hover:text-red-400 hover:bg-red-400/10 transition-all"
|
||||
title="Supprimer cette version"
|
||||
title="Delete this version"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</button>
|
||||
@@ -314,17 +314,17 @@ export default function SuperContexte() {
|
||||
<div className="flex-1 min-w-0 space-y-6">
|
||||
{stateLoading && (
|
||||
<div className="flex items-center justify-center h-40 text-slate-500">
|
||||
<RefreshCw className="w-5 h-5 animate-spin mr-2" /> Chargement…
|
||||
<RefreshCw className="w-5 h-5 animate-spin mr-2" /> Loading…
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!currentState && !stateLoading && (
|
||||
<div className="border border-violet-500/20 rounded-xl p-8 text-center bg-violet-500/5">
|
||||
<Brain className="w-12 h-12 text-violet-400/50 mx-auto mb-3" />
|
||||
<p className="text-slate-300 font-medium mb-1">Aucune synthèse disponible</p>
|
||||
<p className="text-slate-300 font-medium mb-1">No synthesis available</p>
|
||||
<p className="text-slate-500 text-sm mb-4">
|
||||
Lance la première synthèse GPT-4o pour initialiser la base de raisonnement.
|
||||
Le système analysera tous les rapports et trades disponibles.
|
||||
Run the first GPT-4o synthesis to initialize the reasoning base.
|
||||
The system will analyze all available reports and trades.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => handleSynthesize()}
|
||||
@@ -332,7 +332,7 @@ export default function SuperContexte() {
|
||||
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-violet-600 hover:bg-violet-500 disabled:opacity-50 text-sm font-medium transition-colors"
|
||||
>
|
||||
{synthesize.isPending ? <RefreshCw className="w-4 h-4 animate-spin" /> : <Brain className="w-4 h-4" />}
|
||||
{synthesize.isPending ? 'Synthèse en cours…' : 'Initialiser le Super Contexte'}
|
||||
{synthesize.isPending ? 'Synthesis in progress…' : 'Initialize Super Context'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -341,10 +341,10 @@ export default function SuperContexte() {
|
||||
<div className="border border-amber-500/30 bg-amber-500/5 rounded-xl p-4 flex items-start gap-3">
|
||||
<Clock className="w-5 h-5 text-amber-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1 text-sm">
|
||||
<span className="text-amber-300 font-medium">Synthèse non relancée. </span>
|
||||
<span className="text-amber-300 font-medium">Synthesis not rerun. </span>
|
||||
<span className="text-slate-400">
|
||||
Dernière synthèse il y a {synthResult?.age_hours}h (moins de 6h).
|
||||
Les trades immatures n'apportent pas encore d'information fiable — inutile de recalculer.
|
||||
Last synthesis {synthResult?.age_hours}h ago (less than 6h).
|
||||
Immature trades don't yet provide reliable data — no need to recompute.
|
||||
</span>
|
||||
<div className="mt-2">
|
||||
<button
|
||||
@@ -352,7 +352,7 @@ export default function SuperContexte() {
|
||||
disabled={synthesize.isPending}
|
||||
className="text-xs px-3 py-1.5 rounded-lg border border-amber-500/40 text-amber-400 hover:bg-amber-500/10 transition-colors"
|
||||
>
|
||||
Forcer quand même la synthèse
|
||||
Force synthesis anyway
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -363,11 +363,11 @@ export default function SuperContexte() {
|
||||
<div className="border border-emerald-500/30 bg-emerald-500/5 rounded-xl p-4 flex items-center gap-3">
|
||||
<CheckCircle className="w-5 h-5 text-emerald-400 flex-shrink-0" />
|
||||
<div className="text-sm">
|
||||
<span className="text-emerald-300 font-medium">Synthèse complète. </span>
|
||||
<span className="text-emerald-300 font-medium">Synthesis complete. </span>
|
||||
<span className="text-slate-400">
|
||||
{synthResult?.kb_entries_added ?? 0} entrées KB ajoutées ·{' '}
|
||||
{synthResult?.sources?.reports ?? 0} rapports ·{' '}
|
||||
{synthResult?.sources?.trades ?? 0} trades analysés.
|
||||
{synthResult?.kb_entries_added ?? 0} KB entries added ·{' '}
|
||||
{synthResult?.sources?.reports ?? 0} reports ·{' '}
|
||||
{synthResult?.sources?.trades ?? 0} trades analyzed.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -379,7 +379,7 @@ export default function SuperContexte() {
|
||||
<div className="border border-violet-500/20 bg-violet-500/5 rounded-xl p-5 space-y-3">
|
||||
<div className="flex items-center gap-2 text-violet-300">
|
||||
<Brain className="w-5 h-5" />
|
||||
<span className="text-sm font-semibold">Narrative de raisonnement</span>
|
||||
<span className="text-sm font-semibold">Reasoning narrative</span>
|
||||
{selectedHistoryId && (
|
||||
<span className="text-xs text-slate-500 ml-auto">v{currentState.version} (archivé)</span>
|
||||
)}
|
||||
@@ -393,7 +393,7 @@ export default function SuperContexte() {
|
||||
{synthesis.regime_insights?.length > 0 && (
|
||||
<div className="border border-slate-700/50 rounded-xl p-4 space-y-3">
|
||||
<div className="flex items-center gap-2 text-blue-400 text-xs font-semibold uppercase tracking-wide">
|
||||
<Activity className="w-4 h-4" />Régimes macro identifiés
|
||||
<Activity className="w-4 h-4" />Identified macro regimes
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{synthesis.regime_insights.map((r: any, i: number) => (
|
||||
@@ -407,7 +407,7 @@ export default function SuperContexte() {
|
||||
{synthesis.pattern_insights?.length > 0 && (
|
||||
<div className="border border-slate-700/50 rounded-xl p-4 space-y-3">
|
||||
<div className="flex items-center gap-2 text-purple-400 text-xs font-semibold uppercase tracking-wide">
|
||||
<BarChart2 className="w-4 h-4" />Patterns documentés
|
||||
<BarChart2 className="w-4 h-4" />Documented patterns
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{synthesis.pattern_insights.map((p: any, i: number) => (
|
||||
@@ -427,7 +427,7 @@ export default function SuperContexte() {
|
||||
{synthesis.macro_correlations?.length > 0 && (
|
||||
<div className="border border-slate-700/50 rounded-xl p-4 space-y-3">
|
||||
<div className="flex items-center gap-2 text-emerald-400 text-xs font-semibold uppercase tracking-wide">
|
||||
<TrendingUp className="w-4 h-4" />Corrélations macro/géo
|
||||
<TrendingUp className="w-4 h-4" />Macro/geo correlations
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{synthesis.macro_correlations.map((c: any, i: number) => (
|
||||
@@ -448,25 +448,25 @@ export default function SuperContexte() {
|
||||
{/* Risk params + insights */}
|
||||
<div className="border border-slate-700/50 rounded-xl p-4 space-y-4">
|
||||
<InsightList
|
||||
title="Priorités stratégiques"
|
||||
title="Strategic priorities"
|
||||
icon={<Target className="w-4 h-4" />}
|
||||
items={synthesis.strategic_priorities || []}
|
||||
color="text-violet-400"
|
||||
/>
|
||||
<InsightList
|
||||
title="Forces identifiées"
|
||||
title="Identified strengths"
|
||||
icon={<Zap className="w-4 h-4" />}
|
||||
items={synthesis.strengths || []}
|
||||
color="text-emerald-400"
|
||||
/>
|
||||
<InsightList
|
||||
title="Angles morts"
|
||||
title="Blind spots"
|
||||
icon={<AlertTriangle className="w-4 h-4" />}
|
||||
items={synthesis.blind_spots || []}
|
||||
color="text-amber-400"
|
||||
/>
|
||||
<InsightList
|
||||
title="Erreurs récurrentes"
|
||||
title="Recurring mistakes"
|
||||
icon={<TrendingDown className="w-4 h-4" />}
|
||||
items={(synthesis.recurring_mistakes || []).map((m: any) =>
|
||||
typeof m === 'string' ? m : `${m.mistake} → ${m.mitigation}`
|
||||
@@ -476,13 +476,13 @@ export default function SuperContexte() {
|
||||
{synthesis.risk_parameters && (
|
||||
<>
|
||||
<InsightList
|
||||
title="Préférer quand"
|
||||
title="Prefer when"
|
||||
icon={<Shield className="w-4 h-4" />}
|
||||
items={synthesis.risk_parameters.prefer_when || []}
|
||||
color="text-blue-400"
|
||||
/>
|
||||
<InsightList
|
||||
title="Éviter quand"
|
||||
title="Avoid when"
|
||||
icon={<XCircle className="w-4 h-4" />}
|
||||
items={synthesis.risk_parameters.avoid_when || []}
|
||||
color="text-rose-400"
|
||||
@@ -499,7 +499,7 @@ export default function SuperContexte() {
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<BookOpen className="w-4 h-4 text-slate-400" />
|
||||
<h2 className="text-sm font-semibold text-slate-300">Base de connaissances ({totalEntries} entrées)</h2>
|
||||
<h2 className="text-sm font-semibold text-slate-300">Knowledge base ({totalEntries} entries)</h2>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{Object.entries(byCategory).map(([cat, entries]: [string, any]) => (
|
||||
|
||||
@@ -80,7 +80,7 @@ function LogRow({ log }: { log: any }) {
|
||||
const AI_CALL_CONFIG: Record<string, { label: string; color: string; icon: React.ReactNode }> = {
|
||||
suggest: { label: 'Suggestion patterns', color: 'text-purple-300 border-purple-700/40 bg-purple-900/20', icon: <Brain className="w-3 h-3" /> },
|
||||
score_batch: { label: 'Scoring batch', color: 'text-cyan-300 border-cyan-700/40 bg-cyan-900/20', icon: <BarChart2 className="w-3 h-3" /> },
|
||||
unknown: { label: 'Appel IA', color: 'text-slate-300 border-slate-700/40 bg-slate-800', icon: <Zap className="w-3 h-3" /> },
|
||||
unknown: { label: 'AI call', color: 'text-slate-300 border-slate-700/40 bg-slate-800', icon: <Zap className="w-3 h-3" /> },
|
||||
}
|
||||
|
||||
function AiCallRow({ call }: { call: any }) {
|
||||
@@ -126,19 +126,19 @@ function AiCallRow({ call }: { call: any }) {
|
||||
{/* Token breakdown */}
|
||||
{totalTokens > 0 && (
|
||||
<div className="flex gap-4 px-3 py-2 bg-slate-900/50 text-[10px] font-mono text-slate-500 border-b border-slate-800">
|
||||
<span>Modèle: <span className="text-slate-300">{call.model || 'gpt-4o'}</span></span>
|
||||
<span>Model: <span className="text-slate-300">{call.model || 'gpt-4o'}</span></span>
|
||||
<span>Prompt: <span className="text-amber-300">{(call.tokens_prompt ?? 0).toLocaleString()} tok</span></span>
|
||||
<span>Completion: <span className="text-green-300">{(call.tokens_completion ?? 0).toLocaleString()} tok</span></span>
|
||||
<span>Total: <span className="text-white">{totalTokens.toLocaleString()} tok</span></span>
|
||||
<span>Durée: <span className="text-purple-300">{(call.duration_ms / 1000).toFixed(2)}s</span></span>
|
||||
<span>Duration: <span className="text-purple-300">{(call.duration_ms / 1000).toFixed(2)}s</span></span>
|
||||
</div>
|
||||
)}
|
||||
{/* Pane selector */}
|
||||
<div className="flex gap-0 border-b border-slate-800">
|
||||
{([
|
||||
{ key: 'user' as const, label: 'Prompt utilisateur', icon: <MessageSquare className="w-3 h-3" /> },
|
||||
{ key: 'system' as const, label: 'Prompt système', icon: <Brain className="w-3 h-3" /> },
|
||||
{ key: 'response' as const, label: 'Réponse IA', icon: <Zap className="w-3 h-3" /> },
|
||||
{ key: 'user' as const, label: 'User prompt', icon: <MessageSquare className="w-3 h-3" /> },
|
||||
{ key: 'system' as const, label: 'System prompt', icon: <Brain className="w-3 h-3" /> },
|
||||
{ key: 'response' as const, label: 'AI response', icon: <Zap className="w-3 h-3" /> },
|
||||
]).map(p => (
|
||||
<button
|
||||
key={p.key}
|
||||
@@ -157,13 +157,13 @@ function AiCallRow({ call }: { call: any }) {
|
||||
{/* Content */}
|
||||
<div className="p-3 bg-black/40 max-h-[500px] overflow-y-auto">
|
||||
{activePane === 'user' && (
|
||||
<pre className="text-[10px] font-mono text-slate-300 whitespace-pre-wrap leading-relaxed">{call.user_prompt || '(vide)'}</pre>
|
||||
<pre className="text-[10px] font-mono text-slate-300 whitespace-pre-wrap leading-relaxed">{call.user_prompt || '(empty)'}</pre>
|
||||
)}
|
||||
{activePane === 'system' && (
|
||||
<pre className="text-[10px] font-mono text-slate-400 whitespace-pre-wrap leading-relaxed">{call.system_prompt || '(vide)'}</pre>
|
||||
<pre className="text-[10px] font-mono text-slate-400 whitespace-pre-wrap leading-relaxed">{call.system_prompt || '(empty)'}</pre>
|
||||
)}
|
||||
{activePane === 'response' && (
|
||||
<pre className="text-[10px] font-mono text-green-300 whitespace-pre-wrap leading-relaxed">{responseStr || '(pas de réponse)'}</pre>
|
||||
<pre className="text-[10px] font-mono text-green-300 whitespace-pre-wrap leading-relaxed">{responseStr || '(no response)'}</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -176,17 +176,17 @@ function AiCallsSection({ runId }: { runId: string }) {
|
||||
const { data, isLoading } = useAiCallLogs(runId)
|
||||
const calls: any[] = data?.calls ?? []
|
||||
|
||||
if (isLoading) return <div className="p-4 text-xs text-slate-500">Chargement des appels IA…</div>
|
||||
if (isLoading) return <div className="p-4 text-xs text-slate-500">Loading AI calls…</div>
|
||||
if (calls.length === 0) return (
|
||||
<div className="p-4 text-xs text-slate-600 text-center">
|
||||
Aucun appel IA enregistré pour ce cycle — les logs seront disponibles dès le prochain cycle.
|
||||
No AI calls recorded for this cycle — logs will be available from the next cycle.
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="text-[10px] text-slate-500 px-1 font-mono">
|
||||
{calls.length} appel(s) IA — cliquer pour voir prompts + réponse complète
|
||||
{calls.length} AI call(s) — click to view prompts + full response
|
||||
</div>
|
||||
{calls.map(c => <AiCallRow key={c.id} call={c} />)}
|
||||
</div>
|
||||
@@ -221,10 +221,10 @@ function ContextTab() {
|
||||
{/* Left: list of snapshots */}
|
||||
<div className="card overflow-y-auto max-h-[700px]">
|
||||
{loadingList ? (
|
||||
<div className="p-4 text-xs text-slate-500">Chargement…</div>
|
||||
<div className="p-4 text-xs text-slate-500">Loading…</div>
|
||||
) : snapshots.length === 0 ? (
|
||||
<div className="p-4 text-xs text-slate-600">
|
||||
Aucun snapshot — les contextes seront sauvegardés lors du prochain cycle.
|
||||
No snapshot — contexts will be saved on the next cycle.
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-slate-800">
|
||||
@@ -250,12 +250,12 @@ function ContextTab() {
|
||||
{!selectedRunId ? (
|
||||
<div className="p-8 text-center text-slate-600 text-sm flex-1">
|
||||
<Brain className="w-8 h-8 mx-auto mb-2 opacity-30" />
|
||||
Sélectionne un cycle à gauche pour voir le contexte complet envoyé à l'IA.
|
||||
Select a cycle on the left to view the full context sent to the AI.
|
||||
</div>
|
||||
) : loadingSnap ? (
|
||||
<div className="p-8 text-center text-slate-500 text-sm flex-1">Chargement du contexte…</div>
|
||||
<div className="p-8 text-center text-slate-500 text-sm flex-1">Loading context…</div>
|
||||
) : !snapData ? (
|
||||
<div className="p-8 text-center text-slate-500 text-sm flex-1">Erreur lors du chargement.</div>
|
||||
<div className="p-8 text-center text-slate-500 text-sm flex-1">Loading error.</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Header strip with meta + replay */}
|
||||
@@ -273,7 +273,7 @@ function ContextTab() {
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
<input
|
||||
className="bg-slate-800 border border-slate-700 rounded px-2 py-1 text-[10px] text-slate-300 w-48"
|
||||
placeholder="Notes de replay (optionnel)"
|
||||
placeholder="Replay notes (optional)"
|
||||
value={replayNotes}
|
||||
onChange={e => setReplayNotes(e.target.value)}
|
||||
/>
|
||||
@@ -283,8 +283,8 @@ function ContextTab() {
|
||||
className="flex items-center gap-1.5 bg-purple-700 hover:bg-purple-600 disabled:opacity-40 text-white px-3 py-1.5 rounded text-xs font-semibold"
|
||||
>
|
||||
{replaying
|
||||
? <><Loader2 className="w-3 h-3 animate-spin" /> Replay en cours…</>
|
||||
: <><Play className="w-3 h-3" /> Rejouer ce cycle</>}
|
||||
? <><Loader2 className="w-3 h-3 animate-spin" /> Replaying…</>
|
||||
: <><Play className="w-3 h-3" /> Replay this cycle</>}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -294,7 +294,7 @@ function ContextTab() {
|
||||
<div className="border-b border-slate-800 bg-green-950/20 p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-xs font-semibold text-green-400">
|
||||
Replay terminé — {replayResult.suggestions_count} patterns générés
|
||||
Replay complete — {replayResult.suggestions_count} patterns generated
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-500">à {fmtTs(replayResult.replayed_at)}</span>
|
||||
</div>
|
||||
@@ -312,8 +312,8 @@ function ContextTab() {
|
||||
{/* Sub-tabs: Contexte / Appels IA */}
|
||||
<div className="flex gap-0 border-b border-slate-800 bg-slate-900/40 shrink-0">
|
||||
{([
|
||||
{ key: 'context' as const, label: 'Contexte IA', icon: <Brain className="w-3 h-3" /> },
|
||||
{ key: 'calls' as const, label: 'Appels IA', icon: <Zap className="w-3 h-3" /> },
|
||||
{ key: 'context' as const, label: 'AI Context', icon: <Brain className="w-3 h-3" /> },
|
||||
{ key: 'calls' as const, label: 'AI Calls', icon: <Zap className="w-3 h-3" /> },
|
||||
]).map(t => (
|
||||
<button
|
||||
key={t.key}
|
||||
@@ -376,7 +376,7 @@ function ContextSection({ label, value }: { label: string; value: any }) {
|
||||
>
|
||||
{open ? <ChevronDown className="w-3 h-3 text-slate-500 shrink-0" /> : <ChevronRight className="w-3 h-3 text-slate-500 shrink-0" />}
|
||||
<span className={clsx('text-xs font-mono font-semibold', labelColor[label] ?? 'text-slate-300')}>{label}</span>
|
||||
<span className="text-[10px] text-slate-600 ml-auto">{lineCount} lignes</span>
|
||||
<span className="text-[10px] text-slate-600 ml-auto">{lineCount} lines</span>
|
||||
</button>
|
||||
{open && (
|
||||
<pre className="text-[10px] font-mono text-slate-400 bg-slate-950 p-3 overflow-x-auto max-h-80 leading-relaxed">
|
||||
@@ -415,7 +415,7 @@ export default function SystemLogs() {
|
||||
}
|
||||
|
||||
const handleClear = () => {
|
||||
if (window.confirm('Supprimer les logs de plus de 30 jours ?')) {
|
||||
if (window.confirm('Delete logs older than 30 days?')) {
|
||||
clearLogs(30, { onSuccess: () => qc.invalidateQueries({ queryKey: ['system-logs'] }) })
|
||||
}
|
||||
}
|
||||
@@ -425,8 +425,8 @@ export default function SystemLogs() {
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-white">Logs Système</h1>
|
||||
<p className="text-xs text-slate-500 mt-0.5">Erreurs, avertissements et contextes complets des cycles IA</p>
|
||||
<h1 className="text-xl font-bold text-white">System Logs</h1>
|
||||
<p className="text-xs text-slate-500 mt-0.5">Errors, warnings and full AI cycle contexts</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
@@ -434,7 +434,7 @@ export default function SystemLogs() {
|
||||
className="btn-secondary flex items-center gap-1.5 text-xs px-3 py-1.5"
|
||||
>
|
||||
<RefreshCw className={clsx('w-3.5 h-3.5', (isLoading || isRefetching) && 'animate-spin')} />
|
||||
Actualiser
|
||||
Refresh
|
||||
</button>
|
||||
{activeTab === 'logs' && (
|
||||
<button
|
||||
@@ -443,7 +443,7 @@ export default function SystemLogs() {
|
||||
className="btn-secondary flex items-center gap-1.5 text-xs px-3 py-1.5 text-red-400 border-red-800/40 hover:bg-red-900/20"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
Purger >30j
|
||||
Purge >30d
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -452,8 +452,8 @@ export default function SystemLogs() {
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-1 border-b border-slate-800">
|
||||
{([
|
||||
{ key: 'logs', label: 'Logs système', icon: <Info className="w-3.5 h-3.5" /> },
|
||||
{ key: 'context', label: 'Contexte IA', icon: <Brain className="w-3.5 h-3.5" /> },
|
||||
{ key: 'logs', label: 'System Logs', icon: <Info className="w-3.5 h-3.5" /> },
|
||||
{ key: 'context', label: 'AI Context', icon: <Brain className="w-3.5 h-3.5" /> },
|
||||
] as const).map(tab => (
|
||||
<button
|
||||
key={tab.key}
|
||||
@@ -493,7 +493,7 @@ export default function SystemLogs() {
|
||||
{level} · {counts[level as keyof typeof counts] ?? 0}
|
||||
</button>
|
||||
))}
|
||||
<span className="text-xs text-slate-600 ml-auto">{logs.length} entrées</span>
|
||||
<span className="text-xs text-slate-600 ml-auto">{logs.length} entries</span>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
@@ -505,7 +505,7 @@ export default function SystemLogs() {
|
||||
value={filters.source ?? ''}
|
||||
onChange={e => applyFilter('source', e.target.value)}
|
||||
>
|
||||
<option value="">Toutes</option>
|
||||
<option value="">All</option>
|
||||
{sources.map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
@@ -517,7 +517,7 @@ export default function SystemLogs() {
|
||||
value={filters.cycle_id ?? ''}
|
||||
onChange={e => applyFilter('cycle_id', e.target.value)}
|
||||
>
|
||||
<option value="">Tous</option>
|
||||
<option value="">All</option>
|
||||
{cycles.map(c => (
|
||||
<option key={c.cycle_id} value={c.cycle_id}>
|
||||
{c.first_ts ? format(new Date(c.first_ts), 'dd/MM HH:mm') : c.cycle_id.slice(0, 16)}
|
||||
@@ -540,7 +540,7 @@ export default function SystemLogs() {
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className="text-[9px] text-slate-500 uppercase tracking-wide block mb-1">Depuis</label>
|
||||
<label className="text-[9px] text-slate-500 uppercase tracking-wide block mb-1">From</label>
|
||||
<input
|
||||
type="date"
|
||||
className="w-full bg-slate-800 border border-slate-700 rounded px-2 py-1 text-xs text-slate-300"
|
||||
@@ -549,7 +549,7 @@ export default function SystemLogs() {
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[9px] text-slate-500 uppercase tracking-wide block mb-1">Jusqu'au</label>
|
||||
<label className="text-[9px] text-slate-500 uppercase tracking-wide block mb-1">Until</label>
|
||||
<input
|
||||
type="date"
|
||||
className="w-full bg-slate-800 border border-slate-700 rounded px-2 py-1 text-xs text-slate-300"
|
||||
@@ -563,19 +563,19 @@ export default function SystemLogs() {
|
||||
{/* Log table */}
|
||||
<div className="card overflow-hidden">
|
||||
{isLoading ? (
|
||||
<div className="p-8 text-center text-slate-500 text-sm">Chargement…</div>
|
||||
<div className="p-8 text-center text-slate-500 text-sm">Loading…</div>
|
||||
) : logs.length === 0 ? (
|
||||
<div className="p-8 text-center text-slate-600 text-sm">
|
||||
Aucun log trouvé pour ces filtres.<br />
|
||||
<span className="text-xs text-slate-700">Les WARNING et ERROR des cycles apparaissent ici automatiquement.</span>
|
||||
No logs found for these filters.<br />
|
||||
<span className="text-xs text-slate-700">WARNING and ERROR entries from cycles appear here automatically.</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-700 text-[9px] uppercase tracking-wide text-slate-500">
|
||||
<th className="px-3 py-2 text-left font-medium whitespace-nowrap">Horodatage</th>
|
||||
<th className="px-3 py-2 text-left font-medium">Niveau</th>
|
||||
<th className="px-3 py-2 text-left font-medium whitespace-nowrap">Timestamp</th>
|
||||
<th className="px-3 py-2 text-left font-medium">Level</th>
|
||||
<th className="px-3 py-2 text-left font-medium">Source</th>
|
||||
<th className="px-3 py-2 text-left font-medium">Cycle</th>
|
||||
<th className="px-3 py-2 text-left font-medium">Ticker</th>
|
||||
|
||||
@@ -11,7 +11,7 @@ import clsx from 'clsx'
|
||||
|
||||
async function fetchLatest() {
|
||||
const r = await fetch('/api/var/latest')
|
||||
if (!r.ok) throw new Error(`Backend inaccessible (${r.status})`)
|
||||
if (!r.ok) throw new Error(`Backend unreachable (${r.status})`)
|
||||
return r.json()
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ function MetricCard({
|
||||
<div className="text-[11px] text-slate-500 mb-3">{method}</div>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<div className="text-[10px] text-slate-500 mb-0.5">VaR {horizon}j</div>
|
||||
<div className="text-[10px] text-slate-500 mb-0.5">VaR {horizon}d</div>
|
||||
<div className={clsx('text-2xl font-bold', displayed < 0 ? 'text-red-400' : 'text-emerald-400')}>
|
||||
{pct(displayed)}
|
||||
</div>
|
||||
@@ -101,10 +101,10 @@ function IdleState({ onCompute, computing }: { onCompute: () => void; computing:
|
||||
<div className="flex flex-col items-center justify-center py-24 gap-6">
|
||||
<ShieldAlert className="w-16 h-16 text-slate-700" />
|
||||
<div className="text-center">
|
||||
<h2 className="text-lg font-semibold text-slate-400 mb-2">Aucun snapshot VaR disponible</h2>
|
||||
<h2 className="text-lg font-semibold text-slate-400 mb-2">No VaR snapshot available</h2>
|
||||
<p className="text-slate-600 text-sm max-w-md">
|
||||
Le calcul VaR ne se lance pas automatiquement pour éviter des appels réseau inutiles.
|
||||
Cliquez sur "Calculer maintenant" ou configurez le scheduler dans la Configuration.
|
||||
The VaR calculation does not run automatically to avoid unnecessary network calls.
|
||||
Click "Compute now" or configure the scheduler in Settings.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -113,7 +113,7 @@ function IdleState({ onCompute, computing }: { onCompute: () => void; computing:
|
||||
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white px-6 py-3 rounded-lg font-semibold text-sm"
|
||||
>
|
||||
<Play className={clsx('w-4 h-4', computing && 'animate-pulse')} />
|
||||
{computing ? 'Calcul en cours…' : 'Calculer maintenant'}
|
||||
{computing ? 'Computing…' : 'Compute now'}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
@@ -180,15 +180,15 @@ export default function VaRAnalysis() {
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<ShieldAlert className="w-6 h-6 text-red-400" />
|
||||
<h1 className="text-xl font-bold text-white">Analyse VaR — Value at Risk</h1>
|
||||
<h1 className="text-xl font-bold text-white">VaR Analysis — Value at Risk</h1>
|
||||
</div>
|
||||
<p className="text-slate-400 text-sm">
|
||||
Approche delta Black-Scholes
|
||||
Delta Black-Scholes approach
|
||||
{displayedMeta && (
|
||||
<span className="ml-2 text-slate-500">
|
||||
· snapshot du {displayedMeta.computed_at?.slice(0, 16).replace('T', ' ')} UTC
|
||||
· snapshot from {displayedMeta.computed_at?.slice(0, 16).replace('T', ' ')} UTC
|
||||
{displayedMeta.data_source === 'simulated' && (
|
||||
<span className="ml-2 text-amber-400">⚠ données simulées</span>
|
||||
<span className="ml-2 text-amber-400">⚠ simulated data</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
@@ -199,7 +199,7 @@ export default function VaRAnalysis() {
|
||||
<div className="flex flex-wrap gap-3 items-end">
|
||||
{/* Confidence */}
|
||||
<div>
|
||||
<div className="text-[10px] text-slate-500 mb-1 uppercase tracking-wider">Confiance</div>
|
||||
<div className="text-[10px] text-slate-500 mb-1 uppercase tracking-wider">Confidence</div>
|
||||
<div className="flex gap-1">
|
||||
{[0.90, 0.95, 0.99].map(c => (
|
||||
<button key={c} onClick={() => setConfidence(c)}
|
||||
@@ -218,14 +218,14 @@ export default function VaRAnalysis() {
|
||||
<button key={h} onClick={() => setHorizon(h)}
|
||||
className={clsx('px-2.5 py-1.5 rounded text-xs font-semibold',
|
||||
horizon === h ? 'bg-violet-600 text-white' : 'bg-dark-700 text-slate-400 hover:text-white')}>
|
||||
{h}j
|
||||
{h}d
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* Lookback */}
|
||||
<div>
|
||||
<div className="text-[10px] text-slate-500 mb-1 uppercase tracking-wider">Historique</div>
|
||||
<div className="text-[10px] text-slate-500 mb-1 uppercase tracking-wider">Lookback</div>
|
||||
<div className="flex gap-1">
|
||||
{[63, 126, 252].map(l => (
|
||||
<button key={l} onClick={() => setLookback(l)}
|
||||
@@ -238,7 +238,7 @@ export default function VaRAnalysis() {
|
||||
</div>
|
||||
{/* IV */}
|
||||
<div>
|
||||
<div className="text-[10px] text-slate-500 mb-1 uppercase tracking-wider">IV défaut</div>
|
||||
<div className="text-[10px] text-slate-500 mb-1 uppercase tracking-wider">Default IV</div>
|
||||
<div className="flex gap-1">
|
||||
{[0.15, 0.20, 0.30, 0.40].map(v => (
|
||||
<button key={v} onClick={() => setIv(v)}
|
||||
@@ -256,7 +256,7 @@ export default function VaRAnalysis() {
|
||||
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white px-4 py-2 rounded text-sm font-semibold"
|
||||
>
|
||||
<Play className={clsx('w-3.5 h-3.5', computing && 'animate-pulse')} />
|
||||
{computing ? 'Calcul…' : 'Calculer'}
|
||||
{computing ? 'Computing…' : 'Compute'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -265,7 +265,7 @@ export default function VaRAnalysis() {
|
||||
{backendDown && (
|
||||
<div className="bg-red-900/20 border border-red-700/40 rounded-xl p-4 text-red-400 flex items-center gap-2 text-sm">
|
||||
<AlertTriangle className="w-4 h-4 shrink-0" />
|
||||
Backend inaccessible — vérifiez que le serveur FastAPI est démarré sur le port 8000.
|
||||
Backend unreachable — check that the FastAPI server is running on port 8000.
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -281,12 +281,12 @@ export default function VaRAnalysis() {
|
||||
{snapshots.length > 1 && (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Database className="w-3.5 h-3.5 text-slate-500" />
|
||||
<span className="text-xs text-slate-500">Historique :</span>
|
||||
<span className="text-xs text-slate-500">History:</span>
|
||||
<button
|
||||
onClick={() => setActiveSnap(null)}
|
||||
className={clsx('text-xs px-2 py-1 rounded', !activeSnap ? 'bg-blue-900/40 text-blue-400 border border-blue-700/40' : 'bg-dark-700 text-slate-500 hover:text-slate-300')}
|
||||
>
|
||||
Dernier
|
||||
Latest
|
||||
</button>
|
||||
{snapshots.slice(1).map((s: any) => (
|
||||
<button
|
||||
@@ -307,7 +307,7 @@ export default function VaRAnalysis() {
|
||||
|
||||
{/* Empty / loading state */}
|
||||
{loadingLatest && !displayedResult && (
|
||||
<div className="text-center py-16 text-slate-600">Chargement du dernier snapshot…</div>
|
||||
<div className="text-center py-16 text-slate-600">Loading latest snapshot…</div>
|
||||
)}
|
||||
|
||||
{!loadingLatest && !displayedResult && !backendDown && (
|
||||
@@ -322,9 +322,9 @@ export default function VaRAnalysis() {
|
||||
{[
|
||||
{ label: 'Positions', val: String(portfolio.n_positions) },
|
||||
{ label: 'Notionnel', val: `${portfolio.total_notional_eur.toLocaleString('fr-FR', { maximumFractionDigits: 0 })} €` },
|
||||
{ label: 'Confiance', val: `${portfolio.confidence_pct}%` },
|
||||
{ label: 'Lookback', val: `${portfolio.lookback_days}j` },
|
||||
{ label: 'Source', val: portfolio.data_source === 'simulated' ? '⚠ Simulée' : '✓ Live' },
|
||||
{ label: 'Confidence', val: `${portfolio.confidence_pct}%` },
|
||||
{ label: 'Lookback', val: `${portfolio.lookback_days}d` },
|
||||
{ label: 'Source', val: portfolio.data_source === 'simulated' ? '⚠ Simulated' : '✓ Live' },
|
||||
].map(({ label, val }) => (
|
||||
<div key={label} className="bg-dark-800 border border-slate-700/30 rounded-lg px-3 py-2">
|
||||
<div className="text-slate-500">{label}</div>
|
||||
@@ -336,21 +336,21 @@ export default function VaRAnalysis() {
|
||||
{/* 3 VaR method cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<MetricCard
|
||||
label="Historique" method="Percentile empirique"
|
||||
label="Historical" method="Empirical percentile"
|
||||
var1d={varData.historical.var_1d_pct} varNd={varData.historical.var_nd_pct}
|
||||
cvar={varData.historical.cvar_pct}
|
||||
varEur={varData.historical.var_1d_eur} cvarEur={varData.historical.cvar_eur}
|
||||
horizon={horizon} colorClass="border-blue-700/40"
|
||||
/>
|
||||
<MetricCard
|
||||
label="Paramétrique" method="Distribution normale"
|
||||
label="Parametric" method="Normal distribution"
|
||||
var1d={varData.parametric.var_1d_pct} varNd={varData.parametric.var_nd_pct}
|
||||
cvar={varData.parametric.cvar_pct}
|
||||
varEur={varData.parametric.var_1d_eur} cvarEur={varData.parametric.cvar_eur}
|
||||
horizon={horizon} colorClass="border-violet-700/40"
|
||||
/>
|
||||
<MetricCard
|
||||
label="Monte Carlo" method="Vol stressée ×1.5"
|
||||
label="Monte Carlo" method="Stressed vol ×1.5"
|
||||
var1d={varData.monte_carlo.var_1d_pct} varNd={varData.monte_carlo.var_nd_pct}
|
||||
cvar={varData.monte_carlo.cvar_pct}
|
||||
varEur={varData.monte_carlo.var_1d_eur} cvarEur={varData.monte_carlo.cvar_eur}
|
||||
@@ -364,8 +364,8 @@ export default function VaRAnalysis() {
|
||||
<div className="bg-dark-800 border border-slate-700/40 rounded-xl p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Activity className="w-4 h-4 text-slate-400" />
|
||||
<h3 className="text-sm font-semibold text-white">Distribution des Retours</h3>
|
||||
<span className="text-[10px] text-slate-600 ml-auto">trait rouge = VaR hist.</span>
|
||||
<h3 className="text-sm font-semibold text-white">Return Distribution</h3>
|
||||
<span className="text-[10px] text-slate-600 ml-auto">red line = hist. VaR</span>
|
||||
</div>
|
||||
{histogram.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height={210}>
|
||||
@@ -383,7 +383,7 @@ export default function VaRAnalysis() {
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="h-[210px] flex items-center justify-center text-slate-600 text-sm">Aucune donnée</div>
|
||||
<div className="h-[210px] flex items-center justify-center text-slate-600 text-sm">No data</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -391,7 +391,7 @@ export default function VaRAnalysis() {
|
||||
<div className="bg-dark-800 border border-slate-700/40 rounded-xl p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<TrendingDown className="w-4 h-4 text-red-400" />
|
||||
<h3 className="text-sm font-semibold text-white">VaR 95% Glissante (fenêtre 30J)</h3>
|
||||
<h3 className="text-sm font-semibold text-white">Rolling VaR 95% (30d window)</h3>
|
||||
</div>
|
||||
{rolling.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height={210}>
|
||||
@@ -406,7 +406,7 @@ export default function VaRAnalysis() {
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="h-[210px] flex items-center justify-center text-slate-600 text-sm">Historique insuffisant</div>
|
||||
<div className="h-[210px] flex items-center justify-center text-slate-600 text-sm">Insufficient history</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -415,7 +415,7 @@ export default function VaRAnalysis() {
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{/* Positions */}
|
||||
<div className="bg-dark-800 border border-slate-700/40 rounded-xl p-4">
|
||||
<h3 className="text-sm font-semibold text-white mb-3">Positions & Deltas</h3>
|
||||
<h3 className="text-sm font-semibold text-white mb-3">Positions & Deltas</h3>
|
||||
<div className="space-y-1.5">
|
||||
{positions.map((p: any, i: number) => (
|
||||
<div key={i} className="flex items-center justify-between text-xs py-1 border-b border-slate-700/20 last:border-0">
|
||||
@@ -442,13 +442,13 @@ export default function VaRAnalysis() {
|
||||
{/* Kupiec backtest */}
|
||||
{backtest && (
|
||||
<div className="bg-dark-800 border border-slate-700/40 rounded-xl p-4">
|
||||
<h3 className="text-sm font-semibold text-white mb-3">Backtest — Test de Kupiec</h3>
|
||||
<h3 className="text-sm font-semibold text-white mb-3">Backtest — Kupiec Test</h3>
|
||||
<div className="space-y-2.5">
|
||||
{[
|
||||
{ label: 'Observations', val: String(backtest.n_observations) },
|
||||
{ label: 'Violations VaR', val: String(backtest.n_breaches) },
|
||||
{ label: 'Taux réel', val: `${backtest.breach_rate_pct}%`, color: backtest.kupiec_ok ? 'text-emerald-400' : 'text-red-400' },
|
||||
{ label: 'Taux attendu', val: `${backtest.expected_breach_rate_pct}%`, color: 'text-slate-300' },
|
||||
{ label: 'VaR Violations', val: String(backtest.n_breaches) },
|
||||
{ label: 'Actual rate', val: `${backtest.breach_rate_pct}%`, color: backtest.kupiec_ok ? 'text-emerald-400' : 'text-red-400' },
|
||||
{ label: 'Expected rate', val: `${backtest.expected_breach_rate_pct}%`, color: 'text-slate-300' },
|
||||
].map(({ label, val, color }) => (
|
||||
<div key={label} className="flex justify-between text-sm">
|
||||
<span className="text-slate-400">{label}</span>
|
||||
@@ -460,11 +460,11 @@ export default function VaRAnalysis() {
|
||||
? 'bg-emerald-900/20 border border-emerald-700/40 text-emerald-400'
|
||||
: 'bg-red-900/20 border border-red-700/40 text-red-400')}>
|
||||
{backtest.kupiec_ok
|
||||
? '✓ Modèle validé — violations dans la tolérance'
|
||||
: '✗ Excès de violations — modèle sous-estime le risque'}
|
||||
? '✓ Model validated — violations within tolerance'
|
||||
: '✗ Excess violations — model underestimates risk'}
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-600">
|
||||
Règle Kupiec : taux réel ≤ {(backtest.expected_breach_rate_pct * 2).toFixed(1)}%
|
||||
Kupiec rule: actual rate ≤ {(backtest.expected_breach_rate_pct * 2).toFixed(1)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user