fixed test strat

This commit is contained in:
Boki 2025-07-03 18:33:15 -04:00
parent 6cf3179092
commit 8a9a4bc336
12 changed files with 1301 additions and 8 deletions

View file

@ -89,7 +89,7 @@ export class StrategyManager extends EventEmitter {
case 'smacrossover':
case 'sma-crossover':
case 'moving-average': {
const { SimpleMovingAverageCrossover } = await import('./examples/SimpleMovingAverageCrossover');
const { SimpleMovingAverageCrossover } = await import('./examples/SimpleMovingAverageCrossoverFixed');
strategy = new SimpleMovingAverageCrossover(
config,
this.container.custom?.ModeManager,

View file

@ -110,9 +110,8 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
}
}
if (barsSinceLastTrade < this.MIN_HOLDING_BARS && lastTradeBar > 0) {
return null; // Too soon to trade again
}
// Note: We removed the MIN_HOLDING_BARS check here to allow closing and opening on same bar
// The check is now done individually for new position entries
if (goldenCross) {
logger.info(`🟢 Golden cross detected for ${symbol} @ ${timestamp}`);
@ -144,6 +143,7 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
return signal;
} else if (currentPosition === 0) {
// No position, open long
logger.info(` No current position, opening 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)}`);
@ -207,6 +207,8 @@ export class SimpleMovingAverageCrossover extends BaseStrategy {
return signal;
} else if (currentPosition === 0) {
// Open short position for long/short strategy
logger.info(` No current position, opening short`);
const positionSize = this.calculatePositionSize(currentPrice);
logger.info(` Opening short position: ${positionSize} shares`);

View file

@ -0,0 +1,305 @@
import { BaseStrategy, Signal } from '../BaseStrategy';
import { MarketData } from '../../types';
import { getLogger } from '@stock-bot/logger';
const logger = getLogger('SimpleMovingAverageCrossover');
export class SimpleMovingAverageCrossover extends BaseStrategy {
private priceHistory = new Map<string, number[]>();
private lastSignalBar = new Map<string, number>();
private barCount = new Map<string, number>();
private totalSignals = 0;
private lastCrossoverType = new Map<string, 'golden' | 'death' | null>();
// Strategy parameters
private readonly FAST_PERIOD = 5; // Changed from 10 to generate more signals
private readonly SLOW_PERIOD = 15; // Changed from 20 to generate more signals
private readonly POSITION_SIZE = 0.1; // 10% of capital per position
private readonly MIN_HOLDING_BARS = 3; // Minimum bars to hold position
private readonly DEBUG_INTERVAL = 10; // Log every N bars for debugging
constructor(config: any, modeManager?: any, executionService?: any) {
super(config, modeManager, executionService);
logger.info(`SimpleMovingAverageCrossover initialized with Fast=${this.FAST_PERIOD}, Slow=${this.SLOW_PERIOD}`);
}
protected updateIndicators(data: MarketData): void {
if (data.type !== 'bar') return;
const symbol = data.data.symbol;
const price = data.data.close;
// Initialize or get price history
if (!this.priceHistory.has(symbol)) {
this.priceHistory.set(symbol, []);
this.barCount.set(symbol, 0);
logger.info(`📊 Starting to track ${symbol} @ ${price.toFixed(2)}`);
}
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();
}
// Log when we have enough data to start trading
if (history.length === this.SLOW_PERIOD) {
logger.info(`${symbol} now has enough history (${history.length} bars) to start trading`);
}
}
protected async generateSignal(data: MarketData): Promise<Signal | null> {
if (data.type !== 'bar') return null;
const symbol = data.data.symbol;
const history = this.priceHistory.get(symbol);
if (!history || history.length < this.SLOW_PERIOD) {
return null;
}
const currentPrice = data.data.close;
const timestamp = data.data.timestamp;
const currentBar = this.barCount.get(symbol) || 0;
// Calculate moving averages
const fastMA = this.calculateSMA(history, this.FAST_PERIOD);
const slowMA = this.calculateSMA(history, this.SLOW_PERIOD);
// Get previous MAs for crossover detection
const historyWithoutLast = history.slice(0, -1);
const prevFastMA = this.calculateSMA(historyWithoutLast, this.FAST_PERIOD);
const prevSlowMA = this.calculateSMA(historyWithoutLast, this.SLOW_PERIOD);
// Detect crossovers
const goldenCross = prevFastMA <= prevSlowMA && fastMA > slowMA;
const deathCross = prevFastMA >= prevSlowMA && fastMA < slowMA;
// Get current position
const currentPosition = this.getPosition(symbol);
// Track last signal bar
const lastSignalBar = this.lastSignalBar.get(symbol) || 0;
const barsSinceLastSignal = currentBar - lastSignalBar;
// Calculate MA difference for debugging
const maDiff = fastMA - slowMA;
const maDiffPct = (maDiff / slowMA) * 100;
// Debug logging every N bars
if (currentBar % this.DEBUG_INTERVAL === 0 || goldenCross || deathCross) {
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)}`);
logger.info(` MA Diff: ${maDiff.toFixed(2)} (${maDiffPct.toFixed(2)}%)`);
logger.info(` Position: ${currentPosition} shares`);
logger.info(` Bars since last signal: ${barsSinceLastSignal}`);
if (goldenCross) {
logger.info(` 🟢 GOLDEN CROSS DETECTED!`);
}
if (deathCross) {
logger.info(` 🔴 DEATH CROSS DETECTED!`);
}
}
// Store crossover type for position management
if (goldenCross) {
this.lastCrossoverType.set(symbol, 'golden');
} else if (deathCross) {
this.lastCrossoverType.set(symbol, 'death');
}
// Check if we should enter a position based on the current trend
const lastCrossover = this.lastCrossoverType.get(symbol);
if (currentPosition === 0 && barsSinceLastSignal >= this.MIN_HOLDING_BARS) {
// No position - check if we should enter based on last crossover
if (lastCrossover === 'golden' && fastMA > slowMA) {
// Trend is still bullish after golden cross - open long
logger.info(`🟢 Opening LONG position - Bullish trend after golden cross`);
logger.info(` Current position: 0 shares`);
const positionSize = this.calculatePositionSize(currentPrice);
logger.info(` Opening long position: ${positionSize} shares`);
const signal: Signal = {
type: 'buy',
symbol,
strength: 0.8,
reason: 'Bullish trend - Opening long position',
metadata: {
fastMA,
slowMA,
prevFastMA,
prevSlowMA,
crossoverType: 'trend_long',
price: currentPrice,
quantity: positionSize
}
};
this.lastSignalBar.set(symbol, currentBar);
this.totalSignals++;
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
return signal;
} else if (lastCrossover === 'death' && fastMA < slowMA) {
// Trend is still bearish after death cross - open short
logger.info(`🔴 Opening SHORT position - Bearish trend after death cross`);
logger.info(` Current position: 0 shares`);
const positionSize = this.calculatePositionSize(currentPrice);
logger.info(` Opening short position: ${positionSize} shares`);
const signal: Signal = {
type: 'sell',
symbol,
strength: 0.8,
reason: 'Bearish trend - Opening short position',
metadata: {
fastMA,
slowMA,
prevFastMA,
prevSlowMA,
crossoverType: 'trend_short',
price: currentPrice,
quantity: positionSize
}
};
this.lastSignalBar.set(symbol, currentBar);
this.totalSignals++;
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
return signal;
}
}
// Handle crossovers for position changes
if (goldenCross) {
logger.info(`🟢 Golden cross detected for ${symbol} @ ${timestamp}`);
logger.info(` Current position: ${currentPosition} shares`);
if (currentPosition < 0) {
// Close short position
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)
}
};
this.lastSignalBar.set(symbol, currentBar);
this.totalSignals++;
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
return signal;
}
} else if (deathCross) {
logger.info(`🔴 Death cross detected for ${symbol} @ ${timestamp}`);
logger.info(` Current position: ${currentPosition} shares`);
if (currentPosition > 0) {
// Close long position
logger.info(` Closing long position of ${currentPosition} shares`);
const signal: Signal = {
type: 'sell',
symbol,
strength: 0.8,
reason: 'Death cross - Closing long position',
metadata: {
fastMA,
slowMA,
prevFastMA,
prevSlowMA,
crossoverType: 'death',
price: currentPrice,
quantity: currentPosition
}
};
this.lastSignalBar.set(symbol, currentBar);
this.totalSignals++;
logger.info(`👉 Total signals generated: ${this.totalSignals}`);
return signal;
}
}
return null;
}
private calculateSMA(prices: number[], period: number): number {
if (prices.length < period) {
logger.warn(`Not enough data for SMA calculation: ${prices.length} < ${period}`);
return 0;
}
const slice = prices.slice(-period);
const sum = slice.reduce((a, b) => a + b, 0);
const sma = sum / period;
// Sanity check
if (isNaN(sma) || !isFinite(sma)) {
logger.error(`Invalid SMA calculation: sum=${sum}, period=${period}, prices=${slice.length}`);
return 0;
}
return sma;
}
private calculatePositionSize(price: number): number {
// Get account balance from trading engine
const tradingEngine = this.modeManager?.getTradingEngine();
if (!tradingEngine) {
logger.warn('No trading engine available, using default position size');
return 100;
}
// Try to get account balance from trading engine
let accountBalance = 100000; // Default
try {
if (tradingEngine.getAccountBalance) {
accountBalance = tradingEngine.getAccountBalance();
} else if (tradingEngine.getTotalPnl) {
const [realized, unrealized] = tradingEngine.getTotalPnl();
accountBalance = 100000 + realized + unrealized; // Assuming 100k initial
}
} catch (error) {
logger.warn('Could not get account balance:', error);
}
const positionValue = accountBalance * this.POSITION_SIZE;
const shares = Math.floor(positionValue / price);
logger.debug(`Position sizing: Balance=$${accountBalance}, Position Size=${this.POSITION_SIZE}, Price=$${price}, Shares=${shares}`);
return Math.max(1, shares); // At least 1 share
}
protected onOrderFilled(fill: any): void {
const { symbol, side, quantity, price } = fill;
logger.info(`${side.toUpperCase()} filled: ${symbol} - ${quantity} shares @ ${price}`);
// Update our internal position tracking to match
// This is redundant with BaseStrategy but helps with debugging
const currentPosition = this.getPosition(symbol);
logger.info(` Position after fill: ${currentPosition} shares`);
}
}