backtest work

This commit is contained in:
Boki 2025-07-03 11:04:33 -04:00
parent 143e2e1678
commit 55b4ca78c9
6 changed files with 427 additions and 129 deletions

View file

@ -232,7 +232,9 @@ export class BacktestEngine extends EventEmitter {
}, },
// Chart data (frontend-ready format) // Chart data (frontend-ready format)
equity: this.equityCurve.map(point => ({ equity: this.equityCurve
.sort((a, b) => a.timestamp - b.timestamp)
.map(point => ({
date: new Date(point.timestamp).toISOString(), date: new Date(point.timestamp).toISOString(),
value: point.value value: point.value
})), })),
@ -375,19 +377,34 @@ export class BacktestEngine extends EventEmitter {
let price = 100; // Base price let price = 100; // Base price
let currentTime = startTime; let currentTime = startTime;
let trend = 0; // Current trend direction
let trendStrength = 0;
let trendDuration = 0;
while (currentTime <= endTime) { while (currentTime <= endTime) {
// Generate random price movement // Every 20-50 days, change trend
const changePercent = (Math.random() - 0.5) * 0.04; // +/- 2% daily if (trendDuration <= 0) {
trend = Math.random() > 0.5 ? 1 : -1;
trendStrength = 0.002 + Math.random() * 0.003; // 0.2% to 0.5% daily trend
trendDuration = Math.floor(20 + Math.random() * 30);
}
// Generate price movement with trend and noise
const trendComponent = trend * trendStrength;
const noiseComponent = (Math.random() - 0.5) * 0.03; // +/- 1.5% noise
const changePercent = trendComponent + noiseComponent;
price = price * (1 + changePercent); price = price * (1 + changePercent);
// Generate OHLC // Generate OHLC with realistic intraday movement
const open = price; const open = price * (1 + (Math.random() - 0.5) * 0.005);
const high = price * (1 + Math.random() * 0.02); const dayRange = 0.01 + Math.random() * 0.02; // 1-3% daily range
const low = price * (1 - Math.random() * 0.02); const high = Math.max(open, price) * (1 + Math.random() * dayRange);
const close = price * (1 + (Math.random() - 0.5) * 0.01); const low = Math.min(open, price) * (1 - Math.random() * dayRange);
const close = low + Math.random() * (high - low);
const volume = Math.random() * 1000000 + 500000; const volume = Math.random() * 1000000 + 500000;
trendDuration--;
data.push({ data.push({
type: 'bar', type: 'bar',
data: { data: {
@ -572,7 +589,9 @@ export class BacktestEngine extends EventEmitter {
this.container.logger.debug('Fill processed:', fillResult); this.container.logger.debug('Fill processed:', fillResult);
} }
// Record trade // Handle trade recording based on side
if (fill.side === 'buy') {
// Create new trade entry for buy orders
const trade = { const trade = {
symbol: fill.symbol, symbol: fill.symbol,
side: fill.side, side: fill.side,
@ -590,10 +609,9 @@ export class BacktestEngine extends EventEmitter {
}; };
this.trades.push(trade); this.trades.push(trade);
this.container.logger.info(`💵 Trade recorded: ${fill.side} ${fill.quantity} ${fill.symbol} @ ${fill.price}`); this.container.logger.info(`💵 Buy trade opened: ${fill.quantity} ${fill.symbol} @ ${fill.price}`);
} else if (fill.side === 'sell') {
// Update existing trades if this is a closing trade // Update existing trades for sell orders
if (fill.side === 'sell') {
this.updateClosedTrades(fill); this.updateClosedTrades(fill);
} }
@ -994,7 +1012,8 @@ export class BacktestEngine extends EventEmitter {
low: d.data.low, low: d.data.low,
close: d.data.close, close: d.data.close,
volume: d.data.volume volume: d.data.volume
})); }))
.sort((a, b) => a.time - b.time); // Ensure data is sorted by time
ohlcData[symbol] = symbolData; ohlcData[symbol] = symbolData;
}); });
@ -1066,11 +1085,34 @@ export class BacktestEngine extends EventEmitter {
const tradeToClose = openTrades[0]; const tradeToClose = openTrades[0];
tradeToClose.exitPrice = fill.price; tradeToClose.exitPrice = fill.price;
tradeToClose.exitTime = fill.timestamp || this.currentTime; tradeToClose.exitTime = fill.timestamp || this.currentTime;
tradeToClose.pnl = (tradeToClose.exitPrice - tradeToClose.entryPrice) * tradeToClose.quantity - tradeToClose.commission - fill.commission; tradeToClose.pnl = (tradeToClose.exitPrice - tradeToClose.entryPrice) * tradeToClose.quantity - tradeToClose.commission - (fill.commission || 0);
tradeToClose.returnPct = ((tradeToClose.exitPrice - tradeToClose.entryPrice) / tradeToClose.entryPrice) * 100; tradeToClose.returnPct = ((tradeToClose.exitPrice - tradeToClose.entryPrice) / tradeToClose.entryPrice) * 100;
tradeToClose.holdingPeriod = tradeToClose.exitTime - tradeToClose.entryTime; tradeToClose.holdingPeriod = tradeToClose.exitTime - tradeToClose.entryTime;
this.container.logger.info(`💰 Trade closed: P&L ${tradeToClose.pnl.toFixed(2)}, Return ${tradeToClose.returnPct.toFixed(2)}%`); this.container.logger.info(`💰 Trade closed: P&L ${tradeToClose.pnl.toFixed(2)}, Return ${tradeToClose.returnPct.toFixed(2)}%`);
} else {
// Log if we're trying to sell without an open position
this.container.logger.warn(`⚠️ Sell order for ${fill.symbol} but no open trades found`);
// Still record it as a trade for tracking purposes (short position)
const trade = {
symbol: fill.symbol,
side: 'sell',
quantity: fill.quantity,
entryPrice: fill.price,
entryTime: fill.timestamp || this.currentTime,
exitPrice: null,
exitTime: null,
pnl: 0,
returnPct: 0,
commission: fill.commission || 0,
currentPrice: fill.price,
holdingPeriod: 0,
backtestTime: this.currentTime
};
this.trades.push(trade);
this.container.logger.info(`💵 Short trade opened: ${fill.quantity} ${fill.symbol} @ ${fill.price}`);
} }
} }
} }

View file

@ -121,6 +121,8 @@ export abstract class BaseStrategy extends EventEmitter {
? currentPos + fill.quantity ? currentPos + fill.quantity
: currentPos - fill.quantity; : currentPos - fill.quantity;
logger.info(`[BaseStrategy] Position update for ${update.symbol}: ${currentPos} -> ${newPos} (${update.side} ${fill.quantity})`);
if (Math.abs(newPos) < 0.0001) { if (Math.abs(newPos) < 0.0001) {
this.positions.delete(update.symbol); this.positions.delete(update.symbol);
} else { } else {

View file

@ -6,15 +6,15 @@ const logger = getLogger('SimpleMovingAverageCrossover');
export class SimpleMovingAverageCrossover extends BaseStrategy { export class SimpleMovingAverageCrossover extends BaseStrategy {
private priceHistory = new Map<string, number[]>(); private priceHistory = new Map<string, number[]>();
private positions = new Map<string, number>(); private lastTradeTime = new Map<string, number>();
private lastSignalTime = new Map<string, number>();
private totalSignals = 0; private totalSignals = 0;
// Strategy parameters // Strategy parameters
private readonly FAST_PERIOD = 10; private readonly FAST_PERIOD = 10;
private readonly SLOW_PERIOD = 20; private readonly SLOW_PERIOD = 20;
private readonly POSITION_SIZE = 0.1; // 10% of capital per position private readonly POSITION_SIZE = 0.1; // 10% of capital per position
private readonly MIN_SIGNAL_INTERVAL = 24 * 60 * 60 * 1000; // 1 day minimum between signals private readonly MIN_HOLDING_BARS = 1; // Minimum bars to hold position
private readonly DEBUG_INTERVAL = 20; // Log every N bars for debugging
constructor(config: any, modeManager?: any, executionService?: any) { constructor(config: any, modeManager?: any, executionService?: any) {
super(config, modeManager, executionService); super(config, modeManager, executionService);
@ -69,30 +69,80 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
const prevFastMA = this.calculateSMA(prevHistory, this.FAST_PERIOD); const prevFastMA = this.calculateSMA(prevHistory, this.FAST_PERIOD);
const prevSlowMA = this.calculateSMA(prevHistory, this.SLOW_PERIOD); const prevSlowMA = this.calculateSMA(prevHistory, this.SLOW_PERIOD);
const currentPosition = this.positions.get(symbol) || 0; const currentPosition = this.getPosition(symbol);
const currentPrice = data.data.close; const currentPrice = data.data.close;
const timestamp = new Date(data.data.timestamp).toISOString().split('T')[0]; const timestamp = new Date(data.data.timestamp).toISOString().split('T')[0];
// Log every 50 bars to track MA values and crossover conditions // Check minimum holding period
if (history.length % 50 === 0) { const lastTradeBar = this.lastTradeTime.get(symbol) || 0;
logger.info(`${symbol} @ ${timestamp} - Price: ${currentPrice.toFixed(2)}, Fast MA: ${fastMA.toFixed(2)}, Slow MA: ${slowMA.toFixed(2)}, Position: ${currentPosition}`); const barsSinceLastTrade = lastTradeBar > 0 ? history.length - lastTradeBar : Number.MAX_SAFE_INTEGER;
logger.debug(`${symbol} - Prev Fast MA: ${prevFastMA.toFixed(2)}, Prev Slow MA: ${prevSlowMA.toFixed(2)}`);
logger.debug(`${symbol} - Fast > Slow: ${fastMA > slowMA}, Prev Fast <= Prev Slow: ${prevFastMA <= prevSlowMA}`);
}
// Detect crossovers with detailed logging // Detect crossovers FIRST
const goldenCross = prevFastMA <= prevSlowMA && fastMA > slowMA; const goldenCross = prevFastMA <= prevSlowMA && fastMA > slowMA;
const deathCross = prevFastMA >= prevSlowMA && fastMA < slowMA; const deathCross = prevFastMA >= prevSlowMA && fastMA < slowMA;
if (goldenCross && currentPosition === 0) { // Enhanced debugging - log more frequently and when MAs are close
// Golden cross - buy signal const maDiff = fastMA - slowMA;
const maDiffPct = (maDiff / slowMA) * 100;
const masAreClose = Math.abs(maDiffPct) < 1.0; // Within 1%
if (history.length % this.DEBUG_INTERVAL === 0 || masAreClose || goldenCross || deathCross) {
logger.info(`${symbol} @ ${timestamp} [Bar ${history.length}]:`);
logger.info(` Price: $${currentPrice.toFixed(2)}`);
logger.info(` Fast MA (${this.FAST_PERIOD}): $${fastMA.toFixed(2)}`);
logger.info(` Slow MA (${this.SLOW_PERIOD}): $${slowMA.toFixed(2)}`);
logger.info(` MA Diff: ${maDiff.toFixed(2)} (${maDiffPct.toFixed(2)}%)`);
logger.info(` Position: ${currentPosition} shares`);
logger.info(` Bars since last trade: ${barsSinceLastTrade}`);
if (goldenCross) {
logger.info(` 🟢 GOLDEN CROSS DETECTED!`);
}
if (deathCross) {
logger.info(` 🔴 DEATH CROSS DETECTED!`);
}
}
if (barsSinceLastTrade < this.MIN_HOLDING_BARS && lastTradeBar > 0) {
return null; // Too soon to trade again
}
if (goldenCross) {
logger.info(`🟢 Golden cross detected for ${symbol} @ ${timestamp}`); logger.info(`🟢 Golden cross detected for ${symbol} @ ${timestamp}`);
logger.info(` Current position: ${currentPosition} shares`);
// For golden cross, we want to be long
// If we're short, we need to close the short first
if (currentPosition < 0) {
logger.info(` Closing short position of ${Math.abs(currentPosition)} shares`);
const signal: Signal = {
type: 'buy',
symbol,
strength: 0.8,
reason: 'Golden cross - Closing short position',
metadata: {
fastMA,
slowMA,
prevFastMA,
prevSlowMA,
crossoverType: 'golden',
price: currentPrice,
quantity: Math.abs(currentPosition) // Buy to close short
}
};
this.lastTradeTime.set(symbol, history.length);
this.totalSignals++;
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
return signal;
} else if (currentPosition === 0) {
// No position, open long
logger.info(` Price: ${currentPrice.toFixed(2)}, Fast MA: ${fastMA.toFixed(2)} > Slow MA: ${slowMA.toFixed(2)}`); logger.info(` Price: ${currentPrice.toFixed(2)}, Fast MA: ${fastMA.toFixed(2)} > Slow MA: ${slowMA.toFixed(2)}`);
logger.info(` Prev Fast MA: ${prevFastMA.toFixed(2)} <= Prev Slow MA: ${prevSlowMA.toFixed(2)}`); logger.info(` Prev Fast MA: ${prevFastMA.toFixed(2)} <= Prev Slow MA: ${prevSlowMA.toFixed(2)}`);
// Calculate position size // Calculate position size
const positionSize = this.calculatePositionSize(currentPrice); const positionSize = this.calculatePositionSize(currentPrice);
logger.info(` Position size: ${positionSize} shares`); logger.info(` Opening long position: ${positionSize} shares`);
const signal: Signal = { const signal: Signal = {
type: 'buy', type: 'buy',
@ -110,18 +160,23 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
} }
}; };
// Track signal time this.lastTradeTime.set(symbol, history.length);
this.lastSignalTime.set(symbol, Date.now());
this.totalSignals++; this.totalSignals++;
logger.info(`👉 Total signals generated: ${this.totalSignals}`); logger.info(`👉 Total signals generated: ${this.totalSignals}`);
return signal; return signal;
} else if (deathCross && currentPosition > 0) { } else {
// Death cross - sell signal logger.info(` ⚠️ Already long, skipping buy signal`);
}
} else if (deathCross) {
logger.info(`🔴 Death cross detected for ${symbol} @ ${timestamp}`); logger.info(`🔴 Death cross detected for ${symbol} @ ${timestamp}`);
logger.info(` Current position: ${currentPosition} shares`);
// For death cross, we want to be flat or short
if (currentPosition > 0) {
// Close long position
logger.info(` Closing long position of ${currentPosition} shares`);
logger.info(` Price: ${currentPrice.toFixed(2)}, Fast MA: ${fastMA.toFixed(2)} < Slow MA: ${slowMA.toFixed(2)}`); logger.info(` Price: ${currentPrice.toFixed(2)}, Fast MA: ${fastMA.toFixed(2)} < Slow MA: ${slowMA.toFixed(2)}`);
logger.info(` Prev Fast MA: ${prevFastMA.toFixed(2)} >= Prev Slow MA: ${prevSlowMA.toFixed(2)}`); logger.info(` Prev Fast MA: ${prevFastMA.toFixed(2)} >= Prev Slow MA: ${prevSlowMA.toFixed(2)}`);
logger.info(` Current position: ${currentPosition} shares`);
const signal: Signal = { const signal: Signal = {
type: 'sell', type: 'sell',
@ -139,12 +194,42 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
} }
}; };
// Track signal time this.lastTradeTime.set(symbol, history.length);
this.lastSignalTime.set(symbol, Date.now());
this.totalSignals++; this.totalSignals++;
logger.info(`👉 Total signals generated: ${this.totalSignals}`); logger.info(`👉 Total signals generated: ${this.totalSignals}`);
return signal; return signal;
} else if (currentPosition === 0) {
// Optional: Open short position (comment out if long-only)
logger.info(` No position, staying flat (long-only strategy)`);
// Uncomment below for long/short strategy:
/*
const positionSize = this.calculatePositionSize(currentPrice);
logger.info(` Opening short position: ${positionSize} shares`);
const signal: Signal = {
type: 'sell',
symbol,
strength: 0.8,
reason: 'Death cross - Opening short position',
metadata: {
fastMA,
slowMA,
prevFastMA,
prevSlowMA,
crossoverType: 'death',
price: currentPrice,
quantity: positionSize
}
};
this.lastTradeTime.set(symbol, history.length);
this.totalSignals++;
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
return signal;
*/
} else {
logger.info(` ⚠️ Already short, skipping sell signal`);
}
} }
// Log near-crossover conditions // Log near-crossover conditions
@ -207,24 +292,21 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
protected onOrderFilled(fill: any): void { protected onOrderFilled(fill: any): void {
const { symbol, side, quantity, price } = fill; const { symbol, side, quantity, price } = fill;
const currentPosition = this.positions.get(symbol) || 0; logger.info(`${side.toUpperCase()} filled: ${symbol} - ${quantity} shares @ ${price}`);
if (side === 'buy') { // Position tracking is handled by BaseStrategy.onOrderUpdate
this.positions.set(symbol, currentPosition + quantity); // Just log the current position
logger.info(`✅ BUY filled: ${symbol} - ${quantity} shares @ ${price}`); const currentPosition = this.getPosition(symbol);
} else { logger.info(`Position for ${symbol}: ${currentPosition} shares`);
this.positions.set(symbol, Math.max(0, currentPosition - quantity));
logger.info(`✅ SELL filled: ${symbol} - ${quantity} shares @ ${price}`);
}
logger.info(`Position updated for ${symbol}: ${this.positions.get(symbol)} shares`);
} }
// Override to provide custom order generation // Override to provide custom order generation
protected async signalToOrder(signal: Signal): Promise<OrderRequest | null> { protected async signalToOrder(signal: Signal): Promise<OrderRequest | null> {
logger.info(`🔄 Converting signal to order:`, signal); logger.info(`🔄 Converting signal to order:`, signal);
// Get position sizing from metadata or calculate const currentPosition = this.getPosition(signal.symbol);
// Get position sizing from metadata
const quantity = signal.metadata?.quantity || 100; const quantity = signal.metadata?.quantity || 100;
if (signal.type === 'buy') { if (signal.type === 'buy') {

View file

@ -0,0 +1,99 @@
#!/usr/bin/env bun
import { createContainer } from './src/simple-container';
import { BacktestEngine } from './src/backtest/BacktestEngine';
import { StrategyManager } from './src/strategies/StrategyManager';
import { SimpleMovingAverageCrossover } from './src/strategies/examples/SimpleMovingAverageCrossover';
async function runBacktest() {
console.log('Starting backtest test...');
// Create container with minimal config
const config = {
port: 2004,
mode: 'paper',
enableWebSocket: false,
database: {
mongodb: { enabled: false },
postgres: { enabled: false },
questdb: { enabled: false }
},
redis: {
url: 'redis://localhost:6379'
},
backtesting: {
maxConcurrent: 1,
defaultSpeed: 'max',
dataResolutions: ['1d']
},
strategies: {
maxActive: 10,
defaultTimeout: 30000
}
};
const container = await createContainer(config);
// Initialize strategy manager
const strategyManager = new StrategyManager(container.executionService, container.modeManager);
// Create and add strategy
const strategyConfig = {
id: 'sma-test',
name: 'SMA Test',
type: 'sma-crossover',
symbols: ['AA'],
active: true,
allocation: 1.0,
riskLimit: 0.02,
maxPositions: 10
};
const strategy = new SimpleMovingAverageCrossover(strategyConfig, container.modeManager, container.executionService);
await strategyManager.addStrategy(strategy);
// Create backtest engine
const backtestEngine = new BacktestEngine(container, strategyManager);
// Run backtest
const backtestConfig = {
symbols: ['AA'],
strategy: 'sma-crossover',
startDate: '2024-01-01',
endDate: '2024-12-31',
initialCapital: 100000,
commission: 0.001,
slippage: 0.0005,
dataFrequency: '1d'
};
try {
console.log('Running backtest...');
const result = await backtestEngine.runBacktest(backtestConfig);
console.log('\n=== Backtest Results ===');
console.log(`Total trades: ${result.metrics.totalTrades}`);
console.log(`Win rate: ${result.metrics.winRate.toFixed(2)}%`);
console.log(`Total return: ${result.metrics.totalReturn.toFixed(2)}%`);
console.log(`Sharpe ratio: ${result.metrics.sharpeRatio.toFixed(2)}`);
console.log(`Max drawdown: ${result.metrics.maxDrawdown.toFixed(2)}%`);
console.log('\n=== Trade History ===');
result.trades.forEach((trade, i) => {
console.log(`Trade ${i + 1}: ${trade.side} ${trade.quantity} ${trade.symbol} @ ${trade.entryPrice.toFixed(2)}`);
if (trade.exitDate) {
console.log(` Exit: ${trade.exitPrice.toFixed(2)}, P&L: ${trade.pnl.toFixed(2)} (${trade.pnlPercent.toFixed(2)}%)`);
}
});
console.log(`\nTotal trades in result: ${result.trades.length}`);
} catch (error) {
console.error('Backtest failed:', error);
} finally {
// Cleanup
await container.shutdownManager.shutdown();
process.exit(0);
}
}
runBacktest().catch(console.error);

View file

@ -11,6 +11,16 @@ export interface ChartData {
volume?: number; volume?: number;
} }
export interface TradeMarker {
time: number;
position: 'aboveBar' | 'belowBar';
color: string;
shape: 'arrowUp' | 'arrowDown';
text: string;
id?: string;
price?: number;
}
export interface ChartProps { export interface ChartProps {
data: ChartData[]; data: ChartData[];
height?: number; height?: number;
@ -23,6 +33,7 @@ export interface ChartProps {
color?: string; color?: string;
lineWidth?: number; lineWidth?: number;
}>; }>;
tradeMarkers?: TradeMarker[];
className?: string; className?: string;
} }
@ -33,6 +44,7 @@ export function Chart({
showVolume = true, showVolume = true,
theme = 'dark', theme = 'dark',
overlayData = [], overlayData = [],
tradeMarkers = [],
className = '', className = '',
}: ChartProps) { }: ChartProps) {
const chartContainerRef = useRef<HTMLDivElement>(null); const chartContainerRef = useRef<HTMLDivElement>(null);
@ -94,16 +106,18 @@ export function Chart({
chartRef.current = chart; chartRef.current = chart;
// Filter and validate data // Filter, validate and sort data
const validateAndFilterData = (rawData: any[]) => { const validateAndFilterData = (rawData: any[]) => {
const seen = new Set<number>(); const seen = new Set<number>();
return rawData.filter((item, index) => { return rawData
.filter((item, index) => {
if (seen.has(item.time)) { if (seen.has(item.time)) {
return false; return false;
} }
seen.add(item.time); seen.add(item.time);
return true; return true;
}); })
.sort((a, b) => a.time - b.time); // Ensure ascending time order
}; };
// Create main series // Create main series
@ -193,7 +207,8 @@ export function Chart({
} }
// Filter out duplicate timestamps and ensure ascending order // Filter out duplicate timestamps and ensure ascending order
const uniqueData = overlay.data.reduce((acc: any[], curr) => { const sortedData = [...overlay.data].sort((a, b) => a.time - b.time);
const uniqueData = sortedData.reduce((acc: any[], curr) => {
if (!acc.length || curr.time > acc[acc.length - 1].time) { if (!acc.length || curr.time > acc[acc.length - 1].time) {
acc.push(curr); acc.push(curr);
} }
@ -203,6 +218,22 @@ export function Chart({
overlaySeriesRef.current.set(overlay.name, series); overlaySeriesRef.current.set(overlay.name, series);
}); });
// Add trade markers
if (tradeMarkers.length > 0 && mainSeriesRef.current) {
// Sort markers by time to ensure they're in ascending order
const sortedMarkers = [...tradeMarkers].sort((a, b) => a.time - b.time);
const markers: LightweightCharts.SeriesMarker<LightweightCharts.Time>[] = sortedMarkers.map(marker => ({
time: marker.time as LightweightCharts.Time,
position: marker.position,
color: marker.color,
shape: marker.shape as LightweightCharts.SeriesMarkerShape,
text: marker.text,
id: marker.id,
size: 1
}));
mainSeriesRef.current.setMarkers(markers);
}
// Fit content with a slight delay to ensure all series are loaded // Fit content with a slight delay to ensure all series are loaded
setTimeout(() => { setTimeout(() => {
chart.timeScale().fitContent(); chart.timeScale().fitContent();
@ -251,7 +282,7 @@ export function Chart({
chart.remove(); chart.remove();
} }
}; };
}, [data, height, type, showVolume, theme, overlayData]); }, [data, height, type, showVolume, theme, overlayData, tradeMarkers]);
return ( return (
<div className={`relative ${className}`}> <div className={`relative ${className}`}>

View file

@ -12,6 +12,8 @@ interface BacktestResultsProps {
} }
export function BacktestResults({ status, results, currentTime }: BacktestResultsProps) { export function BacktestResults({ status, results, currentTime }: BacktestResultsProps) {
const [selectedSymbol, setSelectedSymbol] = useState<string>('');
if (status === 'idle') { if (status === 'idle') {
return ( return (
<div className="bg-surface-secondary p-8 rounded-lg border border-border h-full flex items-center justify-center"> <div className="bg-surface-secondary p-8 rounded-lg border border-border h-full flex items-center justify-center">
@ -112,16 +114,55 @@ export function BacktestResults({ status, results, currentTime }: BacktestResult
{/* Performance Chart */} {/* Performance Chart */}
<div className="bg-surface-secondary p-4 rounded-lg border border-border"> <div className="bg-surface-secondary p-4 rounded-lg border border-border">
<h3 className="text-base font-medium text-text-primary mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="text-base font-medium text-text-primary">
Portfolio Performance Portfolio Performance
</h3> </h3>
{results.ohlcData && Object.keys(results.ohlcData).length > 1 && (
<select
value={selectedSymbol || Object.keys(results.ohlcData)[0]}
onChange={(e) => setSelectedSymbol(e.target.value)}
className="px-3 py-1 text-sm bg-background border border-border rounded-md text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500"
>
{Object.keys(results.ohlcData).map(symbol => (
<option key={symbol} value={symbol}>{symbol}</option>
))}
</select>
)}
</div>
{(() => { {(() => {
const hasOhlcData = results.ohlcData && Object.keys(results.ohlcData).length > 0; const hasOhlcData = results.ohlcData && Object.keys(results.ohlcData).length > 0;
const hasEquityData = results.equity && results.equity.length > 0; const hasEquityData = results.equity && results.equity.length > 0;
if (hasOhlcData) { if (hasOhlcData) {
const firstSymbol = Object.keys(results.ohlcData)[0]; const activeSymbol = selectedSymbol || Object.keys(results.ohlcData)[0];
const ohlcData = results.ohlcData[firstSymbol]; const ohlcData = results.ohlcData[activeSymbol];
// Create trade markers for the selected symbol
const tradeMarkers = results.trades
.filter(trade => trade.symbol === activeSymbol)
.map(trade => ({
time: Math.floor(new Date(trade.entryDate).getTime() / 1000),
position: 'belowBar' as const,
color: '#10b981',
shape: 'arrowUp' as const,
text: `Buy ${trade.quantity}@${trade.entryPrice.toFixed(2)}`,
id: `${trade.id}-entry`,
price: trade.entryPrice
}))
.concat(
results.trades
.filter(trade => trade.symbol === activeSymbol && trade.exitDate)
.map(trade => ({
time: Math.floor(new Date(trade.exitDate!).getTime() / 1000),
position: 'aboveBar' as const,
color: '#ef4444',
shape: 'arrowDown' as const,
text: `Sell ${trade.quantity}@${trade.exitPrice.toFixed(2)} (${trade.pnl >= 0 ? '+' : ''}${trade.pnl.toFixed(2)})`,
id: `${trade.id}-exit`,
price: trade.exitPrice
}))
);
return ( return (
<Chart <Chart
@ -141,6 +182,7 @@ export function BacktestResults({ status, results, currentTime }: BacktestResult
lineWidth: 3 lineWidth: 3
} }
] : []} ] : []}
tradeMarkers={tradeMarkers}
className="rounded" className="rounded"
/> />
); );