Files
OpenFin/frontend/src/pages/Portfolio.tsx
OpenSquared dcbc9f19fc 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>
2026-06-22 09:06:37 +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 || 'Error adding 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" /> 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">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">
Underlying <span className="text-slate-600">(Yahoo Finance ticker)</span>
</label>
<input value={form.underlying} onChange={e => { set('underlying', e.target.value.toUpperCase()); setAddError(null) }}
placeholder="^GSPC, GLD, CL=F..."
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500" />
<div className="text-xs text-slate-600 mt-0.5">S&amp;P 500: ^GSPC · 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">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">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>)}
</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="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>
<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">Premium paid</label>
<input type="number" value={form.premium} onChange={e => set('premium', e.target.value)}
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 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">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">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">Rationale</label>
<textarea value={form.rationale} onChange={e => set('rationale', e.target.value)}
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 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">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">
{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 ? 'Adding...' : '+ Add to portfolio'}
</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="View in markets"
>
{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="Delete">
<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 ? '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">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">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">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}d` : '—'}
</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>/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">IB fees: <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" />}
Black-Scholes simulation detail
</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>Entry spot: <span className="text-slate-300 font-mono">${Number(pos.entry_underlying_price).toFixed(2)}</span></span>
)}
{pos.sigma_used != null && (
<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>Initial duration: <span className="text-slate-300">{pos.expiry_days}d</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">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>
{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 contract = 100 shares · Price computed at entry time (spot, historical IV, 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 ? 'Cancel' : '💰 Close position'}
</button>
</div>
{showClose && (
<div className="mt-2 space-y-1">
<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={`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 ? '...' : 'Confirm'}
</button>
</div>
{closeVal && (
<div className={clsx('text-xs', parseFloat(closeVal) - entryRef - (pos.ib_fees_entry||0) - 1 >= 0 ? 'positive' : 'negative')}>
Net IB P&L: {(parseFloat(closeVal) - entryRef - (pos.ib_fees_entry||0) - 1).toFixed(2)}
(exit fees: ~$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="Delete">
<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" /> Portfolio
</h1>
<p className="text-xs text-slate-500 mt-0.5">
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" /> 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" /> New position
</button>
</div>
</div>
{/* Summary KPIs */}
{summary && (
<div className="grid grid-cols-5 gap-3">
{[
{ 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>
<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: `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', {
'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>No open positions</div>
<div className="text-xs mt-1">Add a trade from ideas or manually</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">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>
<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">No closed trades</td></tr>
)}
</tbody>
</table>
</div>
)}
{/* P&L history */}
{tab === 'history' && (
<div className="card">
<div className="section-title">Cumulative P&L curve (realized trades)</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)}`, '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>
</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">
Close positions to see the P&L curve
</div>
)}
</div>
)}
{showModal && <AddPositionModal onClose={() => setShowModal(false)} />}
</div>
)
}