initial backtests
This commit is contained in:
parent
fa70ada2bb
commit
5a3a23a2ba
6 changed files with 400 additions and 129 deletions
|
|
@ -14,14 +14,78 @@ interface BacktestEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BacktestResult {
|
interface BacktestResult {
|
||||||
id: string;
|
// Identification
|
||||||
config: any;
|
backtestId: string;
|
||||||
performance: PerformanceMetrics;
|
status: 'completed' | 'failed' | 'cancelled';
|
||||||
trades: any[];
|
completedAt: string;
|
||||||
equityCurve: { timestamp: number; value: number }[];
|
|
||||||
drawdown: { timestamp: number; value: number }[];
|
// Configuration
|
||||||
dailyReturns: number[];
|
config: {
|
||||||
finalPositions: any[];
|
name: string;
|
||||||
|
strategy: string;
|
||||||
|
symbols: string[];
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
initialCapital: number;
|
||||||
|
commission: number;
|
||||||
|
slippage: number;
|
||||||
|
dataFrequency: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Performance metrics
|
||||||
|
metrics: {
|
||||||
|
totalReturn: number;
|
||||||
|
sharpeRatio: number;
|
||||||
|
maxDrawdown: number;
|
||||||
|
winRate: number;
|
||||||
|
totalTrades: number;
|
||||||
|
profitFactor: number;
|
||||||
|
profitableTrades: number;
|
||||||
|
avgWin: number;
|
||||||
|
avgLoss: number;
|
||||||
|
expectancy: number;
|
||||||
|
calmarRatio: number;
|
||||||
|
sortinoRatio: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Chart data
|
||||||
|
equity: Array<{ date: string; value: number }>;
|
||||||
|
ohlcData: Record<string, any[]>;
|
||||||
|
|
||||||
|
// Trade history
|
||||||
|
trades: Array<{
|
||||||
|
id: string;
|
||||||
|
symbol: string;
|
||||||
|
entryDate: string;
|
||||||
|
exitDate: string | null;
|
||||||
|
entryPrice: number;
|
||||||
|
exitPrice: number;
|
||||||
|
quantity: number;
|
||||||
|
side: string;
|
||||||
|
pnl: number;
|
||||||
|
pnlPercent: number;
|
||||||
|
commission: number;
|
||||||
|
duration: number;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
// Positions
|
||||||
|
positions: Array<{
|
||||||
|
symbol: string;
|
||||||
|
quantity: number;
|
||||||
|
averagePrice: number;
|
||||||
|
currentPrice: number;
|
||||||
|
unrealizedPnl: number;
|
||||||
|
realizedPnl: number;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
// Analytics
|
||||||
|
analytics: {
|
||||||
|
drawdownSeries: { timestamp: number; value: number }[];
|
||||||
|
dailyReturns: number[];
|
||||||
|
monthlyReturns: Record<string, number>;
|
||||||
|
exposureTime: number;
|
||||||
|
riskMetrics: Record<string, number>;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BacktestEngine extends EventEmitter {
|
export class BacktestEngine extends EventEmitter {
|
||||||
|
|
@ -117,17 +181,85 @@ export class BacktestEngine extends EventEmitter {
|
||||||
// Get final positions
|
// Get final positions
|
||||||
const finalPositions = await this.getFinalPositions();
|
const finalPositions = await this.getFinalPositions();
|
||||||
|
|
||||||
// Store results
|
// Create comprehensive frontend-ready result
|
||||||
const result: BacktestResult = {
|
const result: BacktestResult = {
|
||||||
id: backtestId,
|
// Identification
|
||||||
config: validatedConfig,
|
backtestId,
|
||||||
performance,
|
status: 'completed' as const,
|
||||||
trades: this.trades,
|
completedAt: new Date().toISOString(),
|
||||||
equityCurve: this.equityCurve,
|
|
||||||
drawdown: this.calculateDrawdown(),
|
// Configuration used
|
||||||
dailyReturns: this.calculateDailyReturns(),
|
config: {
|
||||||
finalPositions,
|
name: validatedConfig.name || 'Backtest',
|
||||||
ohlcData: this.getOHLCData(marketData, validatedConfig.symbols)
|
strategy: validatedConfig.strategy,
|
||||||
|
symbols: validatedConfig.symbols,
|
||||||
|
startDate: validatedConfig.startDate,
|
||||||
|
endDate: validatedConfig.endDate,
|
||||||
|
initialCapital: validatedConfig.initialCapital,
|
||||||
|
commission: validatedConfig.commission || 0,
|
||||||
|
slippage: validatedConfig.slippage || 0,
|
||||||
|
dataFrequency: validatedConfig.dataFrequency || '1d'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Performance metrics (frontend-ready)
|
||||||
|
metrics: {
|
||||||
|
totalReturn: performance.totalReturn,
|
||||||
|
sharpeRatio: performance.sharpeRatio,
|
||||||
|
maxDrawdown: performance.maxDrawdown,
|
||||||
|
winRate: performance.winRate,
|
||||||
|
totalTrades: performance.totalTrades,
|
||||||
|
profitFactor: performance.profitFactor || 0,
|
||||||
|
profitableTrades: Math.round(performance.totalTrades * performance.winRate / 100),
|
||||||
|
avgWin: performance.avgWin || 0,
|
||||||
|
avgLoss: performance.avgLoss || 0,
|
||||||
|
expectancy: performance.expectancy || 0,
|
||||||
|
calmarRatio: performance.calmarRatio || 0,
|
||||||
|
sortinoRatio: performance.sortinoRatio || 0
|
||||||
|
},
|
||||||
|
|
||||||
|
// Chart data (frontend-ready format)
|
||||||
|
equity: this.equityCurve.map(point => ({
|
||||||
|
date: new Date(point.timestamp).toISOString(),
|
||||||
|
value: point.value
|
||||||
|
})),
|
||||||
|
|
||||||
|
// OHLC data for charts
|
||||||
|
ohlcData: this.getOHLCData(marketData, validatedConfig.symbols),
|
||||||
|
|
||||||
|
// Trade history (frontend-ready)
|
||||||
|
trades: this.trades.map(trade => ({
|
||||||
|
id: `${trade.symbol}-${trade.entryTime}`,
|
||||||
|
symbol: trade.symbol,
|
||||||
|
entryDate: new Date(trade.entryTime).toISOString(),
|
||||||
|
exitDate: trade.exitTime ? new Date(trade.exitTime).toISOString() : null,
|
||||||
|
entryPrice: trade.entryPrice,
|
||||||
|
exitPrice: trade.exitPrice || trade.currentPrice,
|
||||||
|
quantity: trade.quantity,
|
||||||
|
side: trade.side,
|
||||||
|
pnl: trade.pnl || 0,
|
||||||
|
pnlPercent: trade.returnPct || 0,
|
||||||
|
commission: trade.commission || 0,
|
||||||
|
duration: trade.holdingPeriod || 0
|
||||||
|
})),
|
||||||
|
|
||||||
|
// Final positions
|
||||||
|
positions: finalPositions.map(pos => ({
|
||||||
|
symbol: pos.symbol,
|
||||||
|
quantity: pos.quantity,
|
||||||
|
averagePrice: pos.avgPrice,
|
||||||
|
currentPrice: pos.currentPrice || pos.avgPrice,
|
||||||
|
unrealizedPnl: pos.unrealizedPnl || 0,
|
||||||
|
realizedPnl: pos.realizedPnl || 0
|
||||||
|
})),
|
||||||
|
|
||||||
|
// Additional analytics
|
||||||
|
analytics: {
|
||||||
|
drawdownSeries: this.calculateDrawdown(),
|
||||||
|
dailyReturns: this.calculateDailyReturns(),
|
||||||
|
monthlyReturns: this.calculateMonthlyReturns(),
|
||||||
|
exposureTime: this.calculateExposureTime(),
|
||||||
|
riskMetrics: this.calculateRiskMetrics()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.storeResults(result);
|
await this.storeResults(result);
|
||||||
|
|
@ -484,6 +616,99 @@ export class BacktestEngine extends EventEmitter {
|
||||||
return drawdowns;
|
return drawdowns;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private calculateMonthlyReturns(): Record<string, number> {
|
||||||
|
const monthlyReturns: Record<string, number> = {};
|
||||||
|
const monthlyEquity = new Map<string, { start: number; end: number }>();
|
||||||
|
|
||||||
|
for (const point of this.equityCurve) {
|
||||||
|
const date = new Date(point.timestamp);
|
||||||
|
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
||||||
|
|
||||||
|
if (!monthlyEquity.has(monthKey)) {
|
||||||
|
monthlyEquity.set(monthKey, { start: point.value, end: point.value });
|
||||||
|
} else {
|
||||||
|
const month = monthlyEquity.get(monthKey)!;
|
||||||
|
month.end = point.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [month, values] of monthlyEquity) {
|
||||||
|
monthlyReturns[month] = ((values.end - values.start) / values.start) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
return monthlyReturns;
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateExposureTime(): number {
|
||||||
|
if (this.trades.length === 0) return 0;
|
||||||
|
|
||||||
|
let totalExposureTime = 0;
|
||||||
|
for (const trade of this.trades) {
|
||||||
|
if (trade.exitTime) {
|
||||||
|
totalExposureTime += trade.exitTime - trade.entryTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use equity curve to determine actual trading period
|
||||||
|
const startTime = this.equityCurve.length > 0 ? this.equityCurve[0].timestamp : 0;
|
||||||
|
const endTime = this.equityCurve.length > 0 ? this.equityCurve[this.equityCurve.length - 1].timestamp : 0;
|
||||||
|
const totalTime = endTime - startTime;
|
||||||
|
|
||||||
|
return totalTime > 0 ? (totalExposureTime / totalTime) * 100 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateRiskMetrics(): Record<string, number> {
|
||||||
|
const returns = this.calculateDailyReturns();
|
||||||
|
|
||||||
|
// Calculate various risk metrics
|
||||||
|
const avgReturn = returns.reduce((a, b) => a + b, 0) / returns.length;
|
||||||
|
const variance = returns.reduce((sum, r) => sum + Math.pow(r - avgReturn, 2), 0) / returns.length;
|
||||||
|
const stdDev = Math.sqrt(variance);
|
||||||
|
|
||||||
|
// Downside deviation (for Sortino)
|
||||||
|
const downsideReturns = returns.filter(r => r < 0);
|
||||||
|
const downsideVariance = downsideReturns.reduce((sum, r) => sum + Math.pow(r, 2), 0) / downsideReturns.length;
|
||||||
|
const downsideDeviation = Math.sqrt(downsideVariance);
|
||||||
|
|
||||||
|
return {
|
||||||
|
volatility: stdDev * Math.sqrt(252), // Annualized
|
||||||
|
downsideDeviation: downsideDeviation * Math.sqrt(252),
|
||||||
|
var95: this.calculateVaR(returns, 0.95),
|
||||||
|
var99: this.calculateVaR(returns, 0.99),
|
||||||
|
cvar95: this.calculateCVaR(returns, 0.95),
|
||||||
|
maxConsecutiveLosses: this.calculateMaxConsecutiveLosses()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateVaR(returns: number[], confidence: number): number {
|
||||||
|
const sorted = [...returns].sort((a, b) => a - b);
|
||||||
|
const index = Math.floor((1 - confidence) * sorted.length);
|
||||||
|
return sorted[index] || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateCVaR(returns: number[], confidence: number): number {
|
||||||
|
const sorted = [...returns].sort((a, b) => a - b);
|
||||||
|
const index = Math.floor((1 - confidence) * sorted.length);
|
||||||
|
const tail = sorted.slice(0, index + 1);
|
||||||
|
return tail.reduce((a, b) => a + b, 0) / tail.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateMaxConsecutiveLosses(): number {
|
||||||
|
let maxLosses = 0;
|
||||||
|
let currentLosses = 0;
|
||||||
|
|
||||||
|
for (const trade of this.trades) {
|
||||||
|
if (trade.pnl && trade.pnl < 0) {
|
||||||
|
currentLosses++;
|
||||||
|
maxLosses = Math.max(maxLosses, currentLosses);
|
||||||
|
} else {
|
||||||
|
currentLosses = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxLosses;
|
||||||
|
}
|
||||||
|
|
||||||
private calculateDailyReturns(): number[] {
|
private calculateDailyReturns(): number[] {
|
||||||
const dailyReturns: number[] = [];
|
const dailyReturns: number[] = [];
|
||||||
const dailyEquity = new Map<string, number>();
|
const dailyEquity = new Map<string, number>();
|
||||||
|
|
|
||||||
|
|
@ -82,15 +82,22 @@ export class BacktestService {
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
// Store result when available
|
// Store result directly without transformation
|
||||||
if (result.performance) {
|
if (result.status === 'completed') {
|
||||||
// Backtest completed immediately
|
|
||||||
backtest.status = 'completed';
|
backtest.status = 'completed';
|
||||||
|
backtest.updatedAt = new Date();
|
||||||
backtestStore.set(backtestId, backtest);
|
backtestStore.set(backtestId, backtest);
|
||||||
backtestResults.set(backtestId, result);
|
backtestResults.set(backtestId, result);
|
||||||
|
|
||||||
|
logger.info('Backtest completed', {
|
||||||
|
backtestId,
|
||||||
|
trades: result.metrics?.totalTrades,
|
||||||
|
return: result.metrics?.totalReturn
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// Update status to running if not completed
|
// Update status to running if not completed
|
||||||
backtest.status = 'running';
|
backtest.status = 'running';
|
||||||
|
backtest.updatedAt = new Date();
|
||||||
backtestStore.set(backtestId, backtest);
|
backtestStore.set(backtestId, backtest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,35 +116,8 @@ export class BacktestService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getBacktestResults(id: string): Promise<any> {
|
async getBacktestResults(id: string): Promise<any> {
|
||||||
const results = backtestResults.get(id);
|
// Return results directly without any transformation
|
||||||
if (!results) return null;
|
return backtestResults.get(id) || null;
|
||||||
|
|
||||||
// Transform orchestrator response to frontend expected format
|
|
||||||
return {
|
|
||||||
backtestId: results.id || id,
|
|
||||||
metrics: {
|
|
||||||
totalReturn: results.performance?.totalReturn || 0,
|
|
||||||
sharpeRatio: results.performance?.sharpeRatio || 0,
|
|
||||||
maxDrawdown: results.performance?.maxDrawdown || 0,
|
|
||||||
winRate: results.performance?.winRate || 0,
|
|
||||||
totalTrades: results.performance?.totalTrades || 0,
|
|
||||||
profitFactor: results.performance?.profitFactor
|
|
||||||
},
|
|
||||||
equity: results.equityCurve?.map((point: any) => ({
|
|
||||||
date: new Date(point.timestamp).toISOString(),
|
|
||||||
value: point.value
|
|
||||||
})) || [],
|
|
||||||
trades: results.trades?.map((trade: any) => ({
|
|
||||||
symbol: trade.symbol,
|
|
||||||
entryDate: new Date(trade.entryTime).toISOString(),
|
|
||||||
exitDate: new Date(trade.exitTime).toISOString(),
|
|
||||||
entryPrice: trade.entryPrice,
|
|
||||||
exitPrice: trade.exitPrice,
|
|
||||||
quantity: trade.quantity,
|
|
||||||
pnl: trade.pnl
|
|
||||||
})) || [],
|
|
||||||
ohlcData: results.ohlcData || {}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async listBacktests(params: { limit: number; offset: number }): Promise<BacktestJob[]> {
|
async listBacktests(params: { limit: number; offset: number }): Promise<BacktestJob[]> {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import * as LightweightCharts from 'lightweight-charts';
|
import * as LightweightCharts from 'lightweight-charts';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useCallback } from 'react';
|
||||||
|
|
||||||
export interface ChartData {
|
export interface ChartData {
|
||||||
time: number;
|
time: number;
|
||||||
|
|
@ -41,19 +41,15 @@ export function Chart({
|
||||||
const volumeSeriesRef = useRef<LightweightCharts.ISeriesApi<any> | null>(null);
|
const volumeSeriesRef = useRef<LightweightCharts.ISeriesApi<any> | null>(null);
|
||||||
const overlaySeriesRef = useRef<Map<string, LightweightCharts.ISeriesApi<any>>>(new Map());
|
const overlaySeriesRef = useRef<Map<string, LightweightCharts.ISeriesApi<any>>>(new Map());
|
||||||
|
|
||||||
// Debug logging
|
// Reset zoom handler
|
||||||
console.log('Chart - data received:', data);
|
const resetZoom = useCallback(() => {
|
||||||
console.log('Chart - data length:', data?.length);
|
if (chartRef.current) {
|
||||||
console.log('Chart - data type:', Array.isArray(data) ? 'array' : typeof data);
|
chartRef.current.timeScale().fitContent();
|
||||||
console.log('Chart - first data point:', data?.[0]);
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!chartContainerRef.current || !data || !data.length) {
|
if (!chartContainerRef.current || !data || !data.length) {
|
||||||
console.log('Chart - early return:', {
|
|
||||||
hasContainer: !!chartContainerRef.current,
|
|
||||||
hasData: !!data,
|
|
||||||
dataLength: data?.length
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,11 +85,27 @@ export function Chart({
|
||||||
borderColor: theme === 'dark' ? '#1f2937' : '#e5e7eb',
|
borderColor: theme === 'dark' ? '#1f2937' : '#e5e7eb',
|
||||||
timeVisible: true,
|
timeVisible: true,
|
||||||
secondsVisible: false,
|
secondsVisible: false,
|
||||||
|
rightOffset: 12,
|
||||||
|
barSpacing: 3,
|
||||||
|
fixLeftEdge: true,
|
||||||
|
fixRightEdge: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
chartRef.current = chart;
|
chartRef.current = chart;
|
||||||
|
|
||||||
|
// Filter and validate data
|
||||||
|
const validateAndFilterData = (rawData: any[]) => {
|
||||||
|
const seen = new Set<number>();
|
||||||
|
return rawData.filter((item, index) => {
|
||||||
|
if (seen.has(item.time)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
seen.add(item.time);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Create main series
|
// Create main series
|
||||||
if (type === 'candlestick' && data[0].open !== undefined) {
|
if (type === 'candlestick' && data[0].open !== undefined) {
|
||||||
mainSeriesRef.current = chart.addCandlestickSeries({
|
mainSeriesRef.current = chart.addCandlestickSeries({
|
||||||
|
|
@ -104,7 +116,8 @@ export function Chart({
|
||||||
wickUpColor: '#10b981',
|
wickUpColor: '#10b981',
|
||||||
wickDownColor: '#ef4444',
|
wickDownColor: '#ef4444',
|
||||||
});
|
});
|
||||||
mainSeriesRef.current.setData(data as LightweightCharts.CandlestickData[]);
|
const validData = validateAndFilterData(data);
|
||||||
|
mainSeriesRef.current.setData(validData as LightweightCharts.CandlestickData[]);
|
||||||
} else if (type === 'line' || (type === 'candlestick' && data[0].value !== undefined)) {
|
} else if (type === 'line' || (type === 'candlestick' && data[0].value !== undefined)) {
|
||||||
mainSeriesRef.current = chart.addLineSeries({
|
mainSeriesRef.current = chart.addLineSeries({
|
||||||
color: '#3b82f6',
|
color: '#3b82f6',
|
||||||
|
|
@ -114,7 +127,8 @@ export function Chart({
|
||||||
time: d.time,
|
time: d.time,
|
||||||
value: d.value ?? d.close ?? 0
|
value: d.value ?? d.close ?? 0
|
||||||
}));
|
}));
|
||||||
mainSeriesRef.current.setData(lineData);
|
const validData = validateAndFilterData(lineData);
|
||||||
|
mainSeriesRef.current.setData(validData);
|
||||||
} else if (type === 'area') {
|
} else if (type === 'area') {
|
||||||
mainSeriesRef.current = chart.addAreaSeries({
|
mainSeriesRef.current = chart.addAreaSeries({
|
||||||
lineColor: '#3b82f6',
|
lineColor: '#3b82f6',
|
||||||
|
|
@ -126,7 +140,8 @@ export function Chart({
|
||||||
time: d.time,
|
time: d.time,
|
||||||
value: d.value ?? d.close ?? 0
|
value: d.value ?? d.close ?? 0
|
||||||
}));
|
}));
|
||||||
mainSeriesRef.current.setData(areaData);
|
const validData = validateAndFilterData(areaData);
|
||||||
|
mainSeriesRef.current.setData(validData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add volume if available
|
// Add volume if available
|
||||||
|
|
@ -177,12 +192,46 @@ export function Chart({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
series.setData(overlay.data);
|
// Filter out duplicate timestamps and ensure ascending order
|
||||||
|
const uniqueData = overlay.data.reduce((acc: any[], curr) => {
|
||||||
|
if (!acc.length || curr.time > acc[acc.length - 1].time) {
|
||||||
|
acc.push(curr);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
series.setData(uniqueData);
|
||||||
overlaySeriesRef.current.set(overlay.name, series);
|
overlaySeriesRef.current.set(overlay.name, series);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fit content
|
// Fit content with a slight delay to ensure all series are loaded
|
||||||
chart.timeScale().fitContent();
|
setTimeout(() => {
|
||||||
|
chart.timeScale().fitContent();
|
||||||
|
|
||||||
|
// Also set the visible range to ensure all data is shown
|
||||||
|
if (data.length > 0) {
|
||||||
|
const firstTime = data[0].time;
|
||||||
|
const lastTime = data[data.length - 1].time;
|
||||||
|
chart.timeScale().setVisibleRange({
|
||||||
|
from: firstTime as any,
|
||||||
|
to: lastTime as any,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Enable mouse wheel zoom and touch gestures
|
||||||
|
chart.applyOptions({
|
||||||
|
handleScroll: {
|
||||||
|
mouseWheel: true,
|
||||||
|
pressedMouseMove: true,
|
||||||
|
horzTouchDrag: true,
|
||||||
|
vertTouchDrag: true,
|
||||||
|
},
|
||||||
|
handleScale: {
|
||||||
|
mouseWheel: true,
|
||||||
|
pinch: true,
|
||||||
|
axisPressedMouseMove: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Handle resize
|
// Handle resize
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
|
|
@ -210,6 +259,13 @@ export function Chart({
|
||||||
ref={chartContainerRef}
|
ref={chartContainerRef}
|
||||||
style={{ width: '100%', height: `${height}px` }}
|
style={{ width: '100%', height: `${height}px` }}
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
onClick={resetZoom}
|
||||||
|
className="absolute top-2 right-2 px-2 py-1 text-xs bg-surface-primary border border-border rounded hover:bg-surface-secondary transition-colors"
|
||||||
|
title="Reset zoom"
|
||||||
|
>
|
||||||
|
Reset Zoom
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -32,40 +32,10 @@ export function BacktestPage() {
|
||||||
// Current time is not available in the new API, so we'll estimate it based on progress
|
// Current time is not available in the new API, so we'll estimate it based on progress
|
||||||
const currentTime = null;
|
const currentTime = null;
|
||||||
|
|
||||||
// Adapt the results when they come in
|
// No adaptation needed - results are already in the correct format
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (results && config) {
|
setAdaptedResults(results);
|
||||||
setAdaptedResults({
|
}, [results]);
|
||||||
id: backtest?.id || '',
|
|
||||||
config,
|
|
||||||
metrics: {
|
|
||||||
totalReturn: results.metrics.totalReturn,
|
|
||||||
sharpeRatio: results.metrics.sharpeRatio,
|
|
||||||
maxDrawdown: results.metrics.maxDrawdown,
|
|
||||||
winRate: results.metrics.winRate,
|
|
||||||
totalTrades: results.metrics.totalTrades,
|
|
||||||
profitableTrades: Math.round(results.metrics.totalTrades * results.metrics.winRate / 100),
|
|
||||||
},
|
|
||||||
positions: [], // Not provided by current API
|
|
||||||
trades: results.trades?.map(t => ({
|
|
||||||
id: `${t.symbol}-${t.entryDate}`,
|
|
||||||
timestamp: t.exitDate,
|
|
||||||
symbol: t.symbol,
|
|
||||||
side: t.pnl > 0 ? 'buy' : 'sell',
|
|
||||||
quantity: t.quantity,
|
|
||||||
price: t.exitPrice,
|
|
||||||
commission: 0,
|
|
||||||
pnl: t.pnl,
|
|
||||||
})) || [],
|
|
||||||
performanceData: results.equity.map(e => ({
|
|
||||||
timestamp: e.date,
|
|
||||||
portfolioValue: e.value,
|
|
||||||
pnl: 0, // Would need to calculate from equity curve
|
|
||||||
drawdown: 0, // Would need to calculate
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [results, config, backtest]);
|
|
||||||
|
|
||||||
const handleConfigSubmit = useCallback(async (newConfig: BacktestConfig) => {
|
const handleConfigSubmit = useCallback(async (newConfig: BacktestConfig) => {
|
||||||
setConfig(newConfig);
|
setConfig(newConfig);
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,6 @@ interface BacktestResultsProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BacktestResults({ status, results, currentTime }: BacktestResultsProps) {
|
export function BacktestResults({ status, results, currentTime }: BacktestResultsProps) {
|
||||||
// Debug logging
|
|
||||||
console.log('BacktestResults - results:', results);
|
|
||||||
console.log('BacktestResults - ohlcData keys:', results?.ohlcData ? Object.keys(results.ohlcData) : 'No ohlcData');
|
|
||||||
console.log('BacktestResults - first symbol data:', results?.ohlcData && Object.keys(results.ohlcData).length > 0 ? results.ohlcData[Object.keys(results.ohlcData)[0]] : 'No data');
|
|
||||||
console.log('BacktestResults - equity data:', results?.equity);
|
|
||||||
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">
|
||||||
|
|
@ -125,14 +120,9 @@ export function BacktestResults({ status, results, currentTime }: BacktestResult
|
||||||
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;
|
||||||
|
|
||||||
console.log('Chart section - hasOhlcData:', hasOhlcData);
|
|
||||||
console.log('Chart section - hasEquityData:', hasEquityData);
|
|
||||||
|
|
||||||
if (hasOhlcData) {
|
if (hasOhlcData) {
|
||||||
const firstSymbol = Object.keys(results.ohlcData)[0];
|
const firstSymbol = Object.keys(results.ohlcData)[0];
|
||||||
const ohlcData = results.ohlcData[firstSymbol];
|
const ohlcData = results.ohlcData[firstSymbol];
|
||||||
console.log('Chart section - using OHLC data for symbol:', firstSymbol);
|
|
||||||
console.log('Chart section - OHLC data points:', ohlcData?.length);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Chart
|
<Chart
|
||||||
|
|
@ -156,7 +146,6 @@ export function BacktestResults({ status, results, currentTime }: BacktestResult
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (hasEquityData) {
|
} else if (hasEquityData) {
|
||||||
console.log('Chart section - using equity data only');
|
|
||||||
return (
|
return (
|
||||||
<Chart
|
<Chart
|
||||||
data={results.equity.map(point => ({
|
data={results.equity.map(point => ({
|
||||||
|
|
@ -171,7 +160,6 @@ export function BacktestResults({ status, results, currentTime }: BacktestResult
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log('Chart section - showing no data message');
|
|
||||||
return (
|
return (
|
||||||
<div className="h-96 bg-background rounded border border-border flex items-center justify-center">
|
<div className="h-96 bg-background rounded border border-border flex items-center justify-center">
|
||||||
<p className="text-sm text-text-muted">
|
<p className="text-sm text-text-muted">
|
||||||
|
|
@ -190,13 +178,13 @@ export function BacktestResults({ status, results, currentTime }: BacktestResult
|
||||||
Trade History
|
Trade History
|
||||||
</h3>
|
</h3>
|
||||||
<TradeLog trades={results.trades.map(trade => ({
|
<TradeLog trades={results.trades.map(trade => ({
|
||||||
id: crypto.randomUUID(),
|
id: trade.id,
|
||||||
timestamp: trade.entryDate,
|
timestamp: trade.exitDate || trade.entryDate,
|
||||||
symbol: trade.symbol,
|
symbol: trade.symbol,
|
||||||
side: 'buy' as const,
|
side: trade.side as 'buy' | 'sell',
|
||||||
quantity: trade.quantity,
|
quantity: trade.quantity,
|
||||||
price: trade.entryPrice,
|
price: trade.exitPrice,
|
||||||
commission: 0,
|
commission: trade.commission,
|
||||||
pnl: trade.pnl
|
pnl: trade.pnl
|
||||||
}))} />
|
}))} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -24,26 +24,43 @@ export interface BacktestJob {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BacktestResult {
|
export interface BacktestResult {
|
||||||
|
// Identification
|
||||||
backtestId: string;
|
backtestId: string;
|
||||||
|
status: 'completed' | 'failed' | 'cancelled';
|
||||||
|
completedAt: string;
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
config: {
|
||||||
|
name: string;
|
||||||
|
strategy: string;
|
||||||
|
symbols: string[];
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
initialCapital: number;
|
||||||
|
commission: number;
|
||||||
|
slippage: number;
|
||||||
|
dataFrequency: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Performance metrics
|
||||||
metrics: {
|
metrics: {
|
||||||
totalReturn: number;
|
totalReturn: number;
|
||||||
sharpeRatio: number;
|
sharpeRatio: number;
|
||||||
maxDrawdown: number;
|
maxDrawdown: number;
|
||||||
winRate: number;
|
winRate: number;
|
||||||
totalTrades: number;
|
totalTrades: number;
|
||||||
profitFactor?: number;
|
profitFactor: number;
|
||||||
|
profitableTrades: number;
|
||||||
|
avgWin: number;
|
||||||
|
avgLoss: number;
|
||||||
|
expectancy: number;
|
||||||
|
calmarRatio: number;
|
||||||
|
sortinoRatio: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Chart data
|
||||||
equity: Array<{ date: string; value: number }>;
|
equity: Array<{ date: string; value: number }>;
|
||||||
trades?: Array<{
|
ohlcData: Record<string, Array<{
|
||||||
symbol: string;
|
|
||||||
entryDate: string;
|
|
||||||
exitDate: string;
|
|
||||||
entryPrice: number;
|
|
||||||
exitPrice: number;
|
|
||||||
quantity: number;
|
|
||||||
pnl: number;
|
|
||||||
}>;
|
|
||||||
ohlcData?: Record<string, Array<{
|
|
||||||
time: number;
|
time: number;
|
||||||
open: number;
|
open: number;
|
||||||
high: number;
|
high: number;
|
||||||
|
|
@ -51,6 +68,41 @@ export interface BacktestResult {
|
||||||
close: number;
|
close: number;
|
||||||
volume?: number;
|
volume?: number;
|
||||||
}>>;
|
}>>;
|
||||||
|
|
||||||
|
// Trade history
|
||||||
|
trades: Array<{
|
||||||
|
id: string;
|
||||||
|
symbol: string;
|
||||||
|
entryDate: string;
|
||||||
|
exitDate: string | null;
|
||||||
|
entryPrice: number;
|
||||||
|
exitPrice: number;
|
||||||
|
quantity: number;
|
||||||
|
side: string;
|
||||||
|
pnl: number;
|
||||||
|
pnlPercent: number;
|
||||||
|
commission: number;
|
||||||
|
duration: number;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
// Positions
|
||||||
|
positions: Array<{
|
||||||
|
symbol: string;
|
||||||
|
quantity: number;
|
||||||
|
averagePrice: number;
|
||||||
|
currentPrice: number;
|
||||||
|
unrealizedPnl: number;
|
||||||
|
realizedPnl: number;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
// Analytics
|
||||||
|
analytics: {
|
||||||
|
drawdownSeries: Array<{ timestamp: number; value: number }>;
|
||||||
|
dailyReturns: number[];
|
||||||
|
monthlyReturns: Record<string, number>;
|
||||||
|
exposureTime: number;
|
||||||
|
riskMetrics: Record<string, number>;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const backtestApi = {
|
export const backtestApi = {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue