messy work. backtests / mock-data
This commit is contained in:
parent
4e4a048988
commit
fa70ada2bb
51 changed files with 2576 additions and 887 deletions
|
|
@ -1,12 +1,11 @@
|
|||
import { logger } from '@stock-bot/logger';
|
||||
import { EventEmitter } from 'events';
|
||||
import { MarketData, BacktestConfigSchema, PerformanceMetrics, MarketMicrostructure } from '../types';
|
||||
import { IServiceContainer } from '@stock-bot/di';
|
||||
import { PerformanceAnalyzer } from '../analytics/PerformanceAnalyzer';
|
||||
import { DataManager } from '../data/DataManager';
|
||||
import { StorageService } from '../services/StorageService';
|
||||
import { StrategyManager } from '../strategies/StrategyManager';
|
||||
import { TradingEngine } from '../../core';
|
||||
import { DataManager } from '../data/DataManager';
|
||||
import { BacktestConfigSchema, MarketData, MarketMicrostructure, PerformanceMetrics } from '../types';
|
||||
import { MarketSimulator } from './MarketSimulator';
|
||||
import { PerformanceAnalyzer } from '../analytics/PerformanceAnalyzer';
|
||||
|
||||
interface BacktestEvent {
|
||||
timestamp: number;
|
||||
|
|
@ -35,12 +34,16 @@ export class BacktestEngine extends EventEmitter {
|
|||
private marketSimulator: MarketSimulator;
|
||||
private performanceAnalyzer: PerformanceAnalyzer;
|
||||
private microstructures: Map<string, MarketMicrostructure> = new Map();
|
||||
private container: IServiceContainer;
|
||||
private initialCapital: number = 100000;
|
||||
|
||||
constructor(
|
||||
container: IServiceContainer,
|
||||
private storageService: StorageService,
|
||||
private strategyManager: StrategyManager
|
||||
) {
|
||||
super();
|
||||
this.container = container;
|
||||
this.dataManager = new DataManager(storageService);
|
||||
this.marketSimulator = new MarketSimulator({
|
||||
useHistoricalSpreads: true,
|
||||
|
|
@ -55,11 +58,24 @@ export class BacktestEngine extends EventEmitter {
|
|||
// Validate config
|
||||
const validatedConfig = BacktestConfigSchema.parse(config);
|
||||
|
||||
logger.info(`Starting backtest from ${validatedConfig.startDate} to ${validatedConfig.endDate}`);
|
||||
this.container.logger.info(`Starting backtest from ${validatedConfig.startDate} to ${validatedConfig.endDate}`);
|
||||
|
||||
// Reset state
|
||||
this.reset();
|
||||
this.isRunning = true;
|
||||
this.initialCapital = validatedConfig.initialCapital;
|
||||
|
||||
// Initialize equity curve with starting capital
|
||||
this.equityCurve.push({
|
||||
timestamp: new Date(validatedConfig.startDate).getTime(),
|
||||
value: this.initialCapital
|
||||
});
|
||||
|
||||
// Initialize performance analyzer with starting capital
|
||||
this.performanceAnalyzer.addEquityPoint(
|
||||
new Date(validatedConfig.startDate),
|
||||
this.initialCapital
|
||||
);
|
||||
|
||||
// Generate backtest ID
|
||||
const backtestId = `backtest_${Date.now()}`;
|
||||
|
|
@ -84,7 +100,7 @@ export class BacktestEngine extends EventEmitter {
|
|||
});
|
||||
marketData.sort((a, b) => a.data.timestamp - b.data.timestamp);
|
||||
|
||||
logger.info(`Loaded ${marketData.length} market data points`);
|
||||
this.container.logger.info(`Loaded ${marketData.length} market data points`);
|
||||
|
||||
// Initialize strategies
|
||||
await this.strategyManager.initializeStrategies(validatedConfig.strategies || []);
|
||||
|
|
@ -110,17 +126,18 @@ export class BacktestEngine extends EventEmitter {
|
|||
equityCurve: this.equityCurve,
|
||||
drawdown: this.calculateDrawdown(),
|
||||
dailyReturns: this.calculateDailyReturns(),
|
||||
finalPositions
|
||||
finalPositions,
|
||||
ohlcData: this.getOHLCData(marketData, validatedConfig.symbols)
|
||||
};
|
||||
|
||||
await this.storeResults(result);
|
||||
|
||||
logger.info(`Backtest completed: ${performance.totalTrades} trades, ${performance.totalReturn}% return`);
|
||||
this.container.logger.info(`Backtest completed: ${performance.totalTrades} trades, ${performance.totalReturn}% return`);
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Backtest failed:', error);
|
||||
this.container.logger.error('Backtest failed:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.isRunning = false;
|
||||
|
|
@ -133,30 +150,65 @@ export class BacktestEngine extends EventEmitter {
|
|||
const startDate = new Date(config.startDate);
|
||||
const endDate = new Date(config.endDate);
|
||||
|
||||
for (const symbol of config.symbols) {
|
||||
const bars = await this.storageService.getHistoricalBars(
|
||||
symbol,
|
||||
startDate,
|
||||
endDate,
|
||||
config.dataFrequency
|
||||
);
|
||||
|
||||
// Convert to MarketData format
|
||||
bars.forEach(bar => {
|
||||
data.push({
|
||||
type: 'bar',
|
||||
data: {
|
||||
symbol,
|
||||
open: bar.open,
|
||||
high: bar.high,
|
||||
low: bar.low,
|
||||
close: bar.close,
|
||||
volume: bar.volume,
|
||||
vwap: bar.vwap,
|
||||
timestamp: new Date(bar.timestamp).getTime()
|
||||
try {
|
||||
for (const symbol of config.symbols) {
|
||||
const bars = await this.storageService.getHistoricalBars(
|
||||
symbol,
|
||||
startDate,
|
||||
endDate,
|
||||
config.dataFrequency
|
||||
);
|
||||
|
||||
// If no data found, use mock data
|
||||
if (!bars || bars.length === 0) {
|
||||
this.container.logger.warn(`No historical data found for ${symbol}, using mock data`);
|
||||
|
||||
// Tell the Rust core to generate mock data
|
||||
const tradingEngine = this.strategyManager.getTradingEngine();
|
||||
if (tradingEngine && tradingEngine.generateMockData) {
|
||||
await tradingEngine.generateMockData(
|
||||
symbol,
|
||||
startDate.getTime(),
|
||||
endDate.getTime(),
|
||||
42 // seed for reproducibility
|
||||
);
|
||||
|
||||
// For now, we'll generate mock data on the TypeScript side
|
||||
// as the Rust integration needs more work
|
||||
const mockData = this.generateMockData(symbol, startDate, endDate);
|
||||
data.push(...mockData);
|
||||
} else {
|
||||
// Fallback to TypeScript mock data generation
|
||||
const mockData = this.generateMockData(symbol, startDate, endDate);
|
||||
data.push(...mockData);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Convert to MarketData format
|
||||
bars.forEach(bar => {
|
||||
data.push({
|
||||
type: 'bar',
|
||||
data: {
|
||||
symbol,
|
||||
open: bar.open,
|
||||
high: bar.high,
|
||||
low: bar.low,
|
||||
close: bar.close,
|
||||
volume: bar.volume,
|
||||
vwap: bar.vwap,
|
||||
timestamp: new Date(bar.timestamp).getTime()
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.container.logger.warn('Error loading historical data, using mock data:', error);
|
||||
|
||||
// Generate mock data for all symbols
|
||||
for (const symbol of config.symbols) {
|
||||
const mockData = this.generateMockData(symbol, startDate, endDate);
|
||||
data.push(...mockData);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by timestamp
|
||||
|
|
@ -168,6 +220,48 @@ export class BacktestEngine extends EventEmitter {
|
|||
|
||||
return data;
|
||||
}
|
||||
|
||||
private generateMockData(symbol: string, startDate: Date, endDate: Date): MarketData[] {
|
||||
const data: MarketData[] = [];
|
||||
const startTime = startDate.getTime();
|
||||
const endTime = endDate.getTime();
|
||||
const interval = 24 * 60 * 60 * 1000; // 1 day in milliseconds
|
||||
|
||||
let price = 100; // Base price
|
||||
let currentTime = startTime;
|
||||
|
||||
while (currentTime <= endTime) {
|
||||
// Generate random price movement
|
||||
const changePercent = (Math.random() - 0.5) * 0.04; // +/- 2% daily
|
||||
price = price * (1 + changePercent);
|
||||
|
||||
// Generate OHLC
|
||||
const open = price;
|
||||
const high = price * (1 + Math.random() * 0.02);
|
||||
const low = price * (1 - Math.random() * 0.02);
|
||||
const close = price * (1 + (Math.random() - 0.5) * 0.01);
|
||||
const volume = Math.random() * 1000000 + 500000;
|
||||
|
||||
data.push({
|
||||
type: 'bar',
|
||||
data: {
|
||||
symbol,
|
||||
open,
|
||||
high,
|
||||
low,
|
||||
close,
|
||||
volume,
|
||||
vwap: (open + high + low + close) / 4,
|
||||
timestamp: currentTime
|
||||
}
|
||||
});
|
||||
|
||||
currentTime += interval;
|
||||
price = close; // Next bar opens at previous close
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private populateEventQueue(marketData: MarketData[]): void {
|
||||
// Convert market data to events
|
||||
|
|
@ -234,7 +328,7 @@ export class BacktestEngine extends EventEmitter {
|
|||
|
||||
private async processMarketData(data: MarketData): Promise<void> {
|
||||
const tradingEngine = this.strategyManager.getTradingEngine();
|
||||
if (!tradingEngine) return;
|
||||
if (!tradingEngine) {return;}
|
||||
|
||||
// Process through market simulator for realistic orderbook
|
||||
const orderbook = this.marketSimulator.processMarketData(data);
|
||||
|
|
@ -300,7 +394,7 @@ export class BacktestEngine extends EventEmitter {
|
|||
// Track performance
|
||||
this.performanceAnalyzer.addEquityPoint(
|
||||
new Date(this.currentTime),
|
||||
this.getPortfolioValue()
|
||||
await this.getPortfolioValue()
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -321,18 +415,24 @@ export class BacktestEngine extends EventEmitter {
|
|||
}
|
||||
|
||||
private async updateEquityCurve(): Promise<void> {
|
||||
const tradingEngine = this.strategyManager.getTradingEngine();
|
||||
if (!tradingEngine) return;
|
||||
|
||||
// Get current P&L
|
||||
const [realized, unrealized] = tradingEngine.getTotalPnl();
|
||||
const totalEquity = 100000 + realized + unrealized; // Assuming 100k starting capital
|
||||
const totalEquity = await this.getPortfolioValue();
|
||||
|
||||
this.equityCurve.push({
|
||||
timestamp: this.currentTime,
|
||||
value: totalEquity
|
||||
});
|
||||
}
|
||||
|
||||
private async getPortfolioValue(): Promise<number> {
|
||||
const tradingEngine = this.strategyManager.getTradingEngine();
|
||||
if (!tradingEngine) {
|
||||
return this.initialCapital;
|
||||
}
|
||||
|
||||
// Get current P&L
|
||||
const [realized, unrealized] = tradingEngine.getTotalPnl();
|
||||
return this.initialCapital + realized + unrealized;
|
||||
}
|
||||
|
||||
private calculatePerformance(): PerformanceMetrics {
|
||||
// Use sophisticated performance analyzer
|
||||
|
|
@ -365,49 +465,6 @@ export class BacktestEngine extends EventEmitter {
|
|||
maxDrawdownDuration: drawdownAnalysis.maxDrawdownDuration
|
||||
};
|
||||
}
|
||||
|
||||
const initialEquity = this.equityCurve[0].value;
|
||||
const finalEquity = this.equityCurve[this.equityCurve.length - 1].value;
|
||||
const totalReturn = ((finalEquity - initialEquity) / initialEquity) * 100;
|
||||
|
||||
// Calculate daily returns
|
||||
const dailyReturns = this.calculateDailyReturns();
|
||||
|
||||
// Sharpe ratio (assuming 0% risk-free rate)
|
||||
const avgReturn = dailyReturns.reduce((a, b) => a + b, 0) / dailyReturns.length;
|
||||
const stdDev = Math.sqrt(
|
||||
dailyReturns.reduce((sum, r) => sum + Math.pow(r - avgReturn, 2), 0) / dailyReturns.length
|
||||
);
|
||||
const sharpeRatio = stdDev > 0 ? (avgReturn / stdDev) * Math.sqrt(252) : 0; // Annualized
|
||||
|
||||
// Win rate and profit factor
|
||||
const winningTrades = this.trades.filter(t => t.pnl > 0);
|
||||
const losingTrades = this.trades.filter(t => t.pnl < 0);
|
||||
const winRate = this.trades.length > 0 ? (winningTrades.length / this.trades.length) * 100 : 0;
|
||||
|
||||
const totalWins = winningTrades.reduce((sum, t) => sum + t.pnl, 0);
|
||||
const totalLosses = Math.abs(losingTrades.reduce((sum, t) => sum + t.pnl, 0));
|
||||
const profitFactor = totalLosses > 0 ? totalWins / totalLosses : totalWins > 0 ? Infinity : 0;
|
||||
|
||||
const avgWin = winningTrades.length > 0 ? totalWins / winningTrades.length : 0;
|
||||
const avgLoss = losingTrades.length > 0 ? totalLosses / losingTrades.length : 0;
|
||||
|
||||
// Max drawdown
|
||||
const drawdowns = this.calculateDrawdown();
|
||||
const maxDrawdown = Math.min(...drawdowns.map(d => d.value));
|
||||
|
||||
return {
|
||||
totalReturn,
|
||||
sharpeRatio,
|
||||
sortinoRatio: sharpeRatio * 0.8, // Simplified for now
|
||||
maxDrawdown: Math.abs(maxDrawdown),
|
||||
winRate,
|
||||
profitFactor,
|
||||
avgWin,
|
||||
avgLoss,
|
||||
totalTrades: this.trades.length
|
||||
};
|
||||
}
|
||||
|
||||
private calculateDrawdown(): { timestamp: number; value: number }[] {
|
||||
const drawdowns: { timestamp: number; value: number }[] = [];
|
||||
|
|
@ -451,7 +508,7 @@ export class BacktestEngine extends EventEmitter {
|
|||
|
||||
private async getFinalPositions(): Promise<any[]> {
|
||||
const tradingEngine = this.strategyManager.getTradingEngine();
|
||||
if (!tradingEngine) return [];
|
||||
if (!tradingEngine) {return [];}
|
||||
|
||||
const positions = JSON.parse(tradingEngine.getOpenPositions());
|
||||
return positions;
|
||||
|
|
@ -465,7 +522,7 @@ export class BacktestEngine extends EventEmitter {
|
|||
);
|
||||
|
||||
// Could also store detailed results in a separate table or file
|
||||
logger.debug(`Backtest results stored with ID: ${result.id}`);
|
||||
this.container.logger.debug(`Backtest results stored with ID: ${result.id}`);
|
||||
}
|
||||
|
||||
private reset(): void {
|
||||
|
|
@ -521,7 +578,7 @@ export class BacktestEngine extends EventEmitter {
|
|||
|
||||
private getPortfolioValue(): number {
|
||||
const tradingEngine = this.strategyManager.getTradingEngine();
|
||||
if (!tradingEngine) return 100000; // Default initial capital
|
||||
if (!tradingEngine) {return 100000;} // Default initial capital
|
||||
|
||||
const [realized, unrealized] = tradingEngine.getTotalPnl();
|
||||
return 100000 + realized + unrealized;
|
||||
|
|
@ -529,7 +586,7 @@ export class BacktestEngine extends EventEmitter {
|
|||
|
||||
async stopBacktest(): Promise<void> {
|
||||
this.isRunning = false;
|
||||
logger.info('Backtest stop requested');
|
||||
this.container.logger.info('Backtest stop requested');
|
||||
}
|
||||
|
||||
async exportResults(format: 'json' | 'csv' | 'html' = 'json'): Promise<string> {
|
||||
|
|
@ -631,4 +688,25 @@ export class BacktestEngine extends EventEmitter {
|
|||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
private getOHLCData(marketData: MarketData[], symbols: string[]): Record<string, any[]> {
|
||||
const ohlcData: Record<string, any[]> = {};
|
||||
|
||||
symbols.forEach(symbol => {
|
||||
const symbolData = marketData
|
||||
.filter(d => d.type === 'bar' && d.data.symbol === symbol)
|
||||
.map(d => ({
|
||||
time: Math.floor(d.data.timestamp / 1000), // Convert to seconds for lightweight-charts
|
||||
open: d.data.open,
|
||||
high: d.data.high,
|
||||
low: d.data.low,
|
||||
close: d.data.close,
|
||||
volume: d.data.volume
|
||||
}));
|
||||
|
||||
ohlcData[symbol] = symbolData;
|
||||
});
|
||||
|
||||
return ohlcData;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
import { logger } from '@stock-bot/logger';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
|
||||
const logger = getLogger('MarketSimulator');
|
||||
import { MarketData, Quote, Trade, Bar, OrderBookSnapshot, PriceLevel } from '../types';
|
||||
import { MarketMicrostructure } from '../types/MarketMicrostructure';
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue