finished initial backtest / engine
This commit is contained in:
parent
55b4ca78c9
commit
c106a719e8
18 changed files with 1571 additions and 180 deletions
|
|
@ -92,7 +92,6 @@ export class BacktestEngine extends EventEmitter {
|
|||
private eventQueue: BacktestEvent[] = [];
|
||||
private currentTime: number = 0;
|
||||
private equityCurve: { timestamp: number; value: number }[] = [];
|
||||
private trades: any[] = [];
|
||||
private isRunning = false;
|
||||
private dataManager: DataManager;
|
||||
private marketSimulator: MarketSimulator;
|
||||
|
|
@ -100,6 +99,8 @@ export class BacktestEngine extends EventEmitter {
|
|||
private microstructures: Map<string, MarketMicrostructure> = new Map();
|
||||
private container: IServiceContainer;
|
||||
private initialCapital: number = 100000;
|
||||
private pendingOrders: Map<string, any> = new Map();
|
||||
private ordersListenerSetup = false;
|
||||
|
||||
constructor(
|
||||
container: IServiceContainer,
|
||||
|
|
@ -116,6 +117,9 @@ export class BacktestEngine extends EventEmitter {
|
|||
latencyMs: 1
|
||||
});
|
||||
this.performanceAnalyzer = new PerformanceAnalyzer();
|
||||
|
||||
// Set up order listener immediately
|
||||
this.setupOrderListener();
|
||||
}
|
||||
|
||||
async runBacktest(config: any): Promise<BacktestResult> {
|
||||
|
|
@ -183,14 +187,41 @@ export class BacktestEngine extends EventEmitter {
|
|||
await this.strategyManager.initializeStrategies(strategies);
|
||||
this.container.logger.info(`Initialized ${strategies.length} strategies`);
|
||||
|
||||
// Don't setup strategy order listeners - we already listen to StrategyManager
|
||||
// this.setupStrategyOrderListeners();
|
||||
|
||||
// Convert market data to events
|
||||
this.populateEventQueue(marketData);
|
||||
|
||||
// Main backtest loop
|
||||
await this.processEvents();
|
||||
|
||||
// Get trade history from Rust core
|
||||
const tradingEngine = this.strategyManager.getTradingEngine();
|
||||
let closedTrades: any[] = [];
|
||||
let tradeCount = 0;
|
||||
if (tradingEngine) {
|
||||
if (tradingEngine.getClosedTrades) {
|
||||
try {
|
||||
const tradesJson = tradingEngine.getClosedTrades();
|
||||
closedTrades = JSON.parse(tradesJson);
|
||||
this.container.logger.info(`Retrieved ${closedTrades.length} closed trades from Rust core`);
|
||||
} catch (error) {
|
||||
this.container.logger.warn('Failed to get closed trades from Rust core:', error);
|
||||
}
|
||||
}
|
||||
if (tradingEngine.getTradeCount) {
|
||||
try {
|
||||
tradeCount = tradingEngine.getTradeCount();
|
||||
this.container.logger.info(`Total trades executed: ${tradeCount}`);
|
||||
} catch (error) {
|
||||
this.container.logger.warn('Failed to get trade count from Rust core:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate final metrics
|
||||
const performance = this.calculatePerformance();
|
||||
const performance = this.calculatePerformance(closedTrades);
|
||||
|
||||
// Get final positions
|
||||
const finalPositions = await this.getFinalPositions();
|
||||
|
|
@ -221,7 +252,7 @@ export class BacktestEngine extends EventEmitter {
|
|||
sharpeRatio: performance.sharpeRatio,
|
||||
maxDrawdown: performance.maxDrawdown,
|
||||
winRate: performance.winRate,
|
||||
totalTrades: performance.totalTrades,
|
||||
totalTrades: tradeCount || performance.totalTrades,
|
||||
profitFactor: performance.profitFactor || 0,
|
||||
profitableTrades: Math.round(performance.totalTrades * performance.winRate / 100),
|
||||
avgWin: performance.avgWin || 0,
|
||||
|
|
@ -243,19 +274,19 @@ export class BacktestEngine extends EventEmitter {
|
|||
ohlcData: this.getOHLCData(marketData, validatedConfig.symbols),
|
||||
|
||||
// Trade history (frontend-ready)
|
||||
trades: this.trades.map(trade => ({
|
||||
id: `${trade.symbol}-${trade.entryTime}`,
|
||||
trades: closedTrades.map(trade => ({
|
||||
id: trade.id,
|
||||
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,
|
||||
entryDate: trade.entry_time,
|
||||
exitDate: trade.exit_time,
|
||||
entryPrice: trade.entry_price,
|
||||
exitPrice: trade.exit_price,
|
||||
quantity: trade.quantity,
|
||||
side: trade.side,
|
||||
pnl: trade.pnl || 0,
|
||||
pnlPercent: trade.returnPct || 0,
|
||||
commission: trade.commission || 0,
|
||||
duration: trade.holdingPeriod || 0
|
||||
side: trade.side === 'Buy' ? 'buy' : 'sell',
|
||||
pnl: trade.pnl,
|
||||
pnlPercent: trade.pnl_percent,
|
||||
commission: trade.commission,
|
||||
duration: trade.duration_ms
|
||||
})),
|
||||
|
||||
// Final positions
|
||||
|
|
@ -273,8 +304,8 @@ export class BacktestEngine extends EventEmitter {
|
|||
drawdownSeries: this.calculateDrawdown(),
|
||||
dailyReturns: this.calculateDailyReturns(),
|
||||
monthlyReturns: this.calculateMonthlyReturns(),
|
||||
exposureTime: this.calculateExposureTime(),
|
||||
riskMetrics: this.calculateRiskMetrics()
|
||||
exposureTime: this.calculateExposureTime(closedTrades),
|
||||
riskMetrics: this.calculateRiskMetrics(closedTrades)
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -445,6 +476,8 @@ 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`);
|
||||
|
||||
while (this.eventQueue.length > 0 && this.isRunning) {
|
||||
const event = this.eventQueue.shift()!;
|
||||
|
||||
|
|
@ -452,12 +485,6 @@ export class BacktestEngine extends EventEmitter {
|
|||
this.currentTime = event.timestamp;
|
||||
if (tradingEngine) {
|
||||
await tradingEngine.advanceTime(this.currentTime);
|
||||
|
||||
// Check for any fills
|
||||
const fills = tradingEngine.getFills ? tradingEngine.getFills() : [];
|
||||
for (const fill of fills) {
|
||||
await this.processFill(fill);
|
||||
}
|
||||
}
|
||||
|
||||
// Process event based on type
|
||||
|
|
@ -484,7 +511,7 @@ export class BacktestEngine extends EventEmitter {
|
|||
// Emit progress
|
||||
if (this.eventQueue.length % 1000 === 0) {
|
||||
this.emit('progress', {
|
||||
processed: this.trades.length,
|
||||
processed: this.eventQueue.length,
|
||||
remaining: this.eventQueue.length,
|
||||
currentTime: new Date(this.currentTime)
|
||||
});
|
||||
|
|
@ -496,8 +523,12 @@ export class BacktestEngine extends EventEmitter {
|
|||
}
|
||||
|
||||
private async processMarketData(data: MarketData): Promise<void> {
|
||||
this.container.logger.info(`[BacktestEngine] processMarketData called for ${data.data.symbol} @ ${data.data.close}`);
|
||||
const tradingEngine = this.strategyManager.getTradingEngine();
|
||||
if (!tradingEngine) {return;}
|
||||
if (!tradingEngine) {
|
||||
this.container.logger.warn(`[BacktestEngine] No trading engine available`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Process through market simulator for realistic orderbook
|
||||
const orderbook = this.marketSimulator.processMarketData(data);
|
||||
|
|
@ -558,6 +589,7 @@ export class BacktestEngine extends EventEmitter {
|
|||
}
|
||||
|
||||
// Let strategies process the data
|
||||
this.container.logger.info(`[BacktestEngine] Forwarding data to strategy manager for ${data.data.symbol}`);
|
||||
await this.strategyManager.onMarketData(data);
|
||||
|
||||
// Check for any pending orders that should be filled
|
||||
|
|
@ -576,43 +608,45 @@ export class BacktestEngine extends EventEmitter {
|
|||
}
|
||||
|
||||
private async processFill(fill: any): Promise<void> {
|
||||
this.container.logger.info(`[BacktestEngine] Processing fill: ${fill.side} ${fill.quantity} ${fill.symbol} @ ${fill.price} with timestamp ${fill.timestamp}`);
|
||||
|
||||
// Process fill in trading engine for position tracking
|
||||
const tradingEngine = this.strategyManager.getTradingEngine();
|
||||
if (tradingEngine) {
|
||||
const fillResult = await tradingEngine.processFill(
|
||||
fill.symbol,
|
||||
fill.price,
|
||||
fill.quantity,
|
||||
fill.side,
|
||||
fill.commission || 0
|
||||
);
|
||||
this.container.logger.debug('Fill processed:', fillResult);
|
||||
}
|
||||
|
||||
// Handle trade recording based on side
|
||||
if (fill.side === 'buy') {
|
||||
// Create new trade entry for buy orders
|
||||
const trade = {
|
||||
symbol: fill.symbol,
|
||||
side: fill.side,
|
||||
quantity: fill.quantity,
|
||||
entryPrice: fill.price,
|
||||
entryTime: fill.timestamp || this.currentTime,
|
||||
exitPrice: null,
|
||||
exitTime: null,
|
||||
pnl: 0,
|
||||
returnPct: 0,
|
||||
commission: fill.commission || 0,
|
||||
currentPrice: fill.price,
|
||||
holdingPeriod: 0,
|
||||
backtestTime: this.currentTime
|
||||
};
|
||||
this.container.logger.info(`[BacktestEngine] Trading engine available, processing fill`);
|
||||
this.container.logger.info(`[BacktestEngine] Current time in trading engine before advance: ${tradingEngine.getCurrentTime()}`);
|
||||
|
||||
this.trades.push(trade);
|
||||
this.container.logger.info(`💵 Buy trade opened: ${fill.quantity} ${fill.symbol} @ ${fill.price}`);
|
||||
} else if (fill.side === 'sell') {
|
||||
// Update existing trades for sell orders
|
||||
this.updateClosedTrades(fill);
|
||||
// Make sure time is properly advanced
|
||||
if (fill.timestamp) {
|
||||
await tradingEngine.advanceTime(fill.timestamp);
|
||||
this.container.logger.info(`[BacktestEngine] Advanced trading engine time to ${fill.timestamp}, current time now: ${tradingEngine.getCurrentTime()}`);
|
||||
}
|
||||
|
||||
// Use the new process_fill_with_metadata method if available
|
||||
if (tradingEngine.processFillWithMetadata) {
|
||||
const fillResult = await tradingEngine.processFillWithMetadata(
|
||||
fill.symbol,
|
||||
fill.price,
|
||||
fill.quantity,
|
||||
fill.side,
|
||||
fill.commission || 0,
|
||||
fill.orderId || null,
|
||||
fill.strategyId || null
|
||||
);
|
||||
this.container.logger.debug('Fill processed with tracking:', fillResult);
|
||||
} else {
|
||||
// Fallback to old method
|
||||
const fillResult = await tradingEngine.processFill(
|
||||
fill.symbol,
|
||||
fill.price,
|
||||
fill.quantity,
|
||||
fill.side,
|
||||
fill.commission || 0
|
||||
);
|
||||
this.container.logger.debug('Fill processed:', fillResult);
|
||||
}
|
||||
} else {
|
||||
this.container.logger.warn(`[BacktestEngine] No trading engine available to process fill!`);
|
||||
}
|
||||
|
||||
// Notify strategies of fill
|
||||
|
|
@ -651,23 +685,24 @@ export class BacktestEngine extends EventEmitter {
|
|||
return this.initialCapital + realized + unrealized;
|
||||
}
|
||||
|
||||
private calculatePerformance(): PerformanceMetrics {
|
||||
private calculatePerformance(closedTrades: any[] = []): PerformanceMetrics {
|
||||
// Use sophisticated performance analyzer
|
||||
this.trades.forEach(trade => {
|
||||
// Add closed trades from Rust core
|
||||
closedTrades.forEach(trade => {
|
||||
this.performanceAnalyzer.addTrade({
|
||||
entryTime: new Date(trade.entryTime),
|
||||
exitTime: new Date(trade.exitTime || this.currentTime),
|
||||
entryTime: new Date(trade.entry_time),
|
||||
exitTime: new Date(trade.exit_time),
|
||||
symbol: trade.symbol,
|
||||
side: trade.side,
|
||||
entryPrice: trade.entryPrice,
|
||||
exitPrice: trade.exitPrice || trade.currentPrice,
|
||||
side: trade.side === 'Buy' ? 'long' : 'short',
|
||||
entryPrice: trade.entry_price,
|
||||
exitPrice: trade.exit_price,
|
||||
quantity: trade.quantity,
|
||||
commission: trade.commission || 0,
|
||||
pnl: trade.pnl || 0,
|
||||
returnPct: trade.returnPct || 0,
|
||||
holdingPeriod: trade.holdingPeriod || 0,
|
||||
mae: trade.mae || 0,
|
||||
mfe: trade.mfe || 0
|
||||
returnPct: trade.pnl_percent || 0,
|
||||
holdingPeriod: trade.duration_ms / 60000, // Convert to minutes
|
||||
mae: 0, // Not tracked yet
|
||||
mfe: 0 // Not tracked yet
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -724,14 +759,12 @@ export class BacktestEngine extends EventEmitter {
|
|||
return monthlyReturns;
|
||||
}
|
||||
|
||||
private calculateExposureTime(): number {
|
||||
if (this.trades.length === 0) return 0;
|
||||
private calculateExposureTime(closedTrades: any[] = []): number {
|
||||
if (closedTrades.length === 0) return 0;
|
||||
|
||||
let totalExposureTime = 0;
|
||||
for (const trade of this.trades) {
|
||||
if (trade.exitTime) {
|
||||
totalExposureTime += trade.exitTime - trade.entryTime;
|
||||
}
|
||||
for (const trade of closedTrades) {
|
||||
totalExposureTime += trade.duration_ms || 0;
|
||||
}
|
||||
|
||||
// Use equity curve to determine actual trading period
|
||||
|
|
@ -742,7 +775,7 @@ export class BacktestEngine extends EventEmitter {
|
|||
return totalTime > 0 ? (totalExposureTime / totalTime) * 100 : 0;
|
||||
}
|
||||
|
||||
private calculateRiskMetrics(): Record<string, number> {
|
||||
private calculateRiskMetrics(closedTrades: any[] = []): Record<string, number> {
|
||||
const returns = this.calculateDailyReturns();
|
||||
|
||||
// Calculate various risk metrics
|
||||
|
|
@ -761,7 +794,7 @@ export class BacktestEngine extends EventEmitter {
|
|||
var95: this.calculateVaR(returns, 0.95),
|
||||
var99: this.calculateVaR(returns, 0.99),
|
||||
cvar95: this.calculateCVaR(returns, 0.95),
|
||||
maxConsecutiveLosses: this.calculateMaxConsecutiveLosses()
|
||||
maxConsecutiveLosses: this.calculateMaxConsecutiveLosses(closedTrades)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -778,11 +811,11 @@ export class BacktestEngine extends EventEmitter {
|
|||
return tail.reduce((a, b) => a + b, 0) / tail.length;
|
||||
}
|
||||
|
||||
private calculateMaxConsecutiveLosses(): number {
|
||||
private calculateMaxConsecutiveLosses(closedTrades: any[] = []): number {
|
||||
let maxLosses = 0;
|
||||
let currentLosses = 0;
|
||||
|
||||
for (const trade of this.trades) {
|
||||
for (const trade of closedTrades) {
|
||||
if (trade.pnl && trade.pnl < 0) {
|
||||
currentLosses++;
|
||||
maxLosses = Math.max(maxLosses, currentLosses);
|
||||
|
|
@ -839,7 +872,8 @@ export class BacktestEngine extends EventEmitter {
|
|||
this.eventQueue = [];
|
||||
this.currentTime = 0;
|
||||
this.equityCurve = [];
|
||||
this.trades = [];
|
||||
this.pendingOrders.clear();
|
||||
this.ordersListenerSetup = false;
|
||||
this.marketSimulator.reset();
|
||||
}
|
||||
|
||||
|
|
@ -900,9 +934,21 @@ export class BacktestEngine extends EventEmitter {
|
|||
}
|
||||
|
||||
async exportResults(format: 'json' | 'csv' | 'html' = 'json'): Promise<string> {
|
||||
// Get trades from Rust core
|
||||
const tradingEngine = this.strategyManager.getTradingEngine();
|
||||
let closedTrades: any[] = [];
|
||||
if (tradingEngine && tradingEngine.getClosedTrades) {
|
||||
try {
|
||||
const tradesJson = tradingEngine.getClosedTrades();
|
||||
closedTrades = JSON.parse(tradesJson);
|
||||
} catch (error) {
|
||||
this.container.logger.warn('Failed to get closed trades for export:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const result = {
|
||||
summary: this.calculatePerformance(),
|
||||
trades: this.trades,
|
||||
summary: this.calculatePerformance(closedTrades),
|
||||
trades: closedTrades,
|
||||
equityCurve: this.equityCurve,
|
||||
drawdowns: this.calculateDrawdown(),
|
||||
dataQuality: this.dataManager.getDataQualityReport(),
|
||||
|
|
@ -927,14 +973,14 @@ export class BacktestEngine extends EventEmitter {
|
|||
// Simple CSV conversion for trades
|
||||
const headers = ['Date', 'Symbol', 'Side', 'Entry', 'Exit', 'Quantity', 'PnL', 'Return%'];
|
||||
const rows = result.trades.map(t => [
|
||||
new Date(t.entryTime).toISOString(),
|
||||
new Date(t.entry_time).toISOString(),
|
||||
t.symbol,
|
||||
t.side,
|
||||
t.entryPrice,
|
||||
t.exitPrice,
|
||||
t.side === 'Buy' ? 'buy' : 'sell',
|
||||
t.entry_price,
|
||||
t.exit_price,
|
||||
t.quantity,
|
||||
t.pnl,
|
||||
t.returnPct
|
||||
t.pnl_percent
|
||||
]);
|
||||
|
||||
return [headers, ...rows].map(row => row.join(',')).join('\n');
|
||||
|
|
@ -1021,98 +1067,100 @@ export class BacktestEngine extends EventEmitter {
|
|||
return ohlcData;
|
||||
}
|
||||
|
||||
private pendingOrders: Map<string, any> = new Map();
|
||||
private ordersListenerSetup = false;
|
||||
private setupOrderListener(): void {
|
||||
if (this.ordersListenerSetup) return;
|
||||
|
||||
// Listen for orders from strategy manager
|
||||
this.strategyManager.on('order', async (orderEvent: any) => {
|
||||
this.container.logger.info('[BacktestEngine] Order from StrategyManager:', orderEvent);
|
||||
this.pendingOrders.set(orderEvent.orderId, {
|
||||
...orderEvent,
|
||||
timestamp: this.currentTime
|
||||
});
|
||||
});
|
||||
|
||||
this.ordersListenerSetup = true;
|
||||
}
|
||||
|
||||
private setupStrategyOrderListeners(): void {
|
||||
// Listen for orders directly from strategies
|
||||
const strategies = this.strategyManager.getAllStrategies();
|
||||
this.container.logger.info(`Setting up order listeners for ${strategies.length} strategies`);
|
||||
|
||||
strategies.forEach(strategy => {
|
||||
this.container.logger.info(`Setting up order listener for strategy: ${strategy.config.id}`);
|
||||
|
||||
const orderHandler = async (order: any) => {
|
||||
const orderId = `${strategy.config.id}-${Date.now()}-${Math.random()}`;
|
||||
this.container.logger.info(`[BacktestEngine] 🔔 Order from strategy ${strategy.config.id}:`, order);
|
||||
this.container.logger.info(`[BacktestEngine] Adding order ${orderId} to pendingOrders. Current size: ${this.pendingOrders.size}`);
|
||||
this.pendingOrders.set(orderId, {
|
||||
strategyId: strategy.config.id,
|
||||
orderId,
|
||||
order,
|
||||
timestamp: this.currentTime
|
||||
});
|
||||
this.container.logger.info(`[BacktestEngine] After adding, pendingOrders size: ${this.pendingOrders.size}`);
|
||||
};
|
||||
|
||||
strategy.on('order', orderHandler);
|
||||
|
||||
// Also listen for signals
|
||||
strategy.on('signal', (signal: any) => {
|
||||
this.container.logger.info(`[BacktestEngine] 📊 Signal from strategy ${strategy.config.id}:`, signal);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async checkAndFillOrders(data: MarketData): Promise<void> {
|
||||
if (data.type !== 'bar') return;
|
||||
|
||||
const symbol = data.data.symbol;
|
||||
const currentPrice = data.data.close;
|
||||
|
||||
// Listen for orders from strategy manager
|
||||
if (!this.ordersListenerSetup) {
|
||||
this.strategyManager.on('order', async (orderEvent: any) => {
|
||||
this.container.logger.info('New order received:', orderEvent);
|
||||
this.pendingOrders.set(orderEvent.orderId, {
|
||||
...orderEvent,
|
||||
timestamp: this.currentTime
|
||||
});
|
||||
});
|
||||
this.ordersListenerSetup = true;
|
||||
}
|
||||
|
||||
// Check pending orders for this symbol
|
||||
const ordersToFill: string[] = [];
|
||||
|
||||
this.container.logger.info(`[checkAndFillOrders] Checking ${this.pendingOrders.size} pending orders for ${symbol}`);
|
||||
|
||||
for (const [orderId, orderEvent] of this.pendingOrders) {
|
||||
if (orderEvent.order.symbol === symbol) {
|
||||
// For market orders, fill immediately at current price
|
||||
if (orderEvent.order.orderType === 'market') {
|
||||
const fillPrice = orderEvent.order.side === 'buy' ?
|
||||
currentPrice * (1 + (this.marketSimulator.config.slippage || 0.0001)) :
|
||||
currentPrice * (1 - (this.marketSimulator.config.slippage || 0.0001));
|
||||
|
||||
const fill = {
|
||||
orderId,
|
||||
symbol: orderEvent.order.symbol,
|
||||
side: orderEvent.order.side,
|
||||
quantity: orderEvent.order.quantity,
|
||||
price: fillPrice,
|
||||
timestamp: this.currentTime,
|
||||
commission: orderEvent.order.quantity * fillPrice * 0.001, // 0.1% commission
|
||||
strategyId: orderEvent.strategyId
|
||||
};
|
||||
|
||||
this.container.logger.info(`✅ Filling ${orderEvent.order.side} order for ${symbol} @ ${fillPrice}`);
|
||||
|
||||
await this.processFill(fill);
|
||||
this.pendingOrders.delete(orderId);
|
||||
this.container.logger.info(`[checkAndFillOrders] Found market order ${orderId} for ${symbol}`);
|
||||
ordersToFill.push(orderId);
|
||||
}
|
||||
// TODO: Handle limit orders
|
||||
}
|
||||
}
|
||||
|
||||
// Process fills
|
||||
for (const orderId of ordersToFill) {
|
||||
const orderEvent = this.pendingOrders.get(orderId);
|
||||
if (!orderEvent) continue;
|
||||
|
||||
const fillPrice = orderEvent.order.side === 'buy' ?
|
||||
currentPrice * (1 + (this.marketSimulator.config.slippage || 0.0001)) :
|
||||
currentPrice * (1 - (this.marketSimulator.config.slippage || 0.0001));
|
||||
|
||||
const fill = {
|
||||
orderId,
|
||||
symbol: orderEvent.order.symbol,
|
||||
side: orderEvent.order.side,
|
||||
quantity: orderEvent.order.quantity,
|
||||
price: fillPrice,
|
||||
timestamp: this.currentTime,
|
||||
commission: orderEvent.order.quantity * fillPrice * 0.001, // 0.1% commission
|
||||
strategyId: orderEvent.strategyId
|
||||
};
|
||||
|
||||
this.container.logger.info(`✅ Filling ${orderEvent.order.side} order for ${symbol} @ ${fillPrice} at timestamp ${this.currentTime} (${new Date(this.currentTime).toISOString()})`);
|
||||
|
||||
// Remove from pending BEFORE processing to avoid duplicate fills
|
||||
this.pendingOrders.delete(orderId);
|
||||
|
||||
// Process the fill (this will update trading engine AND notify strategies)
|
||||
await this.processFill(fill);
|
||||
}
|
||||
}
|
||||
|
||||
private updateClosedTrades(fill: any): void {
|
||||
// Find open trades for this symbol
|
||||
const openTrades = this.trades.filter(t =>
|
||||
t.symbol === fill.symbol &&
|
||||
t.side === 'buy' &&
|
||||
!t.exitTime
|
||||
);
|
||||
|
||||
if (openTrades.length > 0) {
|
||||
// FIFO - close oldest trade first
|
||||
const tradeToClose = openTrades[0];
|
||||
tradeToClose.exitPrice = fill.price;
|
||||
tradeToClose.exitTime = fill.timestamp || this.currentTime;
|
||||
tradeToClose.pnl = (tradeToClose.exitPrice - tradeToClose.entryPrice) * tradeToClose.quantity - tradeToClose.commission - (fill.commission || 0);
|
||||
tradeToClose.returnPct = ((tradeToClose.exitPrice - tradeToClose.entryPrice) / tradeToClose.entryPrice) * 100;
|
||||
tradeToClose.holdingPeriod = tradeToClose.exitTime - tradeToClose.entryTime;
|
||||
|
||||
this.container.logger.info(`💰 Trade closed: P&L ${tradeToClose.pnl.toFixed(2)}, Return ${tradeToClose.returnPct.toFixed(2)}%`);
|
||||
} else {
|
||||
// Log if we're trying to sell without an open position
|
||||
this.container.logger.warn(`⚠️ Sell order for ${fill.symbol} but no open trades found`);
|
||||
|
||||
// Still record it as a trade for tracking purposes (short position)
|
||||
const trade = {
|
||||
symbol: fill.symbol,
|
||||
side: 'sell',
|
||||
quantity: fill.quantity,
|
||||
entryPrice: fill.price,
|
||||
entryTime: fill.timestamp || this.currentTime,
|
||||
exitPrice: null,
|
||||
exitTime: null,
|
||||
pnl: 0,
|
||||
returnPct: 0,
|
||||
commission: fill.commission || 0,
|
||||
currentPrice: fill.price,
|
||||
holdingPeriod: 0,
|
||||
backtestTime: this.currentTime
|
||||
};
|
||||
|
||||
this.trades.push(trade);
|
||||
this.container.logger.info(`💵 Short trade opened: ${fill.quantity} ${fill.symbol} @ ${fill.price}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -74,7 +74,10 @@ export abstract class BaseStrategy extends EventEmitter {
|
|||
|
||||
// Market data handling
|
||||
async onMarketData(data: MarketData): Promise<void> {
|
||||
if (!this.isActive) return;
|
||||
if (!this.isActive) {
|
||||
logger.info(`[BaseStrategy] Strategy ${this.config.id} received market data but is not active`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Update any indicators or state
|
||||
|
|
@ -210,20 +213,27 @@ export abstract class BaseStrategy extends EventEmitter {
|
|||
// Check if we already have a position
|
||||
const currentPosition = this.getPosition(signal.symbol);
|
||||
|
||||
// Use quantity from signal metadata if provided
|
||||
const quantity = signal.metadata?.quantity || this.calculatePositionSize(signal);
|
||||
|
||||
logger.info(`[BaseStrategy] Converting signal to order: ${signal.type} ${quantity} ${signal.symbol}, current position: ${currentPosition}`);
|
||||
|
||||
// Simple logic - can be overridden by specific strategies
|
||||
if (signal.type === 'buy' && currentPosition <= 0) {
|
||||
if (signal.type === 'buy') {
|
||||
// Allow buying to open long or close short
|
||||
return {
|
||||
symbol: signal.symbol,
|
||||
side: 'buy',
|
||||
quantity: this.calculatePositionSize(signal),
|
||||
quantity: quantity,
|
||||
orderType: 'market',
|
||||
timeInForce: 'DAY'
|
||||
};
|
||||
} else if (signal.type === 'sell' && currentPosition >= 0) {
|
||||
} else if (signal.type === 'sell') {
|
||||
// Allow selling to close long or open short
|
||||
return {
|
||||
symbol: signal.symbol,
|
||||
side: 'sell',
|
||||
quantity: this.calculatePositionSize(signal),
|
||||
quantity: quantity,
|
||||
orderType: 'market',
|
||||
timeInForce: 'DAY'
|
||||
};
|
||||
|
|
|
|||
|
|
@ -163,6 +163,12 @@ export class StrategyManager extends EventEmitter {
|
|||
|
||||
private async handleMarketData(data: MarketData): Promise<void> {
|
||||
// Forward to all active strategies
|
||||
if (this.activeStrategies.size === 0) {
|
||||
this.container.logger.info(`[StrategyManager] No active strategies to process market data! All strategies: ${Array.from(this.strategies.keys()).join(', ')}`);
|
||||
} else {
|
||||
this.container.logger.info(`[StrategyManager] Forwarding market data to ${this.activeStrategies.size} active strategies: ${Array.from(this.activeStrategies).join(', ')}`);
|
||||
}
|
||||
|
||||
for (const strategyId of this.activeStrategies) {
|
||||
const strategy = this.strategies.get(strategyId);
|
||||
if (strategy) {
|
||||
|
|
@ -267,6 +273,10 @@ export class StrategyManager extends EventEmitter {
|
|||
getStrategy(strategyId: string): BaseStrategy | undefined {
|
||||
return this.strategies.get(strategyId);
|
||||
}
|
||||
|
||||
getAllStrategies(): BaseStrategy[] {
|
||||
return Array.from(this.strategies.values());
|
||||
}
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
this.container.logger.info('Shutting down strategy manager...');
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ const logger = getLogger('SimpleMovingAverageCrossover');
|
|||
export class SimpleMovingAverageCrossover extends BaseStrategy {
|
||||
private priceHistory = new Map<string, number[]>();
|
||||
private lastTradeTime = new Map<string, number>();
|
||||
private barCount = new Map<string, number>();
|
||||
private totalSignals = 0;
|
||||
|
||||
// Strategy parameters
|
||||
|
|
@ -30,12 +31,17 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
|
|||
// Update price history
|
||||
if (!this.priceHistory.has(symbol)) {
|
||||
this.priceHistory.set(symbol, []);
|
||||
this.barCount.set(symbol, 0);
|
||||
logger.info(`📊 Starting to track ${symbol} @ ${price}`);
|
||||
}
|
||||
|
||||
const history = this.priceHistory.get(symbol)!;
|
||||
history.push(price);
|
||||
|
||||
// Increment bar count
|
||||
const currentBar = (this.barCount.get(symbol) || 0) + 1;
|
||||
this.barCount.set(symbol, currentBar);
|
||||
|
||||
// Keep only needed history
|
||||
if (history.length > this.SLOW_PERIOD * 2) {
|
||||
history.shift();
|
||||
|
|
@ -74,8 +80,9 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
|
|||
const timestamp = new Date(data.data.timestamp).toISOString().split('T')[0];
|
||||
|
||||
// Check minimum holding period
|
||||
const currentBar = this.barCount.get(symbol) || 0;
|
||||
const lastTradeBar = this.lastTradeTime.get(symbol) || 0;
|
||||
const barsSinceLastTrade = lastTradeBar > 0 ? history.length - lastTradeBar : Number.MAX_SAFE_INTEGER;
|
||||
const barsSinceLastTrade = lastTradeBar > 0 ? currentBar - lastTradeBar : Number.MAX_SAFE_INTEGER;
|
||||
|
||||
// Detect crossovers FIRST
|
||||
const goldenCross = prevFastMA <= prevSlowMA && fastMA > slowMA;
|
||||
|
|
@ -87,7 +94,7 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
|
|||
const masAreClose = Math.abs(maDiffPct) < 1.0; // Within 1%
|
||||
|
||||
if (history.length % this.DEBUG_INTERVAL === 0 || masAreClose || goldenCross || deathCross) {
|
||||
logger.info(`${symbol} @ ${timestamp} [Bar ${history.length}]:`);
|
||||
logger.info(`${symbol} @ ${timestamp} [Bar ${currentBar}]:`);
|
||||
logger.info(` Price: $${currentPrice.toFixed(2)}`);
|
||||
logger.info(` Fast MA (${this.FAST_PERIOD}): $${fastMA.toFixed(2)}`);
|
||||
logger.info(` Slow MA (${this.SLOW_PERIOD}): $${slowMA.toFixed(2)}`);
|
||||
|
|
@ -131,7 +138,7 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
|
|||
}
|
||||
};
|
||||
|
||||
this.lastTradeTime.set(symbol, history.length);
|
||||
this.lastTradeTime.set(symbol, currentBar);
|
||||
this.totalSignals++;
|
||||
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
|
||||
return signal;
|
||||
|
|
@ -160,7 +167,7 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
|
|||
}
|
||||
};
|
||||
|
||||
this.lastTradeTime.set(symbol, history.length);
|
||||
this.lastTradeTime.set(symbol, currentBar);
|
||||
this.totalSignals++;
|
||||
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
|
||||
return signal;
|
||||
|
|
@ -194,7 +201,7 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
|
|||
}
|
||||
};
|
||||
|
||||
this.lastTradeTime.set(symbol, history.length);
|
||||
this.lastTradeTime.set(symbol, currentBar);
|
||||
this.totalSignals++;
|
||||
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
|
||||
return signal;
|
||||
|
|
@ -222,7 +229,7 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
|
|||
}
|
||||
};
|
||||
|
||||
this.lastTradeTime.set(symbol, history.length);
|
||||
this.lastTradeTime.set(symbol, currentBar);
|
||||
this.totalSignals++;
|
||||
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
|
||||
return signal;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue