backtest work
This commit is contained in:
parent
143e2e1678
commit
55b4ca78c9
6 changed files with 427 additions and 129 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue