Files
OpenFin/frontend/src/pages/Portfolio.tsx
OpenSquared d256b65d30 Initial commit — GeoOptions Intelligence Cockpit v2.0
Stack: FastAPI + React/TypeScript + SQLite + GPT-4o
Features: Radar géopolitique, Marchés, Régime Macro, Journal de Bord MTM,
Rapport IA, Super Contexte (base de raisonnement évolutive), Boucle feedback IA.
Deploy: Docker + docker-compose + nginx pour openfin.open-squared.tech

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 20:29:59 +02:00

628 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import {
usePortfolioPositions, usePortfolioSummary, usePnlHistory,
useAddPosition, useClosePosition
} from '../hooks/useApi'
import { useQueryClient, useMutation } from '@tanstack/react-query'
import axios from 'axios'
import clsx from 'clsx'
import {
AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer,
CartesianGrid, ReferenceLine, BarChart, Bar
} from 'recharts'
import { TrendingUp, TrendingDown, Plus, X, DollarSign, BarChart2, RefreshCw, Trash2, ExternalLink, ChevronDown, ChevronUp } from 'lucide-react'
import type { TradeIdea } from '../types'
const useDeletePosition = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: (id: string) => axios.delete(`/api/portfolio/${id}`).then(r => r.data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['portfolio'] })
qc.invalidateQueries({ queryKey: ['portfolio-summary'] })
},
})
}
const STRATEGIES = ['Long Call', 'Long Put', 'Bull Call Spread', 'Bear Put Spread', 'Long Straddle', 'Long Strangle', 'Covered Call']
const ASSET_CLASSES = ['energy', 'metals', 'agriculture', 'equities', 'indices', 'forex']
interface AddModalProps {
prefill?: Partial<TradeIdea>
onClose: () => void
}
const STRADDLE_STRATEGIES = ['Long Straddle', 'Short Straddle', 'Long Strangle', 'Short Strangle']
function AddPositionModal({ prefill, onClose }: AddModalProps) {
const { mutate: addPos, isPending } = useAddPosition()
const [addError, setAddError] = useState<string | null>(null)
const [form, setForm] = useState({
title: prefill?.title || '',
underlying: prefill?.underlying || '',
strategy: prefill?.strategy || 'Long Call',
asset_class: prefill?.asset_class || 'indices',
expiry_days: prefill?.horizon_days || 90,
capital_invested: prefill?.capital_required || 1000,
geo_trigger: prefill?.geo_trigger || '',
rationale: prefill?.rationale || '',
strike: '',
option_type: 'call',
quantity: 1,
premium: '',
})
const set = (k: string, v: unknown) => setForm(f => ({ ...f, [k]: v }))
const isStraddle = STRADDLE_STRATEGIES.includes(form.strategy)
const isShort = form.strategy.startsWith('Short')
const submit = () => {
setAddError(null)
const strikeVal = parseFloat(form.strike) || undefined
const premiumVal = parseFloat(form.premium) || undefined
const legs = isStraddle
? [
{ strike: strikeVal, option_type: 'call', quantity: form.quantity, position: isShort ? 'short' : 'long', premium_paid: premiumVal },
{ strike: strikeVal, option_type: 'put', quantity: form.quantity, position: isShort ? 'short' : 'long', premium_paid: premiumVal },
]
: [{
strike: strikeVal,
option_type: form.option_type,
quantity: form.quantity,
position: form.strategy.startsWith('Short') ? 'short' : 'long',
premium_paid: premiumVal,
}]
addPos({
title: form.title || `${form.strategy} ${form.underlying}`,
underlying: form.underlying,
strategy: form.strategy,
asset_class: form.asset_class,
expiry_days: form.expiry_days,
capital_invested: form.capital_invested,
geo_trigger: form.geo_trigger,
rationale: form.rationale,
legs,
}, {
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')
}
})
}
return (
<div className="fixed inset-0 bg-black/70 z-50 flex items-center justify-center p-4">
<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
</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>
<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>
</label>
<input value={form.underlying} onChange={e => { set('underlying', e.target.value.toUpperCase()); setAddError(null) }}
placeholder="^GSPC, GLD, CL=F..."
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500" />
<div className="text-xs text-slate-600 mt-0.5">S&amp;P 500: ^GSPC · Or: GC=F · WTI: CL=F · GLD · USO</div>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs text-slate-500 mb-1 block">Stratégie</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>
<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>)}
</select>
</div>
</div>
<div className="grid grid-cols-3 gap-3">
<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"
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">Type</label>
<select value={form.option_type} onChange={e => set('option_type', 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="call">Call</option>
<option value="put">Put</option>
</select>
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Prime payée</label>
<input type="number" value={form.premium} onChange={e => set('premium', e.target.value)}
placeholder="Prix option"
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>
<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>
<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>
<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>
<textarea value={form.rationale} onChange={e => set('rationale', e.target.value)}
rows={2} placeholder="Pourquoi ce 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)
</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>
{addError && (
<div className="bg-red-900/40 border border-red-700/40 rounded p-2 text-xs text-red-300">
{addError}
</div>
)}
<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'}
</button>
</div>
</div>
</div>
)
}
function PositionCard({ pos }: { pos: Record<string, any> }) {
const navigate = useNavigate()
const [showClose, setShowClose] = useState(false)
const [showDetails, setShowDetails] = useState(false)
const [closeVal, setCloseVal] = useState('')
const { mutate: closePos, isPending: closing } = useClosePosition()
const { mutate: deletePos, isPending: deleting } = useDeletePosition()
const pnl = pos.pnl ?? 0
const pnlPct = pos.pnl_pct ?? 0
const isProfit = pnl >= 0
const entryRef = pos.entry_ref ?? pos.capital_invested
const hasPremium = pos.entry_ref != null && pos.entry_ref !== pos.capital_invested
return (
<div className={clsx('card transition-all', {
'border-emerald-700/40': isProfit && pnl !== 0,
'border-red-700/40': !isProfit && pnl !== 0,
})}>
{/* Header */}
<div className="flex items-start justify-between mb-2">
<div className="flex-1 min-w-0">
<div className="text-sm font-semibold text-white">{pos.title}</div>
<div className="flex items-center gap-2 mt-0.5 flex-wrap">
<span className="badge badge-blue">{pos.strategy}</span>
<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"
>
{pos.underlying} <ExternalLink className="w-2.5 h-2.5" />
</button>
<span className="text-xs text-slate-600">{pos.entry_date?.slice(0, 10)}</span>
{pos.sigma_used && (
<span className="text-xs text-slate-600">IV {(pos.sigma_used * 100).toFixed(1)}%</span>
)}
</div>
</div>
<div className="flex items-center gap-2 ml-3 shrink-0">
<div className="text-right">
<div className={clsx('text-lg font-bold', isProfit ? 'positive' : 'negative')}>
{pnl >= 0 ? '+' : ''}{pnl.toFixed(2)}€
</div>
<div className={clsx('text-xs font-mono', isProfit ? 'positive' : 'negative')}>
{pnlPct >= 0 ? '+' : ''}{pnlPct.toFixed(2)}%
</div>
</div>
<button onClick={() => deletePos(pos.id)} disabled={deleting}
className="text-slate-600 hover:text-red-400 transition-colors" title="Supprimer">
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
{/* 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-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-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-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={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` : '—'}
</div>
</div>
</div>
{/* Greeks + fees */}
{pos.greeks && (
<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-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>
</div>
)}
{/* Contract detail toggle */}
{pos.legs && pos.legs.length > 0 && (
<button
onClick={() => setShowDetails(s => !s)}
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
</button>
)}
{/* Contract details panel */}
{showDetails && pos.legs && pos.legs.length > 0 && (
<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>
)}
{pos.sigma_used != null && (
<span>σ (IV histo): <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>
)}
</div>
<table className="w-full mb-2">
<thead>
<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>
</tr>
</thead>
<tbody>
{pos.legs.map((leg: any, i: number) => {
const legTotal = leg.premium_paid != null
? leg.premium_paid * (leg.quantity ?? 1) * 100
: null
return (
<tr key={i} className="border-t border-slate-700/20">
<td className="py-1">
<span className={clsx(
'badge text-xs',
(leg.option_type ?? 'call') === 'call' ? 'badge-green' : 'badge-red'
)}>
{(leg.option_type ?? 'call').toUpperCase()}
</span>
<span className="ml-1 text-slate-500">
{leg.position === 'long' ? '▲' : '▼'} {leg.position ?? 'long'}
</span>
</td>
<td className="py-1 font-mono text-slate-300">
{leg.strike != null ? `$${Number(leg.strike).toFixed(2)}` : 'ATM'}
</td>
<td className="py-1 text-right text-slate-300">{leg.quantity ?? 1}</td>
<td className="py-1 text-right font-mono text-slate-300">
{leg.premium_paid != null ? `$${Number(leg.premium_paid).toFixed(4)}` : '—'}
</td>
<td className="py-1 text-right font-mono text-white">
{legTotal != null ? `$${legTotal.toFixed(2)}` : '—'}
</td>
</tr>
)
})}
</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%)
</div>
</div>
)}
{/* Geo trigger */}
{pos.geo_trigger && (
<div className="text-xs text-slate-500 mb-2">
<span className="text-slate-600">⚡ Trigger: </span>{pos.geo_trigger}
</div>
)}
{/* P&L bar */}
<div className="mb-3">
<div className="bg-dark-600 rounded-full h-1.5 relative overflow-hidden">
<div className="absolute inset-y-0 left-1/2 w-px bg-slate-500 z-10"></div>
<div
className={clsx('h-1.5 rounded-full transition-all absolute top-0', isProfit ? 'bg-emerald-500' : 'bg-red-500')}
style={{
width: `${Math.min(50, Math.abs(pnlPct) / 4)}%`,
left: isProfit ? '50%' : `${50 - Math.min(50, Math.abs(pnlPct) / 4)}%`,
}}
/>
</div>
<div className="flex justify-between text-xs text-slate-700 mt-0.5">
<span>-100%</span><span>0</span><span>+100%</span>
</div>
</div>
{/* Actions */}
<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'}
</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="flex gap-2">
<input type="number" value={closeVal} onChange={e => setCloseVal(e.target.value)}
placeholder={`Valeur actuelle: ~${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'}
</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)
</div>
)}
</div>
)}
</div>
)
}
function ClosedRow({ pos, fees, pnl }: { pos: Record<string, any>; fees: number; pnl: number | null }) {
const { mutate: deletePos, isPending } = useDeletePosition()
return (
<tr className="border-b border-slate-700/20 group">
<td className="py-1.5 text-white">{pos.title}</td>
<td className="py-1.5 text-slate-400">{pos.strategy}</td>
<td className="py-1.5 text-right font-mono">{pos.capital_invested}€</td>
<td className="py-1.5 text-right font-mono">{pos.close_value?.toFixed(2) ?? '—'}€</td>
<td className="py-1.5 text-right font-mono text-orange-400">{fees.toFixed(2)}€</td>
<td className={clsx('py-1.5 text-right font-mono font-bold', pnl != null ? (pnl >= 0 ? 'positive' : 'negative') : 'text-slate-500')}>
{pnl != null ? `${pnl >= 0 ? '+' : ''}${pnl.toFixed(2)}€` : '—'}
</td>
<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">
<Trash2 className="w-3.5 h-3.5" />
</button>
</td>
</tr>
)
}
export default function Portfolio() {
const { data: positions, isLoading, refetch } = usePortfolioPositions('open')
const { data: closed } = usePortfolioPositions('closed')
const { data: summary } = usePortfolioSummary()
const { data: pnlHistory } = usePnlHistory()
const [showModal, setShowModal] = useState(false)
const [tab, setTab] = useState<'open' | 'closed' | 'history'>('open')
return (
<div className="p-6 space-y-5">
<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
</h1>
<p className="text-xs text-slate-500 mt-0.5">
Suivi temps réel · Mark-to-market Black-Scholes · Frais IB simulés
</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
</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
</button>
</div>
</div>
{/* Summary KPIs */}
{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' },
].map(({ label, value, color }) => (
<div key={label} className="card-sm text-center">
<div className="stat-label">{label}</div>
<div className={clsx('text-lg font-bold mt-1', color)}>{value}</div>
</div>
))}
</div>
)}
{/* 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' },
].map(({ key, label }) => (
<button key={key} onClick={() => setTab(key as any)}
className={clsx('px-3 py-1.5 rounded text-sm transition-colors', {
'bg-blue-600 text-white': tab === key,
'text-slate-400 hover:text-slate-200': tab !== key,
})}>
{label}
</button>
))}
</div>
{/* Open positions */}
{tab === 'open' && (
<>
{isLoading ? (
<div className="space-y-3">
{[1,2,3].map(i => <div key={i} className="card animate-pulse h-32 bg-dark-700" />)}
</div>
) : positions && positions.length > 0 ? (
<div className="space-y-3">
{(positions as Record<string, any>[]).map(pos => (
<PositionCard key={pos.id} pos={pos} />
))}
</div>
) : (
<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>
)}
</>
)}
{/* Closed positions */}
{tab === 'closed' && (
<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">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="pb-2"></th>
</tr>
</thead>
<tbody>
{(closed as Record<string, any>[] || []).map(pos => {
const fees = (pos.ib_fees_entry || 0) + (pos.ib_fees_exit || 0)
const pnl = pos.close_value != null ? pos.close_value - pos.capital_invested - fees : null
return (
<ClosedRow key={pos.id} pos={pos} fees={fees} pnl={pnl} />
)
})}
{(!closed || closed.length === 0) && (
<tr><td colSpan={8} className="py-8 text-center text-slate-600">Aucun trade clôturé</td></tr>
)}
</tbody>
</table>
</div>
)}
{/* P&L history */}
{tab === 'history' && (
<div className="card">
<div className="section-title">Courbe de P&L cumulé (trades réalisés)</div>
{pnlHistory && pnlHistory.length > 0 ? (
<>
<ResponsiveContainer width="100%" height={250}>
<AreaChart data={pnlHistory}>
<defs>
<linearGradient id="pnl-grad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#10b981" stopOpacity={0.3} />
<stop offset="95%" stopColor="#10b981" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#1e2d4d" />
<XAxis dataKey="date" tick={{ fill: '#475569', fontSize: 9 }} tickLine={false} />
<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é']} />
<ReferenceLine y={0} stroke="#475569" strokeDasharray="4 4" />
<Area type="monotone" dataKey="cumulative" stroke="#10b981" fill="url(#pnl-grad)" strokeWidth={2} dot={false} />
</AreaChart>
</ResponsiveContainer>
<ResponsiveContainer width="100%" height={120} className="mt-2">
<BarChart data={pnlHistory}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e2d4d" />
<XAxis dataKey="date" tick={{ fill: '#475569', fontSize: 8 }} />
<YAxis tick={{ fill: '#475569', fontSize: 8 }} tickFormatter={v => `${v}€`} />
<Tooltip contentStyle={{ background: '#0f1623', border: '1px solid #1e2d4d', fontSize: 10 }}
formatter={(v: number) => [`${v >= 0 ? '+' : ''}${v.toFixed(2)}€`, 'P&L']} />
<Bar dataKey="pnl" fill="#3b82f6" radius={[2,2,0,0]}
label={false} />
</BarChart>
</ResponsiveContainer>
</>
) : (
<div className="h-40 flex items-center justify-center text-slate-600 text-sm">
Clôturer des positions pour voir la courbe P&L
</div>
)}
</div>
)}
{showModal && <AddPositionModal onClose={() => setShowModal(false)} />}
</div>
)
}