fixed test strat
This commit is contained in:
parent
6cf3179092
commit
8a9a4bc336
12 changed files with 1301 additions and 8 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue