feat: risk
This commit is contained in:
@@ -2,14 +2,14 @@ import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import {
|
||||
usePortfolioPositions, usePortfolioSummary, usePnlHistory,
|
||||
useAddPosition, useClosePosition
|
||||
useAddPosition, useClosePosition, usePositionPayoff
|
||||
} 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
|
||||
CartesianGrid, ReferenceLine, BarChart, Bar, LineChart, Line
|
||||
} from 'recharts'
|
||||
import { TrendingUp, TrendingDown, Plus, X, DollarSign, BarChart2, RefreshCw, Trash2, ExternalLink, ChevronDown, ChevronUp } from 'lucide-react'
|
||||
import type { TradeIdea } from '../types'
|
||||
@@ -25,6 +25,12 @@ const useDeletePosition = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const PRICING_SOURCE_LABELS: Record<string, string> = {
|
||||
saxo_quote: 'Cotation Saxo réelle',
|
||||
saxo_surface: 'Surface de vol Saxo',
|
||||
yfinance_bs: 'BS (vol yfinance)',
|
||||
}
|
||||
|
||||
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']
|
||||
|
||||
@@ -205,6 +211,58 @@ function AddPositionModal({ prefill, onClose }: AddModalProps) {
|
||||
)
|
||||
}
|
||||
|
||||
// P&L-vs-underlying-price payoff diagram — "at expiry" (pure intrinsic, no vol) vs "today"
|
||||
// (Black-Scholes reprice holding each leg's current implied vol fixed, from the real Saxo
|
||||
// surface when the option chain is linked — see services.portfolio_pricing.compute_payoff).
|
||||
function PositionPayoffChart({ posId, enabled, legs }: { posId: string; enabled: boolean; legs: any[] }) {
|
||||
const { data, isLoading } = usePositionPayoff(posId, enabled)
|
||||
if (!enabled) return null
|
||||
if (isLoading) return <div className="text-slate-600 text-center py-6 mt-2">Calcul du payoff…</div>
|
||||
if (!data || !data.spot_range?.length) return null
|
||||
|
||||
const chartData = data.spot_range.map((s: number, i: number) => ({
|
||||
spot: s, atExpiry: data.at_expiry[i], today: data.today[i],
|
||||
}))
|
||||
const strikes: number[] = data.strikes ?? []
|
||||
|
||||
return (
|
||||
<div className="mt-3 pt-3 border-t border-slate-700/30">
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<span className="text-slate-500 uppercase tracking-wide text-[10px]">Payoff — P&L vs. spot sous-jacent</span>
|
||||
<span className={clsx('text-[10px]', data.pricing_source === 'yfinance_bs' ? 'text-amber-400' : 'text-emerald-400')}>
|
||||
{data.pricing_source === 'yfinance_bs' ? 'Vol yfinance (pas de chain Saxo lié)' : 'Vol Saxo réelle'}
|
||||
</span>
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={180}>
|
||||
<LineChart data={chartData} margin={{ top: 4, right: 8, left: -14, bottom: 0 }}>
|
||||
<CartesianGrid stroke="#1e2d4d" strokeDasharray="3 3" />
|
||||
<XAxis dataKey="spot" tick={{ fontSize: 9, fill: '#64748b' }}
|
||||
tickFormatter={(v: number) => v.toFixed(v >= 1000 ? 0 : 2)} />
|
||||
<YAxis tick={{ fontSize: 9, fill: '#64748b' }} tickFormatter={(v: number) => `${(v / 1000).toFixed(0)}k`} />
|
||||
<Tooltip
|
||||
contentStyle={{ background: '#0f172a', border: '1px solid #334155', borderRadius: 6, fontSize: 10, padding: '4px 8px' }}
|
||||
formatter={(v: number, name: string) => [`${v.toFixed(2)}€`, name === 'atExpiry' ? 'À échéance' : "Aujourd'hui"]}
|
||||
labelFormatter={(v: number) => `Spot ${v.toFixed(2)}`}
|
||||
/>
|
||||
<ReferenceLine y={0} stroke="#475569" />
|
||||
{data.current_spot != null && <ReferenceLine x={data.current_spot} stroke="#3b82f6" strokeDasharray="4 2" label={{ value: 'Spot', fontSize: 9, fill: '#3b82f6', position: 'top' }} />}
|
||||
{data.entry_spot != null && <ReferenceLine x={data.entry_spot} stroke="#64748b" strokeDasharray="2 2" />}
|
||||
{strikes.map((k: number) => (
|
||||
<ReferenceLine key={k} x={k} stroke="#f59e0b" strokeOpacity={0.4} strokeDasharray="2 2" />
|
||||
))}
|
||||
<Line type="monotone" dataKey="atExpiry" stroke="#22c55e" strokeWidth={1.5} dot={false} isAnimationActive={false} name="atExpiry" />
|
||||
<Line type="monotone" dataKey="today" stroke="#3b82f6" strokeWidth={1.5} strokeDasharray="4 2" dot={false} isAnimationActive={false} name="today" />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
<div className="flex items-center gap-3 text-[9px] text-slate-600 mt-1">
|
||||
<span className="flex items-center gap-1"><span className="w-2.5 h-0.5 bg-emerald-500 inline-block" /> À échéance</span>
|
||||
<span className="flex items-center gap-1"><span className="w-2.5 h-0.5 bg-blue-500 inline-block" style={{ borderTop: '1.5px dashed #3b82f6', background: 'none' }} /> Aujourd'hui (vol actuelle)</span>
|
||||
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-amber-500/40 inline-block" /> Strike{legs.length > 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PositionCard({ pos }: { pos: Record<string, any> }) {
|
||||
const navigate = useNavigate()
|
||||
const [showClose, setShowClose] = useState(false)
|
||||
@@ -307,7 +365,14 @@ 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" />}
|
||||
Black-Scholes simulation detail
|
||||
Détail de la valorisation
|
||||
{pos.pricing_source_summary && (
|
||||
<span className={clsx('ml-1 px-1.5 py-0.5 rounded text-[10px] font-normal',
|
||||
pos.pricing_source_summary === 'yfinance_bs' ? 'bg-amber-900/30 text-amber-400' : 'bg-emerald-900/30 text-emerald-400'
|
||||
)}>
|
||||
{PRICING_SOURCE_LABELS[pos.pricing_source_summary] ?? pos.pricing_source_summary}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -319,7 +384,7 @@ function PositionCard({ pos }: { pos: Record<string, any> }) {
|
||||
<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>σ (moy. legs): <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 && (
|
||||
@@ -334,6 +399,7 @@ function PositionCard({ pos }: { pos: Record<string, any> }) {
|
||||
<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>
|
||||
<th className="text-right pb-1 font-normal">Source</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -364,14 +430,23 @@ function PositionCard({ pos }: { pos: Record<string, any> }) {
|
||||
<td className="py-1 text-right font-mono text-white">
|
||||
{legTotal != null ? `$${legTotal.toFixed(2)}` : '—'}
|
||||
</td>
|
||||
<td className="py-1 text-right">
|
||||
{leg.pricing_source && (
|
||||
<span className={clsx('text-[10px]', leg.pricing_source === 'yfinance_bs' ? 'text-amber-400' : 'text-emerald-400')}>
|
||||
{PRICING_SOURCE_LABELS[leg.pricing_source] ?? leg.pricing_source}
|
||||
</span>
|
||||
)}
|
||||
</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%)
|
||||
1 contract = 100 shares · r=5% · Prix réel Saxo si l'option chain de cet instrument est liée (Config → Instruments Watchlist), sinon Black-Scholes sur vol historique yfinance
|
||||
</div>
|
||||
|
||||
<PositionPayoffChart posId={pos.id} enabled={showDetails} legs={pos.legs} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -473,7 +548,7 @@ export default function Portfolio() {
|
||||
<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
|
||||
Real-time tracking · Mark-to-market Saxo (option chain lié) ou Black-Scholes · Simulated IB fees
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
|
||||
Reference in New Issue
Block a user