messy work. backtests / mock-data

This commit is contained in:
Boki 2025-07-03 08:37:23 -04:00
parent 4e4a048988
commit fa70ada2bb
51 changed files with 2576 additions and 887 deletions

View file

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

View file

@ -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';