backtest work

This commit is contained in:
Boki 2025-07-03 11:04:33 -04:00
parent 143e2e1678
commit 55b4ca78c9
6 changed files with 427 additions and 129 deletions

View file

@ -121,6 +121,8 @@ export abstract class BaseStrategy extends EventEmitter {
? currentPos + fill.quantity
: currentPos - fill.quantity;
logger.info(`[BaseStrategy] Position update for ${update.symbol}: ${currentPos} -> ${newPos} (${update.side} ${fill.quantity})`);
if (Math.abs(newPos) < 0.0001) {
this.positions.delete(update.symbol);
} else {

View file

@ -6,15 +6,15 @@ const logger = getLogger('SimpleMovingAverageCrossover');
export class SimpleMovingAverageCrossover extends BaseStrategy {
private priceHistory = new Map<string, number[]>();
private positions = new Map<string, number>();
private lastSignalTime = new Map<string, number>();
private lastTradeTime = new Map<string, number>();
private totalSignals = 0;
// Strategy parameters
private readonly FAST_PERIOD = 10;
private readonly SLOW_PERIOD = 20;
private readonly POSITION_SIZE = 0.1; // 10% of capital per position
private readonly MIN_SIGNAL_INTERVAL = 24 * 60 * 60 * 1000; // 1 day minimum between signals
private readonly MIN_HOLDING_BARS = 1; // Minimum bars to hold position
private readonly DEBUG_INTERVAL = 20; // Log every N bars for debugging
constructor(config: any, modeManager?: any, executionService?: any) {
super(config, modeManager, executionService);
@ -69,82 +69,167 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
const prevFastMA = this.calculateSMA(prevHistory, this.FAST_PERIOD);
const prevSlowMA = this.calculateSMA(prevHistory, this.SLOW_PERIOD);
const currentPosition = this.positions.get(symbol) || 0;
const currentPosition = this.getPosition(symbol);
const currentPrice = data.data.close;
const timestamp = new Date(data.data.timestamp).toISOString().split('T')[0];
// Log every 50 bars to track MA values and crossover conditions
if (history.length % 50 === 0) {
logger.info(`${symbol} @ ${timestamp} - Price: ${currentPrice.toFixed(2)}, Fast MA: ${fastMA.toFixed(2)}, Slow MA: ${slowMA.toFixed(2)}, Position: ${currentPosition}`);
logger.debug(`${symbol} - Prev Fast MA: ${prevFastMA.toFixed(2)}, Prev Slow MA: ${prevSlowMA.toFixed(2)}`);
logger.debug(`${symbol} - Fast > Slow: ${fastMA > slowMA}, Prev Fast <= Prev Slow: ${prevFastMA <= prevSlowMA}`);
}
// Check minimum holding period
const lastTradeBar = this.lastTradeTime.get(symbol) || 0;
const barsSinceLastTrade = lastTradeBar > 0 ? history.length - lastTradeBar : Number.MAX_SAFE_INTEGER;
// Detect crossovers with detailed logging
// Detect crossovers FIRST
const goldenCross = prevFastMA <= prevSlowMA && fastMA > slowMA;
const deathCross = prevFastMA >= prevSlowMA && fastMA < slowMA;
if (goldenCross && currentPosition === 0) {
// Golden cross - buy signal
// Enhanced debugging - log more frequently and when MAs are close
const maDiff = fastMA - slowMA;
const maDiffPct = (maDiff / slowMA) * 100;
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(` Price: $${currentPrice.toFixed(2)}`);
logger.info(` Fast MA (${this.FAST_PERIOD}): $${fastMA.toFixed(2)}`);
logger.info(` Slow MA (${this.SLOW_PERIOD}): $${slowMA.toFixed(2)}`);
logger.info(` MA Diff: ${maDiff.toFixed(2)} (${maDiffPct.toFixed(2)}%)`);
logger.info(` Position: ${currentPosition} shares`);
logger.info(` Bars since last trade: ${barsSinceLastTrade}`);
if (goldenCross) {
logger.info(` 🟢 GOLDEN CROSS DETECTED!`);
}
if (deathCross) {
logger.info(` 🔴 DEATH CROSS DETECTED!`);
}
}
if (barsSinceLastTrade < this.MIN_HOLDING_BARS && lastTradeBar > 0) {
return null; // Too soon to trade again
}
if (goldenCross) {
logger.info(`🟢 Golden cross detected for ${symbol} @ ${timestamp}`);
logger.info(` Price: ${currentPrice.toFixed(2)}, Fast MA: ${fastMA.toFixed(2)} > Slow MA: ${slowMA.toFixed(2)}`);
logger.info(` Prev Fast MA: ${prevFastMA.toFixed(2)} <= Prev Slow MA: ${prevSlowMA.toFixed(2)}`);
// Calculate position size
const positionSize = this.calculatePositionSize(currentPrice);
logger.info(` Position size: ${positionSize} shares`);
const signal: Signal = {
type: 'buy',
symbol,
strength: 0.8,
reason: 'Golden cross - Fast MA crossed above Slow MA',
metadata: {
fastMA,
slowMA,
prevFastMA,
prevSlowMA,
crossoverType: 'golden',
price: currentPrice,
quantity: positionSize
}
};
// Track signal time
this.lastSignalTime.set(symbol, Date.now());
this.totalSignals++;
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
return signal;
} else if (deathCross && currentPosition > 0) {
// Death cross - sell signal
logger.info(`🔴 Death cross detected for ${symbol} @ ${timestamp}`);
logger.info(` Price: ${currentPrice.toFixed(2)}, Fast MA: ${fastMA.toFixed(2)} < Slow MA: ${slowMA.toFixed(2)}`);
logger.info(` Prev Fast MA: ${prevFastMA.toFixed(2)} >= Prev Slow MA: ${prevSlowMA.toFixed(2)}`);
logger.info(` Current position: ${currentPosition} shares`);
const signal: Signal = {
type: 'sell',
symbol,
strength: 0.8,
reason: 'Death cross - Fast MA crossed below Slow MA',
metadata: {
fastMA,
slowMA,
prevFastMA,
prevSlowMA,
crossoverType: 'death',
price: currentPrice,
quantity: currentPosition
}
};
// For golden cross, we want to be long
// If we're short, we need to close the short first
if (currentPosition < 0) {
logger.info(` Closing short position of ${Math.abs(currentPosition)} shares`);
const signal: Signal = {
type: 'buy',
symbol,
strength: 0.8,
reason: 'Golden cross - Closing short position',
metadata: {
fastMA,
slowMA,
prevFastMA,
prevSlowMA,
crossoverType: 'golden',
price: currentPrice,
quantity: Math.abs(currentPosition) // Buy to close short
}
};
this.lastTradeTime.set(symbol, history.length);
this.totalSignals++;
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
return signal;
} else if (currentPosition === 0) {
// No position, open long
logger.info(` Price: ${currentPrice.toFixed(2)}, Fast MA: ${fastMA.toFixed(2)} > Slow MA: ${slowMA.toFixed(2)}`);
logger.info(` Prev Fast MA: ${prevFastMA.toFixed(2)} <= Prev Slow MA: ${prevSlowMA.toFixed(2)}`);
// Calculate position size
const positionSize = this.calculatePositionSize(currentPrice);
logger.info(` Opening long position: ${positionSize} shares`);
const signal: Signal = {
type: 'buy',
symbol,
strength: 0.8,
reason: 'Golden cross - Fast MA crossed above Slow MA',
metadata: {
fastMA,
slowMA,
prevFastMA,
prevSlowMA,
crossoverType: 'golden',
price: currentPrice,
quantity: positionSize
}
};
this.lastTradeTime.set(symbol, history.length);
this.totalSignals++;
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
return signal;
} else {
logger.info(` ⚠️ Already long, skipping buy signal`);
}
} else if (deathCross) {
logger.info(`🔴 Death cross detected for ${symbol} @ ${timestamp}`);
logger.info(` Current position: ${currentPosition} shares`);
// Track signal time
this.lastSignalTime.set(symbol, Date.now());
this.totalSignals++;
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
return signal;
// For death cross, we want to be flat or short
if (currentPosition > 0) {
// Close long position
logger.info(` Closing long position of ${currentPosition} shares`);
logger.info(` Price: ${currentPrice.toFixed(2)}, Fast MA: ${fastMA.toFixed(2)} < Slow MA: ${slowMA.toFixed(2)}`);
logger.info(` Prev Fast MA: ${prevFastMA.toFixed(2)} >= Prev Slow MA: ${prevSlowMA.toFixed(2)}`);
const signal: Signal = {
type: 'sell',
symbol,
strength: 0.8,
reason: 'Death cross - Fast MA crossed below Slow MA',
metadata: {
fastMA,
slowMA,
prevFastMA,
prevSlowMA,
crossoverType: 'death',
price: currentPrice,
quantity: currentPosition
}
};
this.lastTradeTime.set(symbol, history.length);
this.totalSignals++;
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
return signal;
} else if (currentPosition === 0) {
// Optional: Open short position (comment out if long-only)
logger.info(` No position, staying flat (long-only strategy)`);
// Uncomment below for long/short strategy:
/*
const positionSize = this.calculatePositionSize(currentPrice);
logger.info(` Opening short position: ${positionSize} shares`);
const signal: Signal = {
type: 'sell',
symbol,
strength: 0.8,
reason: 'Death cross - Opening short position',
metadata: {
fastMA,
slowMA,
prevFastMA,
prevSlowMA,
crossoverType: 'death',
price: currentPrice,
quantity: positionSize
}
};
this.lastTradeTime.set(symbol, history.length);
this.totalSignals++;
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
return signal;
*/
} else {
logger.info(` ⚠️ Already short, skipping sell signal`);
}
}
// Log near-crossover conditions
@ -207,24 +292,21 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
protected onOrderFilled(fill: any): void {
const { symbol, side, quantity, price } = fill;
const currentPosition = this.positions.get(symbol) || 0;
logger.info(`${side.toUpperCase()} filled: ${symbol} - ${quantity} shares @ ${price}`);
if (side === 'buy') {
this.positions.set(symbol, currentPosition + quantity);
logger.info(`✅ BUY filled: ${symbol} - ${quantity} shares @ ${price}`);
} else {
this.positions.set(symbol, Math.max(0, currentPosition - quantity));
logger.info(`✅ SELL filled: ${symbol} - ${quantity} shares @ ${price}`);
}
logger.info(`Position updated for ${symbol}: ${this.positions.get(symbol)} shares`);
// Position tracking is handled by BaseStrategy.onOrderUpdate
// Just log the current position
const currentPosition = this.getPosition(symbol);
logger.info(`Position for ${symbol}: ${currentPosition} shares`);
}
// Override to provide custom order generation
protected async signalToOrder(signal: Signal): Promise<OrderRequest | null> {
logger.info(`🔄 Converting signal to order:`, signal);
// Get position sizing from metadata or calculate
const currentPosition = this.getPosition(signal.symbol);
// Get position sizing from metadata
const quantity = signal.metadata?.quantity || 100;
if (signal.type === 'buy') {