finished initial backtest / engine

This commit is contained in:
Boki 2025-07-03 12:49:22 -04:00
parent 55b4ca78c9
commit c106a719e8
18 changed files with 1571 additions and 180 deletions

View file

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

View file

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

View file

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

View file

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