socket reruns

This commit is contained in:
Boki 2025-07-04 17:04:47 -04:00
parent a876f3c35b
commit 11c6c19628
29 changed files with 3921 additions and 233 deletions

View file

@ -86,17 +86,73 @@ export function createBacktestRoutes(container: IServiceContainer): Hono {
}, 500);
}
});
// Pause running backtest
app.post('/pause', async (c) => {
try {
await backtestEngine.pauseBacktest();
return c.json({
message: 'Backtest paused',
timestamp: new Date().toISOString()
});
} catch (error) {
container.logger.error('Error pausing backtest:', error);
return c.json({
error: error instanceof Error ? error.message : 'Failed to pause backtest'
}, 500);
}
});
// Resume paused backtest
app.post('/resume', async (c) => {
try {
await backtestEngine.resumeBacktest();
return c.json({
message: 'Backtest resumed',
timestamp: new Date().toISOString()
});
} catch (error) {
container.logger.error('Error resuming backtest:', error);
return c.json({
error: error instanceof Error ? error.message : 'Failed to resume backtest'
}, 500);
}
});
// Set backtest speed
app.post('/speed', async (c) => {
try {
const body = await c.req.json();
const speed = body.speed ?? 1;
backtestEngine.setSpeedMultiplier(speed);
return c.json({
message: 'Backtest speed updated',
speed: speed === null ? 'unlimited' : speed,
timestamp: new Date().toISOString()
});
} catch (error) {
container.logger.error('Error setting backtest speed:', error);
return c.json({
error: error instanceof Error ? error.message : 'Failed to set speed'
}, 500);
}
});
// Get backtest progress
app.get('/progress', async (c) => {
try {
// In real implementation, would track progress
const status = backtestEngine.getStatus();
return c.json({
status: 'running',
progress: 0.5,
processed: 10000,
total: 20000,
currentTime: new Date().toISOString()
status: status.isPaused ? 'paused' : (status.isRunning ? 'running' : 'idle'),
progress: status.progress,
currentTime: new Date(status.currentTime).toISOString(),
isRunning: status.isRunning,
isPaused: status.isPaused
});
} catch (error) {
container.logger.error('Error getting backtest progress:', error);

View file

@ -93,11 +93,17 @@ export class BacktestEngine extends EventEmitter {
private currentTime: number = 0;
private equityCurve: { timestamp: number; value: number }[] = [];
private isRunning = false;
private isPaused = false;
private speedMultiplier: number | null = 1; // null means unlimited speed
private lastProcessTime = 0;
private dataManager: DataManager;
private marketSimulator: MarketSimulator;
private performanceAnalyzer: PerformanceAnalyzer;
private microstructures: Map<string, MarketMicrostructure> = new Map();
private container: IServiceContainer;
private runId: string | null = null;
private progressUpdateInterval = 100; // Update progress every 100 events
private totalEvents = 0;
private initialCapital: number = 100000;
private commission: number = 0.001; // Default 0.1%
private slippage: number = 0.0001; // Default 0.01%
@ -155,8 +161,33 @@ export class BacktestEngine extends EventEmitter {
this.container.logger.info(`[BacktestEngine] Initial capital set to ${this.initialCapital}, equity curve initialized with ${this.equityCurve.length} points`);
// Generate backtest ID
// Generate backtest ID and store runId if provided
const backtestId = `backtest_${Date.now()}`;
this.runId = config.runId || null;
// Set speed multiplier from config
if (validatedConfig.speed) {
switch (validatedConfig.speed) {
case 'max':
this.speedMultiplier = null; // Unlimited speed
break;
case 'realtime':
this.speedMultiplier = 1;
break;
case '2x':
this.speedMultiplier = 2;
break;
case '5x':
this.speedMultiplier = 5;
break;
case '10x':
this.speedMultiplier = 10;
break;
default:
this.speedMultiplier = null; // Default to max speed
}
this.container.logger.info(`[BacktestEngine] Speed set to: ${validatedConfig.speed} (multiplier: ${this.speedMultiplier})`);
}
try {
// Load historical data with multi-resolution support
@ -486,10 +517,46 @@ export class BacktestEngine extends EventEmitter {
let lastEquityUpdate = 0;
const equityUpdateInterval = 60000; // Update equity every minute
this.container.logger.info(`[BacktestEngine] Processing ${this.eventQueue.length} events`);
this.totalEvents = this.eventQueue.length;
this.container.logger.info(`[BacktestEngine] Processing ${this.totalEvents} events with speed multiplier: ${this.speedMultiplier}`);
let processedCount = 0;
while (this.eventQueue.length > 0 && this.isRunning) {
// Check if paused
if (this.isPaused) {
await new Promise(resolve => setTimeout(resolve, 100));
continue;
}
const event = this.eventQueue.shift()!;
processedCount++;
// Apply speed control if not unlimited
if (this.speedMultiplier !== null && this.speedMultiplier > 0) {
// Base delay in milliseconds for processing each event
// For 1x speed: 1000ms per event (1 event per second)
// For 2x speed: 500ms per event (2 events per second)
// For 10x speed: 100ms per event (10 events per second)
const baseDelay = 1000; // milliseconds
const actualDelay = baseDelay / this.speedMultiplier;
// Add the delay
if (actualDelay > 1) {
if (processedCount % 50 === 0) { // Log every 50 events to avoid spam
this.container.logger.debug(`[BacktestEngine] Applying delay: ${actualDelay}ms (speed: ${this.speedMultiplier}x)`);
}
await new Promise(resolve => setTimeout(resolve, actualDelay));
}
// Also send progress updates more frequently at slower speeds
if (processedCount % Math.max(1, Math.floor(10 / this.speedMultiplier)) === 0) {
const progress = Math.round((processedCount / this.totalEvents) * 100);
this.sendProgressUpdate(progress, new Date(event.timestamp).toISOString());
}
}
this.lastProcessTime = Date.now();
// Advance time
this.currentTime = event.timestamp;
@ -518,18 +585,27 @@ export class BacktestEngine extends EventEmitter {
lastEquityUpdate = this.currentTime;
}
// Emit progress
if (this.eventQueue.length % 1000 === 0) {
// Send progress update via WebSocket (unless already sent in speed control)
if (this.speedMultiplier === null && processedCount % this.progressUpdateInterval === 0) {
const progress = Math.round((processedCount / this.totalEvents) * 100);
this.sendProgressUpdate(progress, new Date(this.currentTime).toISOString());
// Also emit local progress event
this.emit('progress', {
processed: this.eventQueue.length,
processed: processedCount,
total: this.totalEvents,
remaining: this.eventQueue.length,
currentTime: new Date(this.currentTime)
currentTime: new Date(this.currentTime),
progress
});
}
}
// Final equity update
await this.updateEquityCurve();
// Send 100% progress
this.sendProgressUpdate(100, new Date(this.currentTime).toISOString());
}
private async processMarketData(data: MarketData): Promise<void> {
@ -757,6 +833,25 @@ export class BacktestEngine extends EventEmitter {
};
}
private sendProgressUpdate(progress: number, currentDate: string): void {
if (!this.runId) return;
try {
// Try to send WebSocket update to web-api
const url = `http://localhost:2003/api/runs/${this.runId}/progress`;
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ progress, currentDate })
}).catch(err => {
// Ignore errors - WebSocket updates are best-effort
this.container.logger.debug('Failed to send progress update:', err);
});
} catch (error) {
// Ignore errors
}
}
private calculateDrawdown(): { timestamp: number; value: number }[] {
const drawdowns: { timestamp: number; value: number }[] = [];
let peak = this.equityCurve[0]?.value || 0;
@ -913,6 +1008,9 @@ export class BacktestEngine extends EventEmitter {
this.equityCurve = [];
this.pendingOrders.clear();
this.ordersListenerSetup = false;
this.isPaused = false;
this.speedMultiplier = 1;
this.lastProcessTime = 0;
this.marketSimulator.reset();
}
@ -1195,4 +1293,37 @@ export class BacktestEngine extends EventEmitter {
}
}
// Playback control methods
async pauseBacktest(): Promise<void> {
this.isPaused = true;
this.container.logger.info('Backtest paused');
}
async resumeBacktest(): Promise<void> {
this.isPaused = false;
this.container.logger.info('Backtest resumed');
}
async stopBacktest(): Promise<void> {
this.isRunning = false;
this.isPaused = false;
}
setSpeedMultiplier(speed: number | null): void {
this.speedMultiplier = speed;
this.container.logger.info(`Backtest speed multiplier set to: ${speed === null ? 'unlimited' : speed}`);
}
getStatus(): { isRunning: boolean; isPaused: boolean; progress: number; currentTime: number } {
const progress = this.totalEvents > 0
? Math.round(((this.totalEvents - this.eventQueue.length) / this.totalEvents) * 100)
: 0;
return {
isRunning: this.isRunning,
isPaused: this.isPaused,
progress,
currentTime: this.currentTime
};
}
}

View file

@ -298,25 +298,41 @@ export class StorageService {
const msPerInterval = this.getIntervalMilliseconds(interval);
let currentTime = new Date(startTime);
// Starting price based on symbol
let basePrice = symbol === 'AAPL' ? 150 :
symbol === 'MSFT' ? 300 :
symbol === 'GOOGL' ? 120 : 100;
// Use current timestamp to seed randomness for different data each run
const seed = Date.now();
const seedMultiplier = (seed % 1000) / 1000; // 0-1 based on milliseconds
// Starting price based on symbol with some randomness
let basePrice = symbol === 'AAPL' ? 150 + (seedMultiplier * 20 - 10) :
symbol === 'MSFT' ? 300 + (seedMultiplier * 30 - 15) :
symbol === 'GOOGL' ? 120 + (seedMultiplier * 15 - 7.5) :
100 + (seedMultiplier * 10 - 5);
// Random walk seed to make each run different
let walkSeed = seedMultiplier;
while (currentTime <= endTime) {
// Use a pseudo-random based on walkSeed for deterministic but different results
walkSeed = (walkSeed * 9.73 + 0.27) % 1;
// Random walk with trend - increased volatility for testing
const trend = 0.0002; // Slight upward trend
const volatility = 0.01; // 1% volatility (increased from 0.2%)
const change = (Math.random() - 0.5 + trend) * volatility;
const volatility = 0.01; // 1% volatility
const change = (walkSeed - 0.5 + trend) * volatility;
basePrice *= (1 + change);
// Generate OHLC data with more realistic volatility
const open = basePrice * (1 + (Math.random() - 0.5) * 0.005);
const openSeed = (walkSeed * 7.13 + 0.31) % 1;
const highSeed = (walkSeed * 5.17 + 0.41) % 1;
const lowSeed = (walkSeed * 3.19 + 0.59) % 1;
const volumeSeed = (walkSeed * 11.23 + 0.67) % 1;
const open = basePrice * (1 + (openSeed - 0.5) * 0.005);
const close = basePrice;
const high = Math.max(open, close) * (1 + Math.random() * 0.008);
const low = Math.min(open, close) * (1 - Math.random() * 0.008);
const volume = 1000000 + Math.random() * 500000;
const high = Math.max(open, close) * (1 + highSeed * 0.008);
const low = Math.min(open, close) * (1 - lowSeed * 0.008);
const volume = 1000000 + volumeSeed * 500000;
bars.push({
symbol,
@ -332,6 +348,8 @@ export class StorageService {
currentTime = new Date(currentTime.getTime() + msPerInterval);
}
this.container.logger.info(`Generated ${bars.length} mock bars for ${symbol} with seed ${seed}`);
return bars;
}