socket reruns
This commit is contained in:
parent
a876f3c35b
commit
11c6c19628
29 changed files with 3921 additions and 233 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue