import type { BacktestSignal } from './backendApi'; export interface EquityPoint { time: number; equity: number; } export interface TradeMarker { time: number; equity: number; type: 'buy' | 'sell'; price: number; pnlPct?: number; } export interface TradeHistoryRow { id: number; time: number; symbol: string; side: 'buy' | 'sell'; price: number; quantity: number; pnlPct?: number; } const BUY_TYPES = new Set(['BUY']); const SELL_TYPES = new Set(['SELL', 'SHORT_EXIT', 'PARTIAL_SELL']); export function buildEquityFromSignals( signals: BacktestSignal[], initialCapital: number, symbol: string, ): { curve: EquityPoint[]; markers: TradeMarker[]; trades: TradeHistoryRow[] } { const sorted = [...signals].sort((a, b) => a.time - b.time); const curve: EquityPoint[] = []; const markers: TradeMarker[] = []; const trades: TradeHistoryRow[] = []; if (sorted.length === 0) { const now = Math.floor(Date.now() / 1000); return { curve: [{ time: now, equity: initialCapital }], markers: [], trades: [], }; } let cash = initialCapital; let qty = 0; let entryPrice = 0; let tradeId = 0; const pushCurve = (time: number, price: number) => { const equity = qty > 0 ? qty * price : cash; const last = curve[curve.length - 1]; if (!last || last.time !== time || Math.abs(last.equity - equity) > 0.01) { curve.push({ time, equity }); } }; pushCurve(sorted[0].time, sorted[0].price); for (const s of sorted) { if (BUY_TYPES.has(s.type) && qty === 0 && cash > 0) { qty = cash / s.price; entryPrice = s.price; cash = 0; tradeId += 1; const eq = qty * s.price; trades.push({ id: tradeId, time: s.time, symbol, side: 'buy', price: s.price, quantity: qty, }); markers.push({ time: s.time, equity: eq, type: 'buy', price: s.price }); pushCurve(s.time, s.price); } else if (SELL_TYPES.has(s.type) && qty > 0) { const pnlPct = entryPrice > 0 ? (s.price - entryPrice) / entryPrice : 0; cash = qty * s.price; tradeId += 1; trades.push({ id: tradeId, time: s.time, symbol, side: 'sell', price: s.price, quantity: qty, pnlPct, }); markers.push({ time: s.time, equity: cash, type: 'sell', price: s.price, pnlPct }); qty = 0; entryPrice = 0; pushCurve(s.time, s.price); } else { pushCurve(s.time, s.price); } } if (curve.length === 1 && sorted.length > 0) { const last = sorted[sorted.length - 1]; const equity = qty > 0 ? qty * last.price : cash; curve.push({ time: last.time, equity }); } return { curve, markers, trades }; } export function sparklinePath(values: number[], w: number, h: number): string { if (values.length < 2) { const y = h * 0.5; return `M0,${y} L${w},${y}`; } const min = Math.min(...values); const max = Math.max(...values); const range = max - min || 1; return values.map((v, i) => { const x = (i / (values.length - 1)) * w; const y = h - 4 - ((v - min) / range) * (h - 8); return `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`; }).join(' '); } export function equityToSparkline(curve: EquityPoint[]): number[] { if (curve.length <= 1) return [0, 1]; return curve.map(p => p.equity); }