feat: strategy builder
This commit is contained in:
@@ -1784,7 +1784,7 @@ export type VannaSimulation = {
|
||||
}
|
||||
export type PayoffPoint = { underlying: number; pnl: number }
|
||||
export type TimeSlice = { days_from_now: number; label: string; points: PayoffPoint[] }
|
||||
export type PayoffHeatmap = { prices: number[]; rows: { days_from_now: number; pnl: number[] }[] }
|
||||
export type PayoffHeatmap = { prices: number[]; rows: { days_from_now: number; pnl: number[] }[]; breakeven_prices: number[] }
|
||||
|
||||
export type PriceCombo = {
|
||||
entry_cost: number
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
LineChart, Line, AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ReferenceLine, ResponsiveContainer,
|
||||
} from 'recharts'
|
||||
import { Layers, Plus, Trash2, RefreshCw, AlertTriangle, Search, Save, FolderOpen, X, History } from 'lucide-react'
|
||||
import { Layers, Plus, Trash2, RefreshCw, AlertTriangle, Search, Save, FolderOpen, X, History, ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
import {
|
||||
useOptionChainSlice, usePriceStrategy, useOptimizeStrategy, useSuggestedProfile, useReplayStrategy, usePresets, useRealizedScenario,
|
||||
@@ -129,7 +129,12 @@ function TimeDecayChart({ timeSlices, spot, scenarioSpot }: { timeSlices: TimeSl
|
||||
// "Heatmap" view — same payoff grid (services.strategy_engine.payoff_heatmap) as a
|
||||
// price x days-to-expiry table instead of curves. Cell shade encodes sign + magnitude
|
||||
// relative to the grid's own max |P&L|, scaled independently each time (not a fixed
|
||||
// P&L->color scale) since strategies span wildly different notional sizes.
|
||||
// P&L->color scale) since strategies span wildly different notional sizes. The backend
|
||||
// already centers/scales the price columns on spot and the legs' own strikes and pins in
|
||||
// the exact breakeven(s) — this view shows a 9-wide window over that wider grid (arrows
|
||||
// to pan) and highlights the breakeven column(s) instead of leaving them to blend in.
|
||||
const HEATMAP_WINDOW = 9
|
||||
|
||||
function PayoffHeatmapView({ heatmap, spot }: { heatmap: PayoffHeatmap; spot: number }) {
|
||||
const decimals = spot < 5 ? 4 : spot < 50 ? 2 : 0
|
||||
const maxAbs = Math.max(1, ...heatmap.rows.flatMap(r => r.pnl.map(Math.abs)))
|
||||
@@ -139,28 +144,64 @@ function PayoffHeatmapView({ heatmap, spot }: { heatmap: PayoffHeatmap; spot: nu
|
||||
}
|
||||
const rowLabel = (daysFromNow: number, i: number) =>
|
||||
daysFromNow < 0.5 ? "Aujourd'hui" : i === heatmap.rows.length - 1 ? 'Échéance' : `J+${Math.round(daysFromNow)}`
|
||||
const isBreakeven = (p: number) => heatmap.breakeven_prices.some(be => Math.abs(be - p) < 1e-6)
|
||||
|
||||
const total = heatmap.prices.length
|
||||
// Centers the window on spot once, on mount — deliberately not re-centered on every
|
||||
// reprice (scrubbing/tweaking a leg would otherwise yank the scroll position back under
|
||||
// the user while they're mid-interaction with the arrows below).
|
||||
const [windowStart, setWindowStart] = useState(() => {
|
||||
const closest = heatmap.prices.reduce((best, p, i) => Math.abs(p - spot) < Math.abs(heatmap.prices[best] - spot) ? i : best, 0)
|
||||
return Math.max(0, Math.min(total - HEATMAP_WINDOW, closest - Math.floor(HEATMAP_WINDOW / 2)))
|
||||
})
|
||||
const start = Math.max(0, Math.min(windowStart, Math.max(total - HEATMAP_WINDOW, 0)))
|
||||
const visible = Array.from({ length: Math.min(HEATMAP_WINDOW, total) }, (_, i) => start + i)
|
||||
const scroll = (dir: -1 | 1) => setWindowStart(s => Math.max(0, Math.min(Math.max(total - HEATMAP_WINDOW, 0), s + dir * 3)))
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<thead>
|
||||
<tr className="text-slate-500 text-left">
|
||||
<th className="py-1 pr-3">Jours</th>
|
||||
{heatmap.prices.map((p, i) => <th key={i} className="py-1 px-2 text-right">{p.toFixed(decimals)}</th>)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{heatmap.rows.map((row, ri) => (
|
||||
<tr key={ri} className="border-t border-slate-700/30">
|
||||
<td className="py-1 pr-3 text-slate-400 whitespace-nowrap">{rowLabel(row.days_from_now, ri)}</td>
|
||||
{row.pnl.map((v, ci) => (
|
||||
<td key={ci} className="py-1 px-2 text-right font-mono text-white" style={{ backgroundColor: cellBg(v) }}>
|
||||
{fmtMoney(v)}
|
||||
</td>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => scroll(-1)} disabled={start <= 0}
|
||||
className="p-1 rounded border border-slate-700/50 text-slate-400 hover:text-white disabled:opacity-30 shrink-0"
|
||||
>
|
||||
<ChevronLeft className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<div className="overflow-x-auto flex-1">
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<thead>
|
||||
<tr className="text-slate-500 text-left">
|
||||
<th className="py-1 pr-3">Jours</th>
|
||||
{visible.map(i => (
|
||||
<th key={i} className={clsx('py-1 px-2 text-right', isBreakeven(heatmap.prices[i]) && 'text-amber-400')}>
|
||||
{heatmap.prices[i].toFixed(decimals)}
|
||||
{isBreakeven(heatmap.prices[i]) && <div className="text-[9px] font-normal normal-case">breakeven</div>}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{heatmap.rows.map((row, ri) => (
|
||||
<tr key={ri} className="border-t border-slate-700/30">
|
||||
<td className="py-1 pr-3 text-slate-400 whitespace-nowrap">{rowLabel(row.days_from_now, ri)}</td>
|
||||
{visible.map(i => (
|
||||
<td key={i}
|
||||
className={clsx('py-1 px-2 text-right font-mono text-white', isBreakeven(heatmap.prices[i]) && 'border-x border-amber-500/60')}
|
||||
style={{ backgroundColor: cellBg(row.pnl[i]) }}
|
||||
>
|
||||
{fmtMoney(row.pnl[i])}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => scroll(1)} disabled={start + HEATMAP_WINDOW >= total}
|
||||
className="p-1 rounded border border-slate-700/50 text-slate-400 hover:text-white disabled:opacity-30 shrink-0"
|
||||
>
|
||||
<ChevronRight className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user