Files
goldenChart/frontend/src/utils/backtestEquity.ts
T
2026-05-23 21:08:46 +09:00

130 lines
3.3 KiB
TypeScript

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);
}