moved indicators to rust
This commit is contained in:
parent
c106a719e8
commit
6df32dc18b
27 changed files with 6113 additions and 1 deletions
305
apps/stock/orchestrator/src/indicators/TechnicalAnalysis.ts
Normal file
305
apps/stock/orchestrator/src/indicators/TechnicalAnalysis.ts
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
import { TechnicalIndicators, IncrementalSMA, IncrementalEMA, IncrementalRSI, MacdResult, BollingerBandsResult, StochasticResult } from '@stock-bot/core';
|
||||
|
||||
/**
|
||||
* Wrapper class for the Rust TA library with TypeScript-friendly interfaces
|
||||
*/
|
||||
export class TechnicalAnalysis {
|
||||
private indicators: TechnicalIndicators;
|
||||
|
||||
constructor() {
|
||||
this.indicators = new TechnicalIndicators();
|
||||
}
|
||||
|
||||
// Simple indicators
|
||||
sma(values: number[], period: number): number[] {
|
||||
return this.indicators.calculateSma(values, period);
|
||||
}
|
||||
|
||||
ema(values: number[], period: number): number[] {
|
||||
return this.indicators.calculateEma(values, period);
|
||||
}
|
||||
|
||||
rsi(values: number[], period: number): number[] {
|
||||
return this.indicators.calculateRsi(values, period);
|
||||
}
|
||||
|
||||
atr(high: number[], low: number[], close: number[], period: number): number[] {
|
||||
return this.indicators.calculateAtr(high, low, close, period);
|
||||
}
|
||||
|
||||
// Complex indicators with parsed results
|
||||
macd(values: number[], fastPeriod = 12, slowPeriod = 26, signalPeriod = 9): MacdResult {
|
||||
const result = this.indicators.calculateMacd(values, fastPeriod, slowPeriod, signalPeriod);
|
||||
return JSON.parse(result);
|
||||
}
|
||||
|
||||
bollingerBands(values: number[], period = 20, stdDev = 2): BollingerBandsResult {
|
||||
const result = this.indicators.calculateBollingerBands(values, period, stdDev);
|
||||
return JSON.parse(result);
|
||||
}
|
||||
|
||||
stochastic(
|
||||
high: number[],
|
||||
low: number[],
|
||||
close: number[],
|
||||
kPeriod = 14,
|
||||
dPeriod = 3,
|
||||
smoothK = 1
|
||||
): StochasticResult {
|
||||
const result = this.indicators.calculateStochastic(high, low, close, kPeriod, dPeriod, smoothK);
|
||||
return JSON.parse(result);
|
||||
}
|
||||
|
||||
// Helper to get the latest value from an indicator array
|
||||
static latest(values: number[]): number | undefined {
|
||||
return values[values.length - 1];
|
||||
}
|
||||
|
||||
// Helper to check for crossovers
|
||||
static crossover(series1: number[], series2: number[]): boolean {
|
||||
if (series1.length < 2 || series2.length < 2) return false;
|
||||
const prev1 = series1[series1.length - 2];
|
||||
const curr1 = series1[series1.length - 1];
|
||||
const prev2 = series2[series2.length - 2];
|
||||
const curr2 = series2[series2.length - 1];
|
||||
return prev1 <= prev2 && curr1 > curr2;
|
||||
}
|
||||
|
||||
static crossunder(series1: number[], series2: number[]): boolean {
|
||||
if (series1.length < 2 || series2.length < 2) return false;
|
||||
const prev1 = series1[series1.length - 2];
|
||||
const curr1 = series1[series1.length - 1];
|
||||
const prev2 = series2[series2.length - 2];
|
||||
const curr2 = series2[series2.length - 1];
|
||||
return prev1 >= prev2 && curr1 < curr2;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Incremental indicator manager for streaming data
|
||||
*/
|
||||
export class IncrementalIndicators {
|
||||
private indicators: Map<string, IncrementalSMA | IncrementalEMA | IncrementalRSI> = new Map();
|
||||
|
||||
createSMA(key: string, period: number): IncrementalSMA {
|
||||
const indicator = new IncrementalSMA(period);
|
||||
this.indicators.set(key, indicator);
|
||||
return indicator;
|
||||
}
|
||||
|
||||
createEMA(key: string, period: number): IncrementalEMA {
|
||||
const indicator = new IncrementalEMA(period);
|
||||
this.indicators.set(key, indicator);
|
||||
return indicator;
|
||||
}
|
||||
|
||||
createRSI(key: string, period: number): IncrementalRSI {
|
||||
const indicator = new IncrementalRSI(period);
|
||||
this.indicators.set(key, indicator);
|
||||
return indicator;
|
||||
}
|
||||
|
||||
get(key: string): IncrementalSMA | IncrementalEMA | IncrementalRSI | undefined {
|
||||
return this.indicators.get(key);
|
||||
}
|
||||
|
||||
update(key: string, value: number): number | null {
|
||||
const indicator = this.indicators.get(key);
|
||||
if (!indicator) {
|
||||
throw new Error(`Indicator ${key} not found`);
|
||||
}
|
||||
return indicator.update(value);
|
||||
}
|
||||
|
||||
current(key: string): number | null {
|
||||
const indicator = this.indicators.get(key);
|
||||
if (!indicator) {
|
||||
throw new Error(`Indicator ${key} not found`);
|
||||
}
|
||||
return indicator.current();
|
||||
}
|
||||
|
||||
reset(key: string): void {
|
||||
const indicator = this.indicators.get(key);
|
||||
if (indicator && 'reset' in indicator) {
|
||||
indicator.reset();
|
||||
}
|
||||
}
|
||||
|
||||
resetAll(): void {
|
||||
this.indicators.forEach(indicator => {
|
||||
if ('reset' in indicator) {
|
||||
indicator.reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Signal generator using technical indicators
|
||||
*/
|
||||
export interface TradingSignal {
|
||||
symbol: string;
|
||||
timestamp: number;
|
||||
action: 'BUY' | 'SELL' | 'HOLD';
|
||||
strength: number; // 0-1
|
||||
indicators: Record<string, number>;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export class SignalGenerator {
|
||||
private ta: TechnicalAnalysis;
|
||||
|
||||
constructor() {
|
||||
this.ta = new TechnicalAnalysis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate signals based on multiple indicators
|
||||
*/
|
||||
generateSignals(
|
||||
symbol: string,
|
||||
prices: {
|
||||
close: number[];
|
||||
high: number[];
|
||||
low: number[];
|
||||
volume: number[];
|
||||
},
|
||||
timestamp: number
|
||||
): TradingSignal {
|
||||
const indicators: Record<string, number> = {};
|
||||
let buySignals = 0;
|
||||
let sellSignals = 0;
|
||||
let totalWeight = 0;
|
||||
const reasons: string[] = [];
|
||||
|
||||
// RSI signals
|
||||
if (prices.close.length >= 14) {
|
||||
const rsi = this.ta.rsi(prices.close, 14);
|
||||
const currentRsi = TechnicalAnalysis.latest(rsi);
|
||||
if (currentRsi !== undefined) {
|
||||
indicators.rsi = currentRsi;
|
||||
if (currentRsi < 30) {
|
||||
buySignals += 2;
|
||||
totalWeight += 2;
|
||||
reasons.push('RSI oversold');
|
||||
} else if (currentRsi > 70) {
|
||||
sellSignals += 2;
|
||||
totalWeight += 2;
|
||||
reasons.push('RSI overbought');
|
||||
} else {
|
||||
totalWeight += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MACD signals
|
||||
if (prices.close.length >= 26) {
|
||||
const macd = this.ta.macd(prices.close);
|
||||
const currentMacd = TechnicalAnalysis.latest(macd.macd);
|
||||
const currentSignal = TechnicalAnalysis.latest(macd.signal);
|
||||
const currentHistogram = TechnicalAnalysis.latest(macd.histogram);
|
||||
|
||||
if (currentMacd !== undefined && currentSignal !== undefined) {
|
||||
indicators.macd = currentMacd;
|
||||
indicators.macdSignal = currentSignal;
|
||||
indicators.macdHistogram = currentHistogram || 0;
|
||||
|
||||
if (TechnicalAnalysis.crossover(macd.macd, macd.signal)) {
|
||||
buySignals += 3;
|
||||
totalWeight += 3;
|
||||
reasons.push('MACD bullish crossover');
|
||||
} else if (TechnicalAnalysis.crossunder(macd.macd, macd.signal)) {
|
||||
sellSignals += 3;
|
||||
totalWeight += 3;
|
||||
reasons.push('MACD bearish crossover');
|
||||
} else {
|
||||
totalWeight += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bollinger Bands signals
|
||||
if (prices.close.length >= 20) {
|
||||
const bb = this.ta.bollingerBands(prices.close, 20, 2);
|
||||
const currentPrice = prices.close[prices.close.length - 1];
|
||||
const currentUpper = TechnicalAnalysis.latest(bb.upper);
|
||||
const currentLower = TechnicalAnalysis.latest(bb.lower);
|
||||
const currentMiddle = TechnicalAnalysis.latest(bb.middle);
|
||||
|
||||
if (currentUpper && currentLower && currentMiddle) {
|
||||
indicators.bbUpper = currentUpper;
|
||||
indicators.bbLower = currentLower;
|
||||
indicators.bbMiddle = currentMiddle;
|
||||
|
||||
const bbPercent = (currentPrice - currentLower) / (currentUpper - currentLower);
|
||||
indicators.bbPercent = bbPercent;
|
||||
|
||||
if (bbPercent < 0.2) {
|
||||
buySignals += 2;
|
||||
totalWeight += 2;
|
||||
reasons.push('Near lower Bollinger Band');
|
||||
} else if (bbPercent > 0.8) {
|
||||
sellSignals += 2;
|
||||
totalWeight += 2;
|
||||
reasons.push('Near upper Bollinger Band');
|
||||
} else {
|
||||
totalWeight += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stochastic signals
|
||||
if (prices.high.length >= 14 && prices.low.length >= 14 && prices.close.length >= 14) {
|
||||
const stoch = this.ta.stochastic(prices.high, prices.low, prices.close, 14, 3, 3);
|
||||
const currentK = TechnicalAnalysis.latest(stoch.k);
|
||||
const currentD = TechnicalAnalysis.latest(stoch.d);
|
||||
|
||||
if (currentK !== undefined && currentD !== undefined) {
|
||||
indicators.stochK = currentK;
|
||||
indicators.stochD = currentD;
|
||||
|
||||
if (currentK < 20 && currentD < 20) {
|
||||
buySignals += 1;
|
||||
totalWeight += 1;
|
||||
reasons.push('Stochastic oversold');
|
||||
} else if (currentK > 80 && currentD > 80) {
|
||||
sellSignals += 1;
|
||||
totalWeight += 1;
|
||||
reasons.push('Stochastic overbought');
|
||||
} else {
|
||||
totalWeight += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine overall signal
|
||||
let action: 'BUY' | 'SELL' | 'HOLD' = 'HOLD';
|
||||
let strength = 0;
|
||||
|
||||
if (totalWeight > 0) {
|
||||
const buyStrength = buySignals / totalWeight;
|
||||
const sellStrength = sellSignals / totalWeight;
|
||||
|
||||
if (buyStrength > 0.5) {
|
||||
action = 'BUY';
|
||||
strength = buyStrength;
|
||||
} else if (sellStrength > 0.5) {
|
||||
action = 'SELL';
|
||||
strength = sellStrength;
|
||||
} else {
|
||||
action = 'HOLD';
|
||||
strength = Math.max(buyStrength, sellStrength);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
symbol,
|
||||
timestamp,
|
||||
action,
|
||||
strength,
|
||||
indicators,
|
||||
reason: reasons.join('; ') || 'No clear signal'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,551 @@
|
|||
import { BaseStrategy, Signal } from '../BaseStrategy';
|
||||
import { MarketData } from '../../types';
|
||||
import { IndicatorManager } from '../indicators/IndicatorManager';
|
||||
import { PositionManager } from '../position/PositionManager';
|
||||
import { RiskManager } from '../risk/RiskManager';
|
||||
import { SignalManager, CommonRules, CommonFilters } from '../signals/SignalManager';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
|
||||
const logger = getLogger('AdvancedMultiIndicatorStrategy');
|
||||
|
||||
export interface AdvancedStrategyConfig {
|
||||
// Indicator settings
|
||||
fastMA?: number;
|
||||
slowMA?: number;
|
||||
rsiPeriod?: number;
|
||||
atrPeriod?: number;
|
||||
|
||||
// Risk settings
|
||||
riskPerTrade?: number;
|
||||
maxPositions?: number;
|
||||
maxDrawdown?: number;
|
||||
useATRStops?: boolean;
|
||||
atrMultiplier?: number;
|
||||
|
||||
// Signal settings
|
||||
signalAggregation?: 'weighted' | 'majority' | 'unanimous' | 'threshold';
|
||||
minSignalStrength?: number;
|
||||
minSignalConfidence?: number;
|
||||
|
||||
// Position sizing
|
||||
positionSizing?: 'fixed' | 'risk' | 'kelly' | 'volatility';
|
||||
|
||||
// Other
|
||||
debugMode?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advanced strategy using multiple indicators, risk management, and signal aggregation
|
||||
*/
|
||||
export class AdvancedMultiIndicatorStrategy extends BaseStrategy {
|
||||
private indicatorManager: IndicatorManager;
|
||||
private positionManager: PositionManager;
|
||||
private riskManager: RiskManager;
|
||||
private signalManager: SignalManager;
|
||||
|
||||
private config: Required<AdvancedStrategyConfig>;
|
||||
private stopLosses: Map<string, number> = new Map();
|
||||
private takeProfits: Map<string, number> = new Map();
|
||||
|
||||
constructor(strategyConfig: any, modeManager?: any, executionService?: any) {
|
||||
super(strategyConfig, modeManager, executionService);
|
||||
|
||||
// Initialize config
|
||||
this.config = {
|
||||
fastMA: 20,
|
||||
slowMA: 50,
|
||||
rsiPeriod: 14,
|
||||
atrPeriod: 14,
|
||||
riskPerTrade: 0.02,
|
||||
maxPositions: 5,
|
||||
maxDrawdown: 0.2,
|
||||
useATRStops: true,
|
||||
atrMultiplier: 2,
|
||||
signalAggregation: 'weighted',
|
||||
minSignalStrength: 0.5,
|
||||
minSignalConfidence: 0.3,
|
||||
positionSizing: 'risk',
|
||||
debugMode: false,
|
||||
...strategyConfig.params
|
||||
};
|
||||
|
||||
// Initialize managers
|
||||
const initialCapital = strategyConfig.initialCapital || 100000;
|
||||
this.indicatorManager = new IndicatorManager();
|
||||
this.positionManager = new PositionManager(initialCapital);
|
||||
this.riskManager = new RiskManager(initialCapital, {
|
||||
maxPositions: this.config.maxPositions,
|
||||
maxDrawdownPct: this.config.maxDrawdown,
|
||||
maxPositionSizePct: 0.1
|
||||
});
|
||||
|
||||
this.signalManager = new SignalManager({
|
||||
method: this.config.signalAggregation
|
||||
});
|
||||
|
||||
// Setup signal rules
|
||||
this.setupSignalRules();
|
||||
|
||||
logger.info('AdvancedMultiIndicatorStrategy initialized:', this.config);
|
||||
}
|
||||
|
||||
private setupSignalRules(): void {
|
||||
// Moving average rules
|
||||
this.signalManager.addRule({
|
||||
name: `MA Crossover (${this.config.fastMA}/${this.config.slowMA})`,
|
||||
condition: (indicators) => {
|
||||
const fast = indicators[`sma${this.config.fastMA}`];
|
||||
const slow = indicators[`sma${this.config.slowMA}`];
|
||||
const prevFast = indicators[`sma${this.config.fastMA}_prev`];
|
||||
const prevSlow = indicators[`sma${this.config.slowMA}_prev`];
|
||||
|
||||
if (!fast || !slow || !prevFast || !prevSlow) return false;
|
||||
|
||||
// Check for crossover
|
||||
const crossover = prevFast <= prevSlow && fast > slow;
|
||||
const crossunder = prevFast >= prevSlow && fast < slow;
|
||||
|
||||
indicators._maCrossDirection = crossover ? 'up' : crossunder ? 'down' : null;
|
||||
return crossover || crossunder;
|
||||
},
|
||||
weight: 1,
|
||||
direction: 'both'
|
||||
});
|
||||
|
||||
// RSI rules
|
||||
this.signalManager.addRules([
|
||||
CommonRules.rsiOversold(30),
|
||||
CommonRules.rsiOverbought(70)
|
||||
]);
|
||||
|
||||
// MACD rules
|
||||
this.signalManager.addRules([
|
||||
CommonRules.macdBullishCross(),
|
||||
CommonRules.macdBearishCross()
|
||||
]);
|
||||
|
||||
// Bollinger Band rules
|
||||
this.signalManager.addRules([
|
||||
CommonRules.priceAtLowerBand(),
|
||||
CommonRules.priceAtUpperBand(),
|
||||
CommonRules.bollingerSqueeze()
|
||||
]);
|
||||
|
||||
// Add filters
|
||||
this.signalManager.addFilter(CommonFilters.minStrength(this.config.minSignalStrength));
|
||||
this.signalManager.addFilter(CommonFilters.minConfidence(this.config.minSignalConfidence));
|
||||
this.signalManager.addFilter(CommonFilters.trendAlignment('sma200'));
|
||||
}
|
||||
|
||||
protected updateIndicators(data: MarketData): void {
|
||||
if (data.type !== 'bar') return;
|
||||
|
||||
const { symbol, timestamp, open, high, low, close, volume } = data.data;
|
||||
|
||||
// First time setup for symbol
|
||||
if (!this.indicatorManager.getHistoryLength(symbol)) {
|
||||
this.setupSymbolIndicators(symbol);
|
||||
}
|
||||
|
||||
// Update price history
|
||||
this.indicatorManager.updatePrice({
|
||||
symbol,
|
||||
timestamp,
|
||||
open,
|
||||
high,
|
||||
low,
|
||||
close,
|
||||
volume
|
||||
});
|
||||
|
||||
// Update position market values
|
||||
this.updatePositionValues(symbol, close);
|
||||
|
||||
// Check stop losses and take profits
|
||||
this.checkExitConditions(symbol, close);
|
||||
}
|
||||
|
||||
private setupSymbolIndicators(symbol: string): void {
|
||||
// Setup incremental indicators
|
||||
this.indicatorManager.setupIncrementalIndicator(symbol, 'fast_sma', {
|
||||
type: 'sma',
|
||||
period: this.config.fastMA
|
||||
});
|
||||
|
||||
this.indicatorManager.setupIncrementalIndicator(symbol, 'slow_sma', {
|
||||
type: 'sma',
|
||||
period: this.config.slowMA
|
||||
});
|
||||
|
||||
this.indicatorManager.setupIncrementalIndicator(symbol, 'rsi', {
|
||||
type: 'rsi',
|
||||
period: this.config.rsiPeriod
|
||||
});
|
||||
|
||||
logger.info(`Initialized indicators for ${symbol}`);
|
||||
}
|
||||
|
||||
protected async generateSignal(data: MarketData): Promise<Signal | null> {
|
||||
if (data.type !== 'bar') return null;
|
||||
|
||||
const { symbol, timestamp, close } = data.data;
|
||||
const historyLength = this.indicatorManager.getHistoryLength(symbol);
|
||||
|
||||
// Need enough data
|
||||
if (historyLength < Math.max(this.config.slowMA, 26)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Prepare indicators for signal generation
|
||||
const indicators = this.prepareIndicators(symbol, close);
|
||||
if (!indicators) return null;
|
||||
|
||||
// Check risk before generating signals
|
||||
const currentPositions = this.getCurrentPositionMap();
|
||||
const riskCheck = this.riskManager.checkNewPosition(
|
||||
symbol,
|
||||
100, // Dummy size for check
|
||||
close,
|
||||
currentPositions
|
||||
);
|
||||
|
||||
if (!riskCheck.allowed && !this.positionManager.hasPosition(symbol)) {
|
||||
if (this.config.debugMode) {
|
||||
logger.warn(`Risk check failed for ${symbol}: ${riskCheck.reason}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Generate trading signal
|
||||
const tradingSignal = this.signalManager.generateSignal(
|
||||
symbol,
|
||||
timestamp,
|
||||
indicators,
|
||||
{ position: this.positionManager.getPositionQuantity(symbol) }
|
||||
);
|
||||
|
||||
if (!tradingSignal) return null;
|
||||
|
||||
// Log signal if in debug mode
|
||||
if (this.config.debugMode) {
|
||||
logger.info(`Signal generated for ${symbol}:`, {
|
||||
direction: tradingSignal.direction,
|
||||
strength: tradingSignal.strength.toFixed(2),
|
||||
confidence: tradingSignal.confidence.toFixed(2),
|
||||
rules: tradingSignal.rules
|
||||
});
|
||||
}
|
||||
|
||||
// Convert to strategy signal
|
||||
return this.convertToStrategySignal(tradingSignal, symbol, close);
|
||||
}
|
||||
|
||||
private prepareIndicators(symbol: string, currentPrice: number): Record<string, number> | null {
|
||||
const indicators: Record<string, number> = { price: currentPrice };
|
||||
|
||||
// Get moving averages
|
||||
const fastMA = this.indicatorManager.getSMA(symbol, this.config.fastMA);
|
||||
const slowMA = this.indicatorManager.getSMA(symbol, this.config.slowMA);
|
||||
const sma200 = this.indicatorManager.getSMA(symbol, 200);
|
||||
|
||||
if (!fastMA || !slowMA) return null;
|
||||
|
||||
// Current and previous values
|
||||
indicators[`sma${this.config.fastMA}`] = this.indicatorManager.getLatest(fastMA)!;
|
||||
indicators[`sma${this.config.slowMA}`] = this.indicatorManager.getLatest(slowMA)!;
|
||||
|
||||
if (fastMA.length >= 2 && slowMA.length >= 2) {
|
||||
indicators[`sma${this.config.fastMA}_prev`] = fastMA[fastMA.length - 2];
|
||||
indicators[`sma${this.config.slowMA}_prev`] = slowMA[slowMA.length - 2];
|
||||
}
|
||||
|
||||
if (sma200) {
|
||||
indicators.sma200 = this.indicatorManager.getLatest(sma200)!;
|
||||
}
|
||||
|
||||
// RSI
|
||||
const rsi = this.indicatorManager.getRSI(symbol, this.config.rsiPeriod);
|
||||
if (rsi) {
|
||||
indicators.rsi = this.indicatorManager.getLatest(rsi)!;
|
||||
}
|
||||
|
||||
// MACD
|
||||
const macd = this.indicatorManager.getMACD(symbol);
|
||||
if (macd) {
|
||||
indicators.macd = this.indicatorManager.getLatest(macd.macd)!;
|
||||
indicators.macd_signal = this.indicatorManager.getLatest(macd.signal)!;
|
||||
indicators.macd_histogram = this.indicatorManager.getLatest(macd.histogram)!;
|
||||
|
||||
if (macd.macd.length >= 2) {
|
||||
indicators.macd_prev = macd.macd[macd.macd.length - 2];
|
||||
indicators.macd_signal_prev = macd.signal[macd.signal.length - 2];
|
||||
}
|
||||
}
|
||||
|
||||
// Bollinger Bands
|
||||
const bb = this.indicatorManager.getBollingerBands(symbol);
|
||||
if (bb) {
|
||||
indicators.bb_upper = this.indicatorManager.getLatest(bb.upper)!;
|
||||
indicators.bb_middle = this.indicatorManager.getLatest(bb.middle)!;
|
||||
indicators.bb_lower = this.indicatorManager.getLatest(bb.lower)!;
|
||||
}
|
||||
|
||||
// ATR for volatility
|
||||
const atr = this.indicatorManager.getATR(symbol, this.config.atrPeriod);
|
||||
if (atr) {
|
||||
indicators.atr = this.indicatorManager.getLatest(atr)!;
|
||||
}
|
||||
|
||||
// Volume
|
||||
const priceHistory = this.indicatorManager.getPriceHistory(symbol);
|
||||
if (priceHistory) {
|
||||
indicators.volume = priceHistory.volume[priceHistory.volume.length - 1];
|
||||
const avgVolume = priceHistory.volume.slice(-20).reduce((a, b) => a + b, 0) / 20;
|
||||
indicators.avg_volume = avgVolume;
|
||||
}
|
||||
|
||||
return indicators;
|
||||
}
|
||||
|
||||
private convertToStrategySignal(
|
||||
tradingSignal: TradingSignal,
|
||||
symbol: string,
|
||||
currentPrice: number
|
||||
): Signal | null {
|
||||
const currentPosition = this.positionManager.getPositionQuantity(symbol);
|
||||
|
||||
// Determine action based on signal and current position
|
||||
let type: 'buy' | 'sell' | 'close';
|
||||
let quantity: number;
|
||||
|
||||
if (tradingSignal.direction === 'buy') {
|
||||
if (currentPosition < 0) {
|
||||
// Close short position
|
||||
type = 'buy';
|
||||
quantity = Math.abs(currentPosition);
|
||||
} else if (currentPosition === 0) {
|
||||
// Open long position
|
||||
type = 'buy';
|
||||
quantity = this.calculatePositionSize(symbol, currentPrice, tradingSignal);
|
||||
} else {
|
||||
// Already long
|
||||
return null;
|
||||
}
|
||||
} else if (tradingSignal.direction === 'sell') {
|
||||
if (currentPosition > 0) {
|
||||
// Close long position
|
||||
type = 'sell';
|
||||
quantity = currentPosition;
|
||||
} else if (currentPosition === 0 && false) { // Disable shorting for now
|
||||
// Open short position
|
||||
type = 'sell';
|
||||
quantity = this.calculatePositionSize(symbol, currentPrice, tradingSignal);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
type,
|
||||
symbol,
|
||||
strength: tradingSignal.strength,
|
||||
reason: tradingSignal.rules.join(', '),
|
||||
metadata: {
|
||||
...tradingSignal.indicators,
|
||||
quantity,
|
||||
confidence: tradingSignal.confidence,
|
||||
rules: tradingSignal.rules
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private calculatePositionSize(
|
||||
symbol: string,
|
||||
price: number,
|
||||
signal: TradingSignal
|
||||
): number {
|
||||
const accountBalance = this.positionManager.getAccountBalance();
|
||||
|
||||
switch (this.config.positionSizing) {
|
||||
case 'fixed':
|
||||
// Fixed percentage of account
|
||||
const fixedValue = accountBalance * this.config.riskPerTrade * 5;
|
||||
return Math.floor(fixedValue / price);
|
||||
|
||||
case 'risk':
|
||||
// Risk-based sizing with ATR stop
|
||||
const atr = signal.indicators.atr;
|
||||
if (atr && this.config.useATRStops) {
|
||||
const stopDistance = atr * this.config.atrMultiplier;
|
||||
return this.positionManager.calculatePositionSize({
|
||||
accountBalance,
|
||||
riskPerTrade: this.config.riskPerTrade,
|
||||
stopLossDistance: stopDistance
|
||||
}, price);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'kelly':
|
||||
// Kelly criterion based on historical performance
|
||||
const metrics = this.positionManager.getPerformanceMetrics();
|
||||
if (metrics.totalTrades >= 20) {
|
||||
return this.positionManager.calculateKellySize(
|
||||
metrics.winRate / 100,
|
||||
metrics.avgWin,
|
||||
metrics.avgLoss,
|
||||
price
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'volatility':
|
||||
// Volatility-adjusted sizing
|
||||
const atrVol = signal.indicators.atr;
|
||||
if (atrVol) {
|
||||
return this.positionManager.calculatePositionSize({
|
||||
accountBalance,
|
||||
riskPerTrade: this.config.riskPerTrade,
|
||||
volatilityAdjustment: true,
|
||||
atr: atrVol
|
||||
}, price);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Default sizing
|
||||
return this.positionManager.calculatePositionSize({
|
||||
accountBalance,
|
||||
riskPerTrade: this.config.riskPerTrade
|
||||
}, price);
|
||||
}
|
||||
|
||||
private updatePositionValues(symbol: string, currentPrice: number): void {
|
||||
const prices = new Map([[symbol, currentPrice]]);
|
||||
this.positionManager.updateMarketPrices(prices);
|
||||
}
|
||||
|
||||
private checkExitConditions(symbol: string, currentPrice: number): void {
|
||||
const position = this.positionManager.getPosition(symbol);
|
||||
if (!position) return;
|
||||
|
||||
const stopLoss = this.stopLosses.get(symbol);
|
||||
const takeProfit = this.takeProfits.get(symbol);
|
||||
|
||||
// Check stop loss
|
||||
if (stopLoss) {
|
||||
if ((position.quantity > 0 && currentPrice <= stopLoss) ||
|
||||
(position.quantity < 0 && currentPrice >= stopLoss)) {
|
||||
logger.info(`Stop loss triggered for ${symbol} at ${currentPrice}`);
|
||||
this.emit('signal', {
|
||||
type: 'close',
|
||||
symbol,
|
||||
strength: 1,
|
||||
reason: 'Stop loss triggered',
|
||||
metadata: { stopLoss, currentPrice }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check take profit
|
||||
if (takeProfit) {
|
||||
if ((position.quantity > 0 && currentPrice >= takeProfit) ||
|
||||
(position.quantity < 0 && currentPrice <= takeProfit)) {
|
||||
logger.info(`Take profit triggered for ${symbol} at ${currentPrice}`);
|
||||
this.emit('signal', {
|
||||
type: 'close',
|
||||
symbol,
|
||||
strength: 1,
|
||||
reason: 'Take profit triggered',
|
||||
metadata: { takeProfit, currentPrice }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getCurrentPositionMap(): Map<string, { quantity: number; value: number }> {
|
||||
const positionMap = new Map();
|
||||
|
||||
for (const position of this.positionManager.getOpenPositions()) {
|
||||
positionMap.set(position.symbol, {
|
||||
quantity: position.quantity,
|
||||
value: Math.abs(position.quantity * (position.currentPrice || position.avgPrice))
|
||||
});
|
||||
}
|
||||
|
||||
return positionMap;
|
||||
}
|
||||
|
||||
protected async onOrderUpdate(update: any): Promise<void> {
|
||||
await super.onOrderUpdate(update);
|
||||
|
||||
if (update.status === 'filled' && update.fills?.length > 0) {
|
||||
for (const fill of update.fills) {
|
||||
const trade = {
|
||||
symbol: update.symbol,
|
||||
side: update.side as 'buy' | 'sell',
|
||||
quantity: fill.quantity,
|
||||
price: fill.price,
|
||||
commission: fill.commission || 0,
|
||||
timestamp: new Date(fill.timestamp)
|
||||
};
|
||||
|
||||
const position = this.positionManager.updatePosition(trade);
|
||||
|
||||
// Update risk manager
|
||||
if (trade.pnl) {
|
||||
this.riskManager.updateAfterTrade(trade.pnl);
|
||||
}
|
||||
|
||||
// Set stop loss and take profit for new positions
|
||||
if (this.config.useATRStops && position.quantity !== 0) {
|
||||
const atr = this.indicatorManager.getATR(update.symbol);
|
||||
if (atr) {
|
||||
const currentATR = this.indicatorManager.getLatest(atr);
|
||||
if (currentATR) {
|
||||
const stopDistance = currentATR * this.config.atrMultiplier;
|
||||
const profitDistance = currentATR * this.config.atrMultiplier * 2;
|
||||
|
||||
if (position.quantity > 0) {
|
||||
this.stopLosses.set(update.symbol, fill.price - stopDistance);
|
||||
this.takeProfits.set(update.symbol, fill.price + profitDistance);
|
||||
} else {
|
||||
this.stopLosses.set(update.symbol, fill.price + stopDistance);
|
||||
this.takeProfits.set(update.symbol, fill.price - profitDistance);
|
||||
}
|
||||
|
||||
logger.info(`Set stop/take profit for ${update.symbol}: Stop=${this.stopLosses.get(update.symbol)?.toFixed(2)}, TP=${this.takeProfits.get(update.symbol)?.toFixed(2)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear stops if position closed
|
||||
if (position.quantity === 0) {
|
||||
this.stopLosses.delete(update.symbol);
|
||||
this.takeProfits.delete(update.symbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getPerformance(): any {
|
||||
const basePerf = super.getPerformance();
|
||||
const positionMetrics = this.positionManager.getPerformanceMetrics();
|
||||
const riskMetrics = this.riskManager.getMetrics(this.getCurrentPositionMap());
|
||||
const signalStats = this.signalManager.getSignalStats();
|
||||
|
||||
return {
|
||||
...basePerf,
|
||||
...positionMetrics,
|
||||
risk: riskMetrics,
|
||||
signals: signalStats
|
||||
};
|
||||
}
|
||||
|
||||
// Daily reset for risk metrics
|
||||
onDayEnd(): void {
|
||||
this.riskManager.resetDaily();
|
||||
logger.info('Daily risk metrics reset');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
import { BaseStrategy } from '../BaseStrategy';
|
||||
import { Order } from '../../types';
|
||||
import { TechnicalAnalysis, IncrementalIndicators, SignalGenerator, TradingSignal } from '../../indicators/TechnicalAnalysis';
|
||||
|
||||
interface IndicatorBasedConfig {
|
||||
symbol: string;
|
||||
initialCapital: number;
|
||||
positionSize: number;
|
||||
useRSI?: boolean;
|
||||
useMACD?: boolean;
|
||||
useBollingerBands?: boolean;
|
||||
useStochastic?: boolean;
|
||||
minSignalStrength?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Example strategy using multiple technical indicators from the Rust TA library
|
||||
*/
|
||||
export class IndicatorBasedStrategy extends BaseStrategy {
|
||||
private ta: TechnicalAnalysis;
|
||||
private incrementalIndicators: IncrementalIndicators;
|
||||
private signalGenerator: SignalGenerator;
|
||||
private priceHistory: {
|
||||
close: number[];
|
||||
high: number[];
|
||||
low: number[];
|
||||
volume: number[];
|
||||
};
|
||||
private readonly lookbackPeriod = 100; // Keep last 100 bars
|
||||
private lastSignal: TradingSignal | null = null;
|
||||
private config: IndicatorBasedConfig;
|
||||
|
||||
constructor(strategyId: string, config: IndicatorBasedConfig) {
|
||||
super(strategyId, config.symbol, config.initialCapital);
|
||||
this.config = {
|
||||
useRSI: true,
|
||||
useMACD: true,
|
||||
useBollingerBands: true,
|
||||
useStochastic: true,
|
||||
minSignalStrength: 0.6,
|
||||
...config
|
||||
};
|
||||
|
||||
this.ta = new TechnicalAnalysis();
|
||||
this.incrementalIndicators = new IncrementalIndicators();
|
||||
this.signalGenerator = new SignalGenerator();
|
||||
|
||||
this.priceHistory = {
|
||||
close: [],
|
||||
high: [],
|
||||
low: [],
|
||||
volume: []
|
||||
};
|
||||
|
||||
// Initialize incremental indicators for real-time updates
|
||||
this.incrementalIndicators.createSMA('sma20', 20);
|
||||
this.incrementalIndicators.createSMA('sma50', 50);
|
||||
this.incrementalIndicators.createEMA('ema12', 12);
|
||||
this.incrementalIndicators.createEMA('ema26', 26);
|
||||
this.incrementalIndicators.createRSI('rsi14', 14);
|
||||
}
|
||||
|
||||
onMarketData(data: any): Order | null {
|
||||
const { timestamp } = data;
|
||||
|
||||
// Update price history
|
||||
if ('close' in data && 'high' in data && 'low' in data) {
|
||||
this.priceHistory.close.push(data.close);
|
||||
this.priceHistory.high.push(data.high);
|
||||
this.priceHistory.low.push(data.low);
|
||||
this.priceHistory.volume.push(data.volume || 0);
|
||||
|
||||
// Trim to lookback period
|
||||
if (this.priceHistory.close.length > this.lookbackPeriod) {
|
||||
this.priceHistory.close.shift();
|
||||
this.priceHistory.high.shift();
|
||||
this.priceHistory.low.shift();
|
||||
this.priceHistory.volume.shift();
|
||||
}
|
||||
|
||||
// Update incremental indicators
|
||||
this.incrementalIndicators.update('sma20', data.close);
|
||||
this.incrementalIndicators.update('sma50', data.close);
|
||||
this.incrementalIndicators.update('ema12', data.close);
|
||||
this.incrementalIndicators.update('ema26', data.close);
|
||||
this.incrementalIndicators.update('rsi14', data.close);
|
||||
}
|
||||
|
||||
// Need enough data for indicators
|
||||
if (this.priceHistory.close.length < 26) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Generate trading signals
|
||||
const signal = this.signalGenerator.generateSignals(
|
||||
this.symbol,
|
||||
this.priceHistory,
|
||||
timestamp
|
||||
);
|
||||
|
||||
this.lastSignal = signal;
|
||||
|
||||
// Log signal for debugging
|
||||
if (signal.action !== 'HOLD') {
|
||||
console.log(`[${new Date(timestamp).toISOString()}] Signal: ${signal.action} (strength: ${signal.strength.toFixed(2)}) - ${signal.reason}`);
|
||||
}
|
||||
|
||||
// Check if signal is strong enough
|
||||
if (signal.strength < this.config.minSignalStrength) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Generate orders based on signals and position
|
||||
const currentPosition = this.positions[this.symbol] || 0;
|
||||
|
||||
if (signal.action === 'BUY' && currentPosition <= 0) {
|
||||
// Close short position if any
|
||||
if (currentPosition < 0) {
|
||||
return this.createOrder('market', 'buy', Math.abs(currentPosition));
|
||||
}
|
||||
// Open long position
|
||||
return this.createOrder('market', 'buy', this.config.positionSize);
|
||||
} else if (signal.action === 'SELL' && currentPosition >= 0) {
|
||||
// Close long position if any
|
||||
if (currentPosition > 0) {
|
||||
return this.createOrder('market', 'sell', Math.abs(currentPosition));
|
||||
}
|
||||
// Open short position (if allowed)
|
||||
// return this.createOrder('market', 'sell', this.config.positionSize);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
getState() {
|
||||
const incrementalValues: Record<string, number | null> = {
|
||||
sma20: this.incrementalIndicators.current('sma20'),
|
||||
sma50: this.incrementalIndicators.current('sma50'),
|
||||
ema12: this.incrementalIndicators.current('ema12'),
|
||||
ema26: this.incrementalIndicators.current('ema26'),
|
||||
rsi14: this.incrementalIndicators.current('rsi14')
|
||||
};
|
||||
|
||||
return {
|
||||
...super.getState(),
|
||||
priceHistoryLength: this.priceHistory.close.length,
|
||||
incrementalIndicators: incrementalValues,
|
||||
lastSignal: this.lastSignal,
|
||||
config: this.config
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Example of using batch indicator calculation
|
||||
*/
|
||||
analyzeHistoricalData(): void {
|
||||
if (this.priceHistory.close.length < 50) {
|
||||
console.log('Not enough data for historical analysis');
|
||||
return;
|
||||
}
|
||||
|
||||
const closes = this.priceHistory.close;
|
||||
|
||||
// Calculate various indicators
|
||||
const sma20 = this.ta.sma(closes, 20);
|
||||
const sma50 = this.ta.sma(closes, 50);
|
||||
const rsi = this.ta.rsi(closes, 14);
|
||||
const macd = this.ta.macd(closes);
|
||||
const bb = this.ta.bollingerBands(closes, 20, 2);
|
||||
const atr = this.ta.atr(
|
||||
this.priceHistory.high,
|
||||
this.priceHistory.low,
|
||||
this.priceHistory.close,
|
||||
14
|
||||
);
|
||||
|
||||
// Latest values
|
||||
const currentPrice = closes[closes.length - 1];
|
||||
const currentSMA20 = TechnicalAnalysis.latest(sma20);
|
||||
const currentSMA50 = TechnicalAnalysis.latest(sma50);
|
||||
const currentRSI = TechnicalAnalysis.latest(rsi);
|
||||
const currentATR = TechnicalAnalysis.latest(atr);
|
||||
|
||||
console.log('Historical Analysis:');
|
||||
console.log(`Current Price: ${currentPrice}`);
|
||||
console.log(`SMA20: ${currentSMA20?.toFixed(2)}`);
|
||||
console.log(`SMA50: ${currentSMA50?.toFixed(2)}`);
|
||||
console.log(`RSI: ${currentRSI?.toFixed(2)}`);
|
||||
console.log(`ATR: ${currentATR?.toFixed(2)}`);
|
||||
console.log(`MACD: ${TechnicalAnalysis.latest(macd.macd)?.toFixed(2)}`);
|
||||
console.log(`BB %B: ${((currentPrice - TechnicalAnalysis.latest(bb.lower)!) / (TechnicalAnalysis.latest(bb.upper)! - TechnicalAnalysis.latest(bb.lower)!)).toFixed(2)}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,308 @@
|
|||
import { BaseStrategy, Signal } from '../BaseStrategy';
|
||||
import { MarketData } from '../../types';
|
||||
import { IndicatorManager } from '../indicators/IndicatorManager';
|
||||
import { PositionManager, PositionSizingParams } from '../position/PositionManager';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
|
||||
const logger = getLogger('SimpleMovingAverageCrossoverV2');
|
||||
|
||||
export interface SMAStrategyConfig {
|
||||
fastPeriod?: number;
|
||||
slowPeriod?: number;
|
||||
positionSizePct?: number;
|
||||
riskPerTrade?: number;
|
||||
useATRStops?: boolean;
|
||||
minHoldingBars?: number;
|
||||
debugInterval?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refactored SMA Crossover Strategy using new TA library
|
||||
*/
|
||||
export class SimpleMovingAverageCrossoverV2 extends BaseStrategy {
|
||||
private indicatorManager: IndicatorManager;
|
||||
private positionManager: PositionManager;
|
||||
|
||||
// Strategy parameters
|
||||
private readonly config: Required<SMAStrategyConfig>;
|
||||
private lastTradeBar = new Map<string, number>();
|
||||
private barCount = new Map<string, number>();
|
||||
private totalSignals = 0;
|
||||
|
||||
constructor(strategyConfig: any, modeManager?: any, executionService?: any) {
|
||||
super(strategyConfig, modeManager, executionService);
|
||||
|
||||
// Initialize config with defaults
|
||||
this.config = {
|
||||
fastPeriod: 10,
|
||||
slowPeriod: 20,
|
||||
positionSizePct: 0.1,
|
||||
riskPerTrade: 0.02,
|
||||
useATRStops: true,
|
||||
minHoldingBars: 1,
|
||||
debugInterval: 20,
|
||||
...strategyConfig.params
|
||||
};
|
||||
|
||||
this.indicatorManager = new IndicatorManager();
|
||||
this.positionManager = new PositionManager(strategyConfig.initialCapital || 100000);
|
||||
|
||||
logger.info(`SimpleMovingAverageCrossoverV2 initialized:`, this.config);
|
||||
}
|
||||
|
||||
protected updateIndicators(data: MarketData): void {
|
||||
if (data.type !== 'bar') return;
|
||||
|
||||
const { symbol, timestamp } = data.data;
|
||||
const { open, high, low, close, volume } = data.data;
|
||||
|
||||
// Update bar count
|
||||
const currentBar = (this.barCount.get(symbol) || 0) + 1;
|
||||
this.barCount.set(symbol, currentBar);
|
||||
|
||||
// First time seeing this symbol
|
||||
if (!this.indicatorManager.getHistoryLength(symbol)) {
|
||||
logger.info(`📊 Starting to track ${symbol} @ ${close}`);
|
||||
|
||||
// Setup incremental indicators for real-time updates
|
||||
this.indicatorManager.setupIncrementalIndicator(symbol, 'fast_sma', {
|
||||
type: 'sma',
|
||||
period: this.config.fastPeriod
|
||||
});
|
||||
this.indicatorManager.setupIncrementalIndicator(symbol, 'slow_sma', {
|
||||
type: 'sma',
|
||||
period: this.config.slowPeriod
|
||||
});
|
||||
|
||||
if (this.config.useATRStops) {
|
||||
this.indicatorManager.setupIncrementalIndicator(symbol, 'atr', {
|
||||
type: 'sma', // Using SMA as proxy for now
|
||||
period: 14
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update price history
|
||||
this.indicatorManager.updatePrice({
|
||||
symbol,
|
||||
timestamp,
|
||||
open,
|
||||
high,
|
||||
low,
|
||||
close,
|
||||
volume
|
||||
});
|
||||
|
||||
// Update position market prices
|
||||
const currentPrices = new Map([[symbol, close]]);
|
||||
this.positionManager.updateMarketPrices(currentPrices);
|
||||
|
||||
// Log when we have enough data
|
||||
const historyLength = this.indicatorManager.getHistoryLength(symbol);
|
||||
if (historyLength === this.config.slowPeriod) {
|
||||
logger.info(`✅ ${symbol} has enough history (${historyLength} bars) to start trading`);
|
||||
}
|
||||
}
|
||||
|
||||
protected async generateSignal(data: MarketData): Promise<Signal | null> {
|
||||
if (data.type !== 'bar') return null;
|
||||
|
||||
const { symbol, timestamp } = data.data;
|
||||
const { close } = data.data;
|
||||
const historyLength = this.indicatorManager.getHistoryLength(symbol);
|
||||
|
||||
// Need enough data for slow MA
|
||||
if (historyLength < this.config.slowPeriod) {
|
||||
if (historyLength % 5 === 0) {
|
||||
logger.debug(`${symbol} - Building history: ${historyLength}/${this.config.slowPeriod} bars`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate indicators
|
||||
const fastMA = this.indicatorManager.getSMA(symbol, this.config.fastPeriod);
|
||||
const slowMA = this.indicatorManager.getSMA(symbol, this.config.slowPeriod);
|
||||
|
||||
if (!fastMA || !slowMA) return null;
|
||||
|
||||
// Get current and previous values
|
||||
const currentFast = this.indicatorManager.getLatest(fastMA);
|
||||
const currentSlow = this.indicatorManager.getLatest(slowMA);
|
||||
|
||||
if (currentFast === null || currentSlow === null) return null;
|
||||
|
||||
// Check for crossovers
|
||||
const goldenCross = this.indicatorManager.checkCrossover(fastMA, slowMA);
|
||||
const deathCross = this.indicatorManager.checkCrossunder(fastMA, slowMA);
|
||||
|
||||
// Get current position
|
||||
const currentPosition = this.positionManager.getPositionQuantity(symbol);
|
||||
const currentBar = this.barCount.get(symbol) || 0;
|
||||
const lastTradeBar = this.lastTradeBar.get(symbol) || 0;
|
||||
const barsSinceLastTrade = lastTradeBar > 0 ? currentBar - lastTradeBar : Number.MAX_SAFE_INTEGER;
|
||||
|
||||
// Enhanced debugging
|
||||
const maDiff = currentFast - currentSlow;
|
||||
const maDiffPct = (maDiff / currentSlow) * 100;
|
||||
const shouldLog = historyLength % this.config.debugInterval === 0 ||
|
||||
Math.abs(maDiffPct) < 1.0 ||
|
||||
goldenCross ||
|
||||
deathCross;
|
||||
|
||||
if (shouldLog) {
|
||||
const dateStr = new Date(timestamp).toISOString().split('T')[0];
|
||||
logger.info(`${symbol} @ ${dateStr} [Bar ${currentBar}]:`);
|
||||
logger.info(` Price: $${close.toFixed(2)}`);
|
||||
logger.info(` Fast MA (${this.config.fastPeriod}): $${currentFast.toFixed(2)}`);
|
||||
logger.info(` Slow MA (${this.config.slowPeriod}): $${currentSlow.toFixed(2)}`);
|
||||
logger.info(` MA Diff: ${maDiff.toFixed(2)} (${maDiffPct.toFixed(2)}%)`);
|
||||
logger.info(` Position: ${currentPosition} shares`);
|
||||
|
||||
// Show additional indicators if available
|
||||
const rsi = this.indicatorManager.getRSI(symbol);
|
||||
if (rsi) {
|
||||
const currentRSI = this.indicatorManager.getLatest(rsi);
|
||||
logger.info(` RSI: ${currentRSI?.toFixed(2)}`);
|
||||
}
|
||||
|
||||
if (goldenCross) logger.info(` 🟢 GOLDEN CROSS DETECTED!`);
|
||||
if (deathCross) logger.info(` 🔴 DEATH CROSS DETECTED!`);
|
||||
}
|
||||
|
||||
// Check minimum holding period
|
||||
if (barsSinceLastTrade < this.config.minHoldingBars && lastTradeBar > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Position sizing parameters
|
||||
const sizingParams: PositionSizingParams = {
|
||||
accountBalance: this.positionManager.getAccountBalance(),
|
||||
riskPerTrade: this.config.riskPerTrade,
|
||||
volatilityAdjustment: this.config.useATRStops
|
||||
};
|
||||
|
||||
if (this.config.useATRStops) {
|
||||
const atr = this.indicatorManager.getATR(symbol);
|
||||
if (atr) {
|
||||
sizingParams.atr = this.indicatorManager.getLatest(atr) || undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate signals
|
||||
if (goldenCross) {
|
||||
logger.info(`🟢 Golden cross detected for ${symbol}`);
|
||||
|
||||
if (currentPosition < 0) {
|
||||
// Close short position
|
||||
this.lastTradeBar.set(symbol, currentBar);
|
||||
this.totalSignals++;
|
||||
return {
|
||||
type: 'buy',
|
||||
symbol,
|
||||
strength: 0.8,
|
||||
reason: 'Golden cross - Closing short position',
|
||||
metadata: {
|
||||
fastMA: currentFast,
|
||||
slowMA: currentSlow,
|
||||
crossoverType: 'golden',
|
||||
price: close,
|
||||
quantity: Math.abs(currentPosition)
|
||||
}
|
||||
};
|
||||
} else if (currentPosition === 0) {
|
||||
// Calculate position size
|
||||
const positionSize = this.positionManager.calculatePositionSize(sizingParams, close);
|
||||
|
||||
logger.info(` Opening long position: ${positionSize} shares`);
|
||||
logger.info(` Account balance: $${sizingParams.accountBalance.toFixed(2)}`);
|
||||
|
||||
this.lastTradeBar.set(symbol, currentBar);
|
||||
this.totalSignals++;
|
||||
|
||||
return {
|
||||
type: 'buy',
|
||||
symbol,
|
||||
strength: 0.8,
|
||||
reason: 'Golden cross - Fast MA crossed above Slow MA',
|
||||
metadata: {
|
||||
fastMA: currentFast,
|
||||
slowMA: currentSlow,
|
||||
crossoverType: 'golden',
|
||||
price: close,
|
||||
quantity: positionSize
|
||||
}
|
||||
};
|
||||
}
|
||||
} else if (deathCross && currentPosition > 0) {
|
||||
logger.info(`🔴 Death cross detected for ${symbol}`);
|
||||
|
||||
this.lastTradeBar.set(symbol, currentBar);
|
||||
this.totalSignals++;
|
||||
|
||||
return {
|
||||
type: 'sell',
|
||||
symbol,
|
||||
strength: 0.8,
|
||||
reason: 'Death cross - Fast MA crossed below Slow MA',
|
||||
metadata: {
|
||||
fastMA: currentFast,
|
||||
slowMA: currentSlow,
|
||||
crossoverType: 'death',
|
||||
price: close,
|
||||
quantity: currentPosition
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected async onOrderUpdate(update: any): Promise<void> {
|
||||
await super.onOrderUpdate(update);
|
||||
|
||||
// Update position manager with fills
|
||||
if (update.status === 'filled' && update.fills?.length > 0) {
|
||||
for (const fill of update.fills) {
|
||||
this.positionManager.updatePosition({
|
||||
symbol: update.symbol,
|
||||
side: update.side,
|
||||
quantity: fill.quantity,
|
||||
price: fill.price,
|
||||
commission: fill.commission || 0,
|
||||
timestamp: new Date(fill.timestamp)
|
||||
});
|
||||
}
|
||||
|
||||
// Log performance metrics periodically
|
||||
if (this.totalSignals % 5 === 0) {
|
||||
const metrics = this.positionManager.getPerformanceMetrics();
|
||||
logger.info('📊 Strategy Performance:', {
|
||||
trades: metrics.totalTrades,
|
||||
winRate: `${metrics.winRate.toFixed(2)}%`,
|
||||
totalPnL: `$${metrics.totalPnl.toFixed(2)}`,
|
||||
returnPct: `${metrics.returnPct.toFixed(2)}%`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getPerformance(): any {
|
||||
const metrics = this.positionManager.getPerformanceMetrics();
|
||||
return {
|
||||
...super.getPerformance(),
|
||||
...metrics,
|
||||
totalSignals: this.totalSignals,
|
||||
openPositions: this.positionManager.getOpenPositions()
|
||||
};
|
||||
}
|
||||
|
||||
// Optional: Get current state for debugging
|
||||
getState() {
|
||||
return {
|
||||
config: this.config,
|
||||
totalSignals: this.totalSignals,
|
||||
performance: this.getPerformance(),
|
||||
positions: Array.from(this.positions.entries())
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,315 @@
|
|||
import { TechnicalAnalysis, IncrementalIndicators } from '../../indicators/TechnicalAnalysis';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
|
||||
const logger = getLogger('IndicatorManager');
|
||||
|
||||
export interface IndicatorConfig {
|
||||
type: 'sma' | 'ema' | 'rsi' | 'macd' | 'bollinger' | 'stochastic' | 'atr';
|
||||
period?: number;
|
||||
fastPeriod?: number;
|
||||
slowPeriod?: number;
|
||||
signalPeriod?: number;
|
||||
stdDev?: number;
|
||||
kPeriod?: number;
|
||||
dPeriod?: number;
|
||||
smoothK?: number;
|
||||
}
|
||||
|
||||
export interface PriceData {
|
||||
symbol: string;
|
||||
timestamp: number;
|
||||
open: number;
|
||||
high: number;
|
||||
low: number;
|
||||
close: number;
|
||||
volume: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages technical indicators for a strategy
|
||||
* Handles both batch and incremental calculations
|
||||
*/
|
||||
export class IndicatorManager {
|
||||
private ta: TechnicalAnalysis;
|
||||
private incrementalIndicators: IncrementalIndicators;
|
||||
private priceHistory: Map<string, {
|
||||
open: number[];
|
||||
high: number[];
|
||||
low: number[];
|
||||
close: number[];
|
||||
volume: number[];
|
||||
}> = new Map();
|
||||
|
||||
private indicatorCache: Map<string, Map<string, any>> = new Map();
|
||||
private maxHistoryLength: number;
|
||||
|
||||
constructor(maxHistoryLength = 500) {
|
||||
this.ta = new TechnicalAnalysis();
|
||||
this.incrementalIndicators = new IncrementalIndicators();
|
||||
this.maxHistoryLength = maxHistoryLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update price history with new data
|
||||
*/
|
||||
updatePrice(data: PriceData): void {
|
||||
const { symbol, open, high, low, close, volume } = data;
|
||||
|
||||
if (!this.priceHistory.has(symbol)) {
|
||||
this.priceHistory.set(symbol, {
|
||||
open: [],
|
||||
high: [],
|
||||
low: [],
|
||||
close: [],
|
||||
volume: []
|
||||
});
|
||||
}
|
||||
|
||||
const history = this.priceHistory.get(symbol)!;
|
||||
|
||||
// Add new data
|
||||
history.open.push(open);
|
||||
history.high.push(high);
|
||||
history.low.push(low);
|
||||
history.close.push(close);
|
||||
history.volume.push(volume);
|
||||
|
||||
// Trim to max length
|
||||
if (history.close.length > this.maxHistoryLength) {
|
||||
history.open.shift();
|
||||
history.high.shift();
|
||||
history.low.shift();
|
||||
history.close.shift();
|
||||
history.volume.shift();
|
||||
}
|
||||
|
||||
// Clear cache for this symbol as data has changed
|
||||
this.indicatorCache.delete(symbol);
|
||||
|
||||
// Update incremental indicators
|
||||
this.updateIncrementalIndicators(symbol, close);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get price history for a symbol
|
||||
*/
|
||||
getPriceHistory(symbol: string) {
|
||||
return this.priceHistory.get(symbol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of price bars for a symbol
|
||||
*/
|
||||
getHistoryLength(symbol: string): number {
|
||||
const history = this.priceHistory.get(symbol);
|
||||
return history ? history.close.length : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate SMA
|
||||
*/
|
||||
getSMA(symbol: string, period: number): number[] | null {
|
||||
const cacheKey = `sma_${period}`;
|
||||
return this.getCachedOrCalculate(symbol, cacheKey, () => {
|
||||
const history = this.priceHistory.get(symbol);
|
||||
if (!history || history.close.length < period) return null;
|
||||
return this.ta.sma(history.close, period);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate EMA
|
||||
*/
|
||||
getEMA(symbol: string, period: number): number[] | null {
|
||||
const cacheKey = `ema_${period}`;
|
||||
return this.getCachedOrCalculate(symbol, cacheKey, () => {
|
||||
const history = this.priceHistory.get(symbol);
|
||||
if (!history || history.close.length < period) return null;
|
||||
return this.ta.ema(history.close, period);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate RSI
|
||||
*/
|
||||
getRSI(symbol: string, period: number = 14): number[] | null {
|
||||
const cacheKey = `rsi_${period}`;
|
||||
return this.getCachedOrCalculate(symbol, cacheKey, () => {
|
||||
const history = this.priceHistory.get(symbol);
|
||||
if (!history || history.close.length < period + 1) return null;
|
||||
return this.ta.rsi(history.close, period);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate MACD
|
||||
*/
|
||||
getMACD(symbol: string, fastPeriod = 12, slowPeriod = 26, signalPeriod = 9) {
|
||||
const cacheKey = `macd_${fastPeriod}_${slowPeriod}_${signalPeriod}`;
|
||||
return this.getCachedOrCalculate(symbol, cacheKey, () => {
|
||||
const history = this.priceHistory.get(symbol);
|
||||
if (!history || history.close.length < slowPeriod + signalPeriod) return null;
|
||||
return this.ta.macd(history.close, fastPeriod, slowPeriod, signalPeriod);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Bollinger Bands
|
||||
*/
|
||||
getBollingerBands(symbol: string, period = 20, stdDev = 2) {
|
||||
const cacheKey = `bb_${period}_${stdDev}`;
|
||||
return this.getCachedOrCalculate(symbol, cacheKey, () => {
|
||||
const history = this.priceHistory.get(symbol);
|
||||
if (!history || history.close.length < period) return null;
|
||||
return this.ta.bollingerBands(history.close, period, stdDev);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Stochastic
|
||||
*/
|
||||
getStochastic(symbol: string, kPeriod = 14, dPeriod = 3, smoothK = 1) {
|
||||
const cacheKey = `stoch_${kPeriod}_${dPeriod}_${smoothK}`;
|
||||
return this.getCachedOrCalculate(symbol, cacheKey, () => {
|
||||
const history = this.priceHistory.get(symbol);
|
||||
if (!history || history.close.length < kPeriod) return null;
|
||||
return this.ta.stochastic(
|
||||
history.high,
|
||||
history.low,
|
||||
history.close,
|
||||
kPeriod,
|
||||
dPeriod,
|
||||
smoothK
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate ATR
|
||||
*/
|
||||
getATR(symbol: string, period = 14): number[] | null {
|
||||
const cacheKey = `atr_${period}`;
|
||||
return this.getCachedOrCalculate(symbol, cacheKey, () => {
|
||||
const history = this.priceHistory.get(symbol);
|
||||
if (!history || history.close.length < period + 1) return null;
|
||||
return this.ta.atr(history.high, history.low, history.close, period);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get latest value from an indicator
|
||||
*/
|
||||
getLatest(values: number[] | null): number | null {
|
||||
if (!values || values.length === 0) return null;
|
||||
return values[values.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for crossover
|
||||
*/
|
||||
checkCrossover(series1: number[] | null, series2: number[] | null): boolean {
|
||||
if (!series1 || !series2) return false;
|
||||
return TechnicalAnalysis.crossover(series1, series2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for crossunder
|
||||
*/
|
||||
checkCrossunder(series1: number[] | null, series2: number[] | null): boolean {
|
||||
if (!series1 || !series2) return false;
|
||||
return TechnicalAnalysis.crossunder(series1, series2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup incremental indicators
|
||||
*/
|
||||
setupIncrementalIndicator(symbol: string, name: string, config: IndicatorConfig): void {
|
||||
const key = `${symbol}_${name}`;
|
||||
|
||||
switch (config.type) {
|
||||
case 'sma':
|
||||
this.incrementalIndicators.createSMA(key, config.period!);
|
||||
break;
|
||||
case 'ema':
|
||||
this.incrementalIndicators.createEMA(key, config.period!);
|
||||
break;
|
||||
case 'rsi':
|
||||
this.incrementalIndicators.createRSI(key, config.period!);
|
||||
break;
|
||||
default:
|
||||
logger.warn(`Incremental indicator type ${config.type} not supported`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get incremental indicator value
|
||||
*/
|
||||
getIncrementalValue(symbol: string, name: string): number | null {
|
||||
const key = `${symbol}_${name}`;
|
||||
return this.incrementalIndicators.current(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all data for a symbol
|
||||
*/
|
||||
clearSymbol(symbol: string): void {
|
||||
this.priceHistory.delete(symbol);
|
||||
this.indicatorCache.delete(symbol);
|
||||
|
||||
// Reset incremental indicators for this symbol
|
||||
const indicators = this.incrementalIndicators as any;
|
||||
for (const [key, indicator] of indicators.indicators) {
|
||||
if (key.startsWith(`${symbol}_`)) {
|
||||
if ('reset' in indicator) {
|
||||
indicator.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all data
|
||||
*/
|
||||
clearAll(): void {
|
||||
this.priceHistory.clear();
|
||||
this.indicatorCache.clear();
|
||||
this.incrementalIndicators.resetAll();
|
||||
}
|
||||
|
||||
private getCachedOrCalculate<T>(
|
||||
symbol: string,
|
||||
cacheKey: string,
|
||||
calculator: () => T | null
|
||||
): T | null {
|
||||
if (!this.indicatorCache.has(symbol)) {
|
||||
this.indicatorCache.set(symbol, new Map());
|
||||
}
|
||||
|
||||
const symbolCache = this.indicatorCache.get(symbol)!;
|
||||
|
||||
if (symbolCache.has(cacheKey)) {
|
||||
return symbolCache.get(cacheKey);
|
||||
}
|
||||
|
||||
const result = calculator();
|
||||
if (result !== null) {
|
||||
symbolCache.set(cacheKey, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private updateIncrementalIndicators(symbol: string, price: number): void {
|
||||
// Update all incremental indicators for this symbol
|
||||
const indicators = this.incrementalIndicators as any;
|
||||
for (const [key] of indicators.indicators) {
|
||||
if (key.startsWith(`${symbol}_`)) {
|
||||
try {
|
||||
this.incrementalIndicators.update(key, price);
|
||||
} catch (error) {
|
||||
logger.error(`Error updating incremental indicator ${key}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,290 @@
|
|||
import { getLogger } from '@stock-bot/logger';
|
||||
|
||||
const logger = getLogger('PositionManager');
|
||||
|
||||
export interface Position {
|
||||
symbol: string;
|
||||
quantity: number;
|
||||
avgPrice: number;
|
||||
currentPrice?: number;
|
||||
unrealizedPnl?: number;
|
||||
realizedPnl: number;
|
||||
openTime: Date;
|
||||
lastUpdateTime: Date;
|
||||
}
|
||||
|
||||
export interface Trade {
|
||||
symbol: string;
|
||||
side: 'buy' | 'sell';
|
||||
quantity: number;
|
||||
price: number;
|
||||
commission: number;
|
||||
timestamp: Date;
|
||||
pnl?: number;
|
||||
}
|
||||
|
||||
export interface PositionSizingParams {
|
||||
accountBalance: number;
|
||||
riskPerTrade: number; // As percentage (e.g., 0.02 for 2%)
|
||||
stopLossDistance?: number; // Price distance for stop loss
|
||||
maxPositionSize?: number; // Max % of account in one position
|
||||
volatilityAdjustment?: boolean;
|
||||
atr?: number; // For volatility-based sizing
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages positions and calculates position sizes
|
||||
*/
|
||||
export class PositionManager {
|
||||
private positions: Map<string, Position> = new Map();
|
||||
private trades: Trade[] = [];
|
||||
private accountBalance: number;
|
||||
private initialBalance: number;
|
||||
|
||||
constructor(initialBalance: number = 100000) {
|
||||
this.initialBalance = initialBalance;
|
||||
this.accountBalance = initialBalance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update position with a new trade
|
||||
*/
|
||||
updatePosition(trade: Trade): Position {
|
||||
const { symbol, side, quantity, price, commission } = trade;
|
||||
let position = this.positions.get(symbol);
|
||||
|
||||
if (!position) {
|
||||
// New position
|
||||
position = {
|
||||
symbol,
|
||||
quantity: side === 'buy' ? quantity : -quantity,
|
||||
avgPrice: price,
|
||||
realizedPnl: -commission,
|
||||
openTime: trade.timestamp,
|
||||
lastUpdateTime: trade.timestamp
|
||||
};
|
||||
} else {
|
||||
const oldQuantity = position.quantity;
|
||||
const newQuantity = side === 'buy'
|
||||
? oldQuantity + quantity
|
||||
: oldQuantity - quantity;
|
||||
|
||||
if (Math.sign(oldQuantity) !== Math.sign(newQuantity) && oldQuantity !== 0) {
|
||||
// Position flip or close
|
||||
const closedQuantity = Math.min(Math.abs(oldQuantity), quantity);
|
||||
const pnl = this.calculatePnl(
|
||||
position.avgPrice,
|
||||
price,
|
||||
closedQuantity,
|
||||
oldQuantity > 0 ? 'sell' : 'buy'
|
||||
);
|
||||
|
||||
position.realizedPnl += pnl - commission;
|
||||
trade.pnl = pnl - commission;
|
||||
|
||||
// Update average price if position continues
|
||||
if (Math.abs(newQuantity) > 0.0001) {
|
||||
position.avgPrice = price;
|
||||
}
|
||||
} else if (Math.sign(oldQuantity) === Math.sign(newQuantity) || oldQuantity === 0) {
|
||||
// Adding to position
|
||||
const totalCost = Math.abs(oldQuantity) * position.avgPrice + quantity * price;
|
||||
const totalQuantity = Math.abs(oldQuantity) + quantity;
|
||||
position.avgPrice = totalCost / totalQuantity;
|
||||
position.realizedPnl -= commission;
|
||||
}
|
||||
|
||||
position.quantity = newQuantity;
|
||||
position.lastUpdateTime = trade.timestamp;
|
||||
}
|
||||
|
||||
// Store or remove position
|
||||
if (Math.abs(position.quantity) < 0.0001) {
|
||||
this.positions.delete(symbol);
|
||||
logger.info(`Closed position for ${symbol}, realized P&L: $${position.realizedPnl.toFixed(2)}`);
|
||||
} else {
|
||||
this.positions.set(symbol, position);
|
||||
}
|
||||
|
||||
// Record trade
|
||||
this.trades.push(trade);
|
||||
|
||||
// Update account balance
|
||||
if (trade.pnl !== undefined) {
|
||||
this.accountBalance += trade.pnl;
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current position for a symbol
|
||||
*/
|
||||
getPosition(symbol: string): Position | undefined {
|
||||
return this.positions.get(symbol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get position quantity
|
||||
*/
|
||||
getPositionQuantity(symbol: string): number {
|
||||
const position = this.positions.get(symbol);
|
||||
return position ? position.quantity : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if has position
|
||||
*/
|
||||
hasPosition(symbol: string): boolean {
|
||||
const position = this.positions.get(symbol);
|
||||
return position !== undefined && Math.abs(position.quantity) > 0.0001;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all open positions
|
||||
*/
|
||||
getOpenPositions(): Position[] {
|
||||
return Array.from(this.positions.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update market prices for positions
|
||||
*/
|
||||
updateMarketPrices(prices: Map<string, number>): void {
|
||||
for (const [symbol, position] of this.positions) {
|
||||
const currentPrice = prices.get(symbol);
|
||||
if (currentPrice) {
|
||||
position.currentPrice = currentPrice;
|
||||
position.unrealizedPnl = this.calculatePnl(
|
||||
position.avgPrice,
|
||||
currentPrice,
|
||||
Math.abs(position.quantity),
|
||||
position.quantity > 0 ? 'sell' : 'buy'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate position size based on risk parameters
|
||||
*/
|
||||
calculatePositionSize(params: PositionSizingParams, currentPrice: number): number {
|
||||
const {
|
||||
accountBalance,
|
||||
riskPerTrade,
|
||||
stopLossDistance,
|
||||
maxPositionSize = 0.25,
|
||||
volatilityAdjustment = false,
|
||||
atr
|
||||
} = params;
|
||||
|
||||
let positionSize: number;
|
||||
|
||||
if (stopLossDistance && stopLossDistance > 0) {
|
||||
// Risk-based position sizing
|
||||
const riskAmount = accountBalance * riskPerTrade;
|
||||
positionSize = Math.floor(riskAmount / stopLossDistance);
|
||||
} else if (volatilityAdjustment && atr) {
|
||||
// Volatility-based position sizing
|
||||
const riskAmount = accountBalance * riskPerTrade;
|
||||
const stopDistance = atr * 2; // 2 ATR stop
|
||||
positionSize = Math.floor(riskAmount / stopDistance);
|
||||
} else {
|
||||
// Fixed percentage position sizing
|
||||
const positionValue = accountBalance * riskPerTrade * 10; // Simplified
|
||||
positionSize = Math.floor(positionValue / currentPrice);
|
||||
}
|
||||
|
||||
// Apply max position size limit
|
||||
const maxShares = Math.floor((accountBalance * maxPositionSize) / currentPrice);
|
||||
positionSize = Math.min(positionSize, maxShares);
|
||||
|
||||
// Ensure minimum position size
|
||||
return Math.max(1, positionSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Kelly Criterion position size
|
||||
*/
|
||||
calculateKellySize(winRate: number, avgWin: number, avgLoss: number, currentPrice: number): number {
|
||||
if (avgLoss === 0) return 0;
|
||||
|
||||
const b = avgWin / avgLoss;
|
||||
const p = winRate;
|
||||
const q = 1 - p;
|
||||
const kelly = (p * b - q) / b;
|
||||
|
||||
// Apply Kelly fraction (usually 0.25 to be conservative)
|
||||
const kellyFraction = 0.25;
|
||||
const percentageOfCapital = Math.max(0, Math.min(0.25, kelly * kellyFraction));
|
||||
|
||||
const positionValue = this.accountBalance * percentageOfCapital;
|
||||
return Math.max(1, Math.floor(positionValue / currentPrice));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get performance metrics
|
||||
*/
|
||||
getPerformanceMetrics() {
|
||||
const totalTrades = this.trades.length;
|
||||
const winningTrades = this.trades.filter(t => (t.pnl || 0) > 0);
|
||||
const losingTrades = this.trades.filter(t => (t.pnl || 0) < 0);
|
||||
|
||||
const totalPnl = this.trades.reduce((sum, t) => sum + (t.pnl || 0), 0);
|
||||
const unrealizedPnl = Array.from(this.positions.values())
|
||||
.reduce((sum, p) => sum + (p.unrealizedPnl || 0), 0);
|
||||
|
||||
const winRate = totalTrades > 0 ? winningTrades.length / totalTrades : 0;
|
||||
const avgWin = winningTrades.length > 0
|
||||
? winningTrades.reduce((sum, t) => sum + (t.pnl || 0), 0) / winningTrades.length
|
||||
: 0;
|
||||
const avgLoss = losingTrades.length > 0
|
||||
? Math.abs(losingTrades.reduce((sum, t) => sum + (t.pnl || 0), 0) / losingTrades.length)
|
||||
: 0;
|
||||
|
||||
const profitFactor = avgLoss > 0 ? (avgWin * winningTrades.length) / (avgLoss * losingTrades.length) : 0;
|
||||
|
||||
return {
|
||||
totalTrades,
|
||||
winningTrades: winningTrades.length,
|
||||
losingTrades: losingTrades.length,
|
||||
winRate: winRate * 100,
|
||||
totalPnl,
|
||||
unrealizedPnl,
|
||||
totalEquity: this.accountBalance + unrealizedPnl,
|
||||
avgWin,
|
||||
avgLoss,
|
||||
profitFactor,
|
||||
returnPct: ((this.accountBalance - this.initialBalance) / this.initialBalance) * 100
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get account balance
|
||||
*/
|
||||
getAccountBalance(): number {
|
||||
return this.accountBalance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total equity (balance + unrealized P&L)
|
||||
*/
|
||||
getTotalEquity(): number {
|
||||
const unrealizedPnl = Array.from(this.positions.values())
|
||||
.reduce((sum, p) => sum + (p.unrealizedPnl || 0), 0);
|
||||
return this.accountBalance + unrealizedPnl;
|
||||
}
|
||||
|
||||
private calculatePnl(
|
||||
entryPrice: number,
|
||||
exitPrice: number,
|
||||
quantity: number,
|
||||
side: 'buy' | 'sell'
|
||||
): number {
|
||||
if (side === 'sell') {
|
||||
return (exitPrice - entryPrice) * quantity;
|
||||
} else {
|
||||
return (entryPrice - exitPrice) * quantity;
|
||||
}
|
||||
}
|
||||
}
|
||||
262
apps/stock/orchestrator/src/strategies/risk/RiskManager.ts
Normal file
262
apps/stock/orchestrator/src/strategies/risk/RiskManager.ts
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
import { getLogger } from '@stock-bot/logger';
|
||||
|
||||
const logger = getLogger('RiskManager');
|
||||
|
||||
export interface RiskLimits {
|
||||
maxPositions?: number;
|
||||
maxPositionSizePct?: number; // Max % of account per position
|
||||
maxTotalExposurePct?: number; // Max % of account in all positions
|
||||
maxDailyLossPct?: number; // Max daily loss as % of account
|
||||
maxDrawdownPct?: number; // Max drawdown allowed
|
||||
maxConsecutiveLosses?: number;
|
||||
minWinRate?: number; // Minimum win rate to continue trading
|
||||
}
|
||||
|
||||
export interface RiskMetrics {
|
||||
currentExposure: number;
|
||||
currentExposurePct: number;
|
||||
dailyPnl: number;
|
||||
dailyPnlPct: number;
|
||||
currentDrawdown: number;
|
||||
currentDrawdownPct: number;
|
||||
maxDrawdown: number;
|
||||
maxDrawdownPct: number;
|
||||
consecutiveLosses: number;
|
||||
volatility: number;
|
||||
sharpeRatio: number;
|
||||
var95: number; // Value at Risk 95%
|
||||
}
|
||||
|
||||
export interface RiskCheckResult {
|
||||
allowed: boolean;
|
||||
reason?: string;
|
||||
adjustedSize?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages risk limits and calculates risk metrics
|
||||
*/
|
||||
export class RiskManager {
|
||||
private limits: Required<RiskLimits>;
|
||||
private dailyPnl = 0;
|
||||
private dailyStartBalance: number;
|
||||
private peakBalance: number;
|
||||
private consecutiveLosses = 0;
|
||||
private dailyReturns: number[] = [];
|
||||
private readonly lookbackDays = 30;
|
||||
|
||||
constructor(
|
||||
private accountBalance: number,
|
||||
limits: RiskLimits = {}
|
||||
) {
|
||||
this.limits = {
|
||||
maxPositions: 10,
|
||||
maxPositionSizePct: 0.1,
|
||||
maxTotalExposurePct: 0.6,
|
||||
maxDailyLossPct: 0.05,
|
||||
maxDrawdownPct: 0.2,
|
||||
maxConsecutiveLosses: 5,
|
||||
minWinRate: 0.3,
|
||||
...limits
|
||||
};
|
||||
|
||||
this.dailyStartBalance = accountBalance;
|
||||
this.peakBalance = accountBalance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a new position is allowed
|
||||
*/
|
||||
checkNewPosition(
|
||||
symbol: string,
|
||||
proposedSize: number,
|
||||
price: number,
|
||||
currentPositions: Map<string, { quantity: number; value: number }>
|
||||
): RiskCheckResult {
|
||||
const proposedValue = Math.abs(proposedSize * price);
|
||||
const proposedPct = proposedValue / this.accountBalance;
|
||||
|
||||
// Check max position size
|
||||
if (proposedPct > this.limits.maxPositionSizePct) {
|
||||
const maxValue = this.accountBalance * this.limits.maxPositionSizePct;
|
||||
const adjustedSize = Math.floor(maxValue / price);
|
||||
|
||||
return {
|
||||
allowed: true,
|
||||
reason: `Position size reduced from ${proposedSize} to ${adjustedSize} (max ${(this.limits.maxPositionSizePct * 100).toFixed(1)}% per position)`,
|
||||
adjustedSize
|
||||
};
|
||||
}
|
||||
|
||||
// Check max positions
|
||||
if (currentPositions.size >= this.limits.maxPositions && !currentPositions.has(symbol)) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Maximum number of positions (${this.limits.maxPositions}) reached`
|
||||
};
|
||||
}
|
||||
|
||||
// Check total exposure
|
||||
let totalExposure = proposedValue;
|
||||
for (const [sym, pos] of currentPositions) {
|
||||
if (sym !== symbol) {
|
||||
totalExposure += Math.abs(pos.value);
|
||||
}
|
||||
}
|
||||
|
||||
const totalExposurePct = totalExposure / this.accountBalance;
|
||||
if (totalExposurePct > this.limits.maxTotalExposurePct) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Total exposure would be ${(totalExposurePct * 100).toFixed(1)}% (max ${(this.limits.maxTotalExposurePct * 100).toFixed(1)}%)`
|
||||
};
|
||||
}
|
||||
|
||||
// Check daily loss limit
|
||||
const dailyLossPct = Math.abs(this.dailyPnl) / this.dailyStartBalance;
|
||||
if (this.dailyPnl < 0 && dailyLossPct >= this.limits.maxDailyLossPct) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Daily loss limit reached (${(dailyLossPct * 100).toFixed(1)}%)`
|
||||
};
|
||||
}
|
||||
|
||||
// Check consecutive losses
|
||||
if (this.consecutiveLosses >= this.limits.maxConsecutiveLosses) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Maximum consecutive losses (${this.limits.maxConsecutiveLosses}) reached`
|
||||
};
|
||||
}
|
||||
|
||||
// Check drawdown
|
||||
const currentDrawdownPct = (this.peakBalance - this.accountBalance) / this.peakBalance;
|
||||
if (currentDrawdownPct >= this.limits.maxDrawdownPct) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Maximum drawdown reached (${(currentDrawdownPct * 100).toFixed(1)}%)`
|
||||
};
|
||||
}
|
||||
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update metrics after a trade
|
||||
*/
|
||||
updateAfterTrade(pnl: number): void {
|
||||
this.dailyPnl += pnl;
|
||||
this.accountBalance += pnl;
|
||||
|
||||
if (pnl < 0) {
|
||||
this.consecutiveLosses++;
|
||||
} else if (pnl > 0) {
|
||||
this.consecutiveLosses = 0;
|
||||
}
|
||||
|
||||
if (this.accountBalance > this.peakBalance) {
|
||||
this.peakBalance = this.accountBalance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset daily metrics
|
||||
*/
|
||||
resetDaily(): void {
|
||||
// Record daily return
|
||||
const dailyReturn = (this.accountBalance - this.dailyStartBalance) / this.dailyStartBalance;
|
||||
this.dailyReturns.push(dailyReturn);
|
||||
|
||||
// Keep only recent returns
|
||||
if (this.dailyReturns.length > this.lookbackDays) {
|
||||
this.dailyReturns.shift();
|
||||
}
|
||||
|
||||
this.dailyPnl = 0;
|
||||
this.dailyStartBalance = this.accountBalance;
|
||||
|
||||
logger.info(`Daily reset - Balance: $${this.accountBalance.toFixed(2)}, Daily return: ${(dailyReturn * 100).toFixed(2)}%`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate current risk metrics
|
||||
*/
|
||||
getMetrics(currentPositions: Map<string, { quantity: number; value: number }>): RiskMetrics {
|
||||
let currentExposure = 0;
|
||||
for (const pos of currentPositions.values()) {
|
||||
currentExposure += Math.abs(pos.value);
|
||||
}
|
||||
|
||||
const currentDrawdown = this.peakBalance - this.accountBalance;
|
||||
const currentDrawdownPct = this.peakBalance > 0 ? currentDrawdown / this.peakBalance : 0;
|
||||
|
||||
return {
|
||||
currentExposure,
|
||||
currentExposurePct: currentExposure / this.accountBalance,
|
||||
dailyPnl: this.dailyPnl,
|
||||
dailyPnlPct: this.dailyPnl / this.dailyStartBalance,
|
||||
currentDrawdown,
|
||||
currentDrawdownPct,
|
||||
maxDrawdown: Math.max(currentDrawdown, 0),
|
||||
maxDrawdownPct: Math.max(currentDrawdownPct, 0),
|
||||
consecutiveLosses: this.consecutiveLosses,
|
||||
volatility: this.calculateVolatility(),
|
||||
sharpeRatio: this.calculateSharpeRatio(),
|
||||
var95: this.calculateVaR(0.95)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update risk limits
|
||||
*/
|
||||
updateLimits(newLimits: Partial<RiskLimits>): void {
|
||||
this.limits = { ...this.limits, ...newLimits };
|
||||
logger.info('Risk limits updated:', this.limits);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current limits
|
||||
*/
|
||||
getLimits(): Required<RiskLimits> {
|
||||
return { ...this.limits };
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate portfolio volatility
|
||||
*/
|
||||
private calculateVolatility(): number {
|
||||
if (this.dailyReturns.length < 2) return 0;
|
||||
|
||||
const mean = this.dailyReturns.reduce((sum, r) => sum + r, 0) / this.dailyReturns.length;
|
||||
const variance = this.dailyReturns.reduce((sum, r) => sum + Math.pow(r - mean, 2), 0) / (this.dailyReturns.length - 1);
|
||||
|
||||
return Math.sqrt(variance) * Math.sqrt(252); // Annualized
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Sharpe ratio
|
||||
*/
|
||||
private calculateSharpeRatio(riskFreeRate = 0.02): number {
|
||||
if (this.dailyReturns.length < 2) return 0;
|
||||
|
||||
const avgReturn = this.dailyReturns.reduce((sum, r) => sum + r, 0) / this.dailyReturns.length;
|
||||
const annualizedReturn = avgReturn * 252;
|
||||
const volatility = this.calculateVolatility();
|
||||
|
||||
if (volatility === 0) return 0;
|
||||
|
||||
return (annualizedReturn - riskFreeRate) / volatility;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Value at Risk
|
||||
*/
|
||||
private calculateVaR(confidence: number): number {
|
||||
if (this.dailyReturns.length < 5) return 0;
|
||||
|
||||
const sortedReturns = [...this.dailyReturns].sort((a, b) => a - b);
|
||||
const index = Math.floor((1 - confidence) * sortedReturns.length);
|
||||
|
||||
return Math.abs(sortedReturns[index] * this.accountBalance);
|
||||
}
|
||||
}
|
||||
469
apps/stock/orchestrator/src/strategies/signals/SignalManager.ts
Normal file
469
apps/stock/orchestrator/src/strategies/signals/SignalManager.ts
Normal file
|
|
@ -0,0 +1,469 @@
|
|||
import { getLogger } from '@stock-bot/logger';
|
||||
|
||||
const logger = getLogger('SignalManager');
|
||||
|
||||
export interface SignalRule {
|
||||
name: string;
|
||||
condition: (indicators: any) => boolean;
|
||||
weight: number;
|
||||
direction: 'buy' | 'sell' | 'both';
|
||||
}
|
||||
|
||||
export interface SignalFilter {
|
||||
name: string;
|
||||
filter: (signal: TradingSignal, context: any) => boolean;
|
||||
}
|
||||
|
||||
export interface TradingSignal {
|
||||
symbol: string;
|
||||
timestamp: number;
|
||||
direction: 'buy' | 'sell' | 'neutral';
|
||||
strength: number; // -1 to 1 (-1 = strong sell, 1 = strong buy)
|
||||
confidence: number; // 0 to 1
|
||||
rules: string[]; // Rules that triggered
|
||||
indicators: Record<string, number>;
|
||||
metadata?: any;
|
||||
}
|
||||
|
||||
export interface SignalAggregation {
|
||||
method: 'weighted' | 'majority' | 'unanimous' | 'threshold';
|
||||
threshold?: number; // For threshold method
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages trading signals and rules
|
||||
*/
|
||||
export class SignalManager {
|
||||
private rules: SignalRule[] = [];
|
||||
private filters: SignalFilter[] = [];
|
||||
private signalHistory: TradingSignal[] = [];
|
||||
private maxHistorySize = 1000;
|
||||
|
||||
constructor(
|
||||
private aggregation: SignalAggregation = { method: 'weighted' }
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Add a signal rule
|
||||
*/
|
||||
addRule(rule: SignalRule): void {
|
||||
this.rules.push(rule);
|
||||
logger.info(`Added signal rule: ${rule.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multiple rules
|
||||
*/
|
||||
addRules(rules: SignalRule[]): void {
|
||||
rules.forEach(rule => this.addRule(rule));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a signal filter
|
||||
*/
|
||||
addFilter(filter: SignalFilter): void {
|
||||
this.filters.push(filter);
|
||||
logger.info(`Added signal filter: ${filter.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a rule by name
|
||||
*/
|
||||
removeRule(name: string): void {
|
||||
this.rules = this.rules.filter(r => r.name !== name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate signal based on indicators
|
||||
*/
|
||||
generateSignal(
|
||||
symbol: string,
|
||||
timestamp: number,
|
||||
indicators: Record<string, number>,
|
||||
context: any = {}
|
||||
): TradingSignal | null {
|
||||
const triggeredRules: { rule: SignalRule; triggered: boolean }[] = [];
|
||||
|
||||
// Check each rule
|
||||
for (const rule of this.rules) {
|
||||
try {
|
||||
const triggered = rule.condition(indicators);
|
||||
if (triggered) {
|
||||
triggeredRules.push({ rule, triggered: true });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Error evaluating rule ${rule.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (triggeredRules.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Aggregate signals based on method
|
||||
const signal = this.aggregateSignals(symbol, timestamp, indicators, triggeredRules);
|
||||
|
||||
if (!signal) return null;
|
||||
|
||||
// Apply filters
|
||||
for (const filter of this.filters) {
|
||||
try {
|
||||
if (!filter.filter(signal, context)) {
|
||||
logger.debug(`Signal filtered by ${filter.name}`);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Error applying filter ${filter.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Store in history
|
||||
this.addToHistory(signal);
|
||||
|
||||
return signal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate multiple rule triggers into a single signal
|
||||
*/
|
||||
private aggregateSignals(
|
||||
symbol: string,
|
||||
timestamp: number,
|
||||
indicators: Record<string, number>,
|
||||
triggeredRules: { rule: SignalRule; triggered: boolean }[]
|
||||
): TradingSignal | null {
|
||||
let buyWeight = 0;
|
||||
let sellWeight = 0;
|
||||
let totalWeight = 0;
|
||||
const rules: string[] = [];
|
||||
|
||||
for (const { rule } of triggeredRules) {
|
||||
rules.push(rule.name);
|
||||
totalWeight += Math.abs(rule.weight);
|
||||
|
||||
if (rule.direction === 'buy' || rule.direction === 'both') {
|
||||
buyWeight += rule.weight;
|
||||
}
|
||||
if (rule.direction === 'sell' || rule.direction === 'both') {
|
||||
sellWeight += rule.weight;
|
||||
}
|
||||
}
|
||||
|
||||
let direction: 'buy' | 'sell' | 'neutral' = 'neutral';
|
||||
let strength = 0;
|
||||
let confidence = 0;
|
||||
|
||||
switch (this.aggregation.method) {
|
||||
case 'weighted':
|
||||
const netWeight = buyWeight - sellWeight;
|
||||
strength = totalWeight > 0 ? netWeight / totalWeight : 0;
|
||||
confidence = Math.min(triggeredRules.length / this.rules.length, 1);
|
||||
|
||||
if (Math.abs(strength) > 0.1) {
|
||||
direction = strength > 0 ? 'buy' : 'sell';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'majority':
|
||||
const buyCount = triggeredRules.filter(t =>
|
||||
t.rule.direction === 'buy' || t.rule.direction === 'both'
|
||||
).length;
|
||||
const sellCount = triggeredRules.filter(t =>
|
||||
t.rule.direction === 'sell' || t.rule.direction === 'both'
|
||||
).length;
|
||||
|
||||
if (buyCount > sellCount) {
|
||||
direction = 'buy';
|
||||
strength = buyCount / triggeredRules.length;
|
||||
} else if (sellCount > buyCount) {
|
||||
direction = 'sell';
|
||||
strength = -sellCount / triggeredRules.length;
|
||||
}
|
||||
confidence = triggeredRules.length / this.rules.length;
|
||||
break;
|
||||
|
||||
case 'unanimous':
|
||||
const allBuy = triggeredRules.every(t =>
|
||||
t.rule.direction === 'buy' || t.rule.direction === 'both'
|
||||
);
|
||||
const allSell = triggeredRules.every(t =>
|
||||
t.rule.direction === 'sell' || t.rule.direction === 'both'
|
||||
);
|
||||
|
||||
if (allBuy && triggeredRules.length >= 2) {
|
||||
direction = 'buy';
|
||||
strength = 1;
|
||||
confidence = 1;
|
||||
} else if (allSell && triggeredRules.length >= 2) {
|
||||
direction = 'sell';
|
||||
strength = -1;
|
||||
confidence = 1;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'threshold':
|
||||
const threshold = this.aggregation.threshold || 0.7;
|
||||
const avgWeight = totalWeight > 0 ? (buyWeight - sellWeight) / totalWeight : 0;
|
||||
|
||||
if (avgWeight >= threshold) {
|
||||
direction = 'buy';
|
||||
strength = avgWeight;
|
||||
confidence = triggeredRules.length / this.rules.length;
|
||||
} else if (avgWeight <= -threshold) {
|
||||
direction = 'sell';
|
||||
strength = avgWeight;
|
||||
confidence = triggeredRules.length / this.rules.length;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (direction === 'neutral') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
symbol,
|
||||
timestamp,
|
||||
direction,
|
||||
strength,
|
||||
confidence,
|
||||
rules,
|
||||
indicators
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent signals for a symbol
|
||||
*/
|
||||
getRecentSignals(symbol: string, count = 10): TradingSignal[] {
|
||||
return this.signalHistory
|
||||
.filter(s => s.symbol === symbol)
|
||||
.slice(-count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get signal statistics
|
||||
*/
|
||||
getSignalStats(symbol?: string) {
|
||||
const signals = symbol
|
||||
? this.signalHistory.filter(s => s.symbol === symbol)
|
||||
: this.signalHistory;
|
||||
|
||||
const buySignals = signals.filter(s => s.direction === 'buy');
|
||||
const sellSignals = signals.filter(s => s.direction === 'sell');
|
||||
|
||||
const avgBuyStrength = buySignals.length > 0
|
||||
? buySignals.reduce((sum, s) => sum + s.strength, 0) / buySignals.length
|
||||
: 0;
|
||||
|
||||
const avgSellStrength = sellSignals.length > 0
|
||||
? sellSignals.reduce((sum, s) => sum + Math.abs(s.strength), 0) / sellSignals.length
|
||||
: 0;
|
||||
|
||||
const avgConfidence = signals.length > 0
|
||||
? signals.reduce((sum, s) => sum + s.confidence, 0) / signals.length
|
||||
: 0;
|
||||
|
||||
return {
|
||||
totalSignals: signals.length,
|
||||
buySignals: buySignals.length,
|
||||
sellSignals: sellSignals.length,
|
||||
avgBuyStrength,
|
||||
avgSellStrength,
|
||||
avgConfidence,
|
||||
ruleHitRate: this.calculateRuleHitRate()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear signal history
|
||||
*/
|
||||
clearHistory(): void {
|
||||
this.signalHistory = [];
|
||||
}
|
||||
|
||||
private addToHistory(signal: TradingSignal): void {
|
||||
this.signalHistory.push(signal);
|
||||
|
||||
if (this.signalHistory.length > this.maxHistorySize) {
|
||||
this.signalHistory.shift();
|
||||
}
|
||||
}
|
||||
|
||||
private calculateRuleHitRate(): Record<string, number> {
|
||||
const ruleHits: Record<string, number> = {};
|
||||
|
||||
for (const signal of this.signalHistory) {
|
||||
for (const rule of signal.rules) {
|
||||
ruleHits[rule] = (ruleHits[rule] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
const hitRate: Record<string, number> = {};
|
||||
for (const [rule, hits] of Object.entries(ruleHits)) {
|
||||
hitRate[rule] = this.signalHistory.length > 0
|
||||
? hits / this.signalHistory.length
|
||||
: 0;
|
||||
}
|
||||
|
||||
return hitRate;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Common signal rules
|
||||
*/
|
||||
export const CommonRules = {
|
||||
// Moving Average Rules
|
||||
goldenCross: (fastMA: string, slowMA: string): SignalRule => ({
|
||||
name: `Golden Cross (${fastMA}/${slowMA})`,
|
||||
condition: (indicators) => {
|
||||
const fast = indicators[fastMA];
|
||||
const slow = indicators[slowMA];
|
||||
const prevFast = indicators[`${fastMA}_prev`];
|
||||
const prevSlow = indicators[`${slowMA}_prev`];
|
||||
return prevFast <= prevSlow && fast > slow;
|
||||
},
|
||||
weight: 1,
|
||||
direction: 'buy'
|
||||
}),
|
||||
|
||||
deathCross: (fastMA: string, slowMA: string): SignalRule => ({
|
||||
name: `Death Cross (${fastMA}/${slowMA})`,
|
||||
condition: (indicators) => {
|
||||
const fast = indicators[fastMA];
|
||||
const slow = indicators[slowMA];
|
||||
const prevFast = indicators[`${fastMA}_prev`];
|
||||
const prevSlow = indicators[`${slowMA}_prev`];
|
||||
return prevFast >= prevSlow && fast < slow;
|
||||
},
|
||||
weight: 1,
|
||||
direction: 'sell'
|
||||
}),
|
||||
|
||||
// RSI Rules
|
||||
rsiOversold: (threshold = 30): SignalRule => ({
|
||||
name: `RSI Oversold (<${threshold})`,
|
||||
condition: (indicators) => indicators.rsi < threshold,
|
||||
weight: 0.5,
|
||||
direction: 'buy'
|
||||
}),
|
||||
|
||||
rsiOverbought: (threshold = 70): SignalRule => ({
|
||||
name: `RSI Overbought (>${threshold})`,
|
||||
condition: (indicators) => indicators.rsi > threshold,
|
||||
weight: 0.5,
|
||||
direction: 'sell'
|
||||
}),
|
||||
|
||||
// MACD Rules
|
||||
macdBullishCross: (): SignalRule => ({
|
||||
name: 'MACD Bullish Cross',
|
||||
condition: (indicators) => {
|
||||
return indicators.macd_prev < indicators.macd_signal_prev &&
|
||||
indicators.macd > indicators.macd_signal;
|
||||
},
|
||||
weight: 0.8,
|
||||
direction: 'buy'
|
||||
}),
|
||||
|
||||
macdBearishCross: (): SignalRule => ({
|
||||
name: 'MACD Bearish Cross',
|
||||
condition: (indicators) => {
|
||||
return indicators.macd_prev > indicators.macd_signal_prev &&
|
||||
indicators.macd < indicators.macd_signal;
|
||||
},
|
||||
weight: 0.8,
|
||||
direction: 'sell'
|
||||
}),
|
||||
|
||||
// Bollinger Band Rules
|
||||
bollingerSqueeze: (threshold = 0.02): SignalRule => ({
|
||||
name: `Bollinger Squeeze (<${threshold})`,
|
||||
condition: (indicators) => {
|
||||
const bandwidth = (indicators.bb_upper - indicators.bb_lower) / indicators.bb_middle;
|
||||
return bandwidth < threshold;
|
||||
},
|
||||
weight: 0.3,
|
||||
direction: 'both'
|
||||
}),
|
||||
|
||||
priceAtLowerBand: (): SignalRule => ({
|
||||
name: 'Price at Lower Bollinger Band',
|
||||
condition: (indicators) => {
|
||||
const bbPercent = (indicators.price - indicators.bb_lower) /
|
||||
(indicators.bb_upper - indicators.bb_lower);
|
||||
return bbPercent < 0.05;
|
||||
},
|
||||
weight: 0.6,
|
||||
direction: 'buy'
|
||||
}),
|
||||
|
||||
priceAtUpperBand: (): SignalRule => ({
|
||||
name: 'Price at Upper Bollinger Band',
|
||||
condition: (indicators) => {
|
||||
const bbPercent = (indicators.price - indicators.bb_lower) /
|
||||
(indicators.bb_upper - indicators.bb_lower);
|
||||
return bbPercent > 0.95;
|
||||
},
|
||||
weight: 0.6,
|
||||
direction: 'sell'
|
||||
})
|
||||
};
|
||||
|
||||
/**
|
||||
* Common signal filters
|
||||
*/
|
||||
export const CommonFilters = {
|
||||
// Minimum signal strength
|
||||
minStrength: (threshold = 0.5): SignalFilter => ({
|
||||
name: `Min Strength (${threshold})`,
|
||||
filter: (signal) => Math.abs(signal.strength) >= threshold
|
||||
}),
|
||||
|
||||
// Minimum confidence
|
||||
minConfidence: (threshold = 0.3): SignalFilter => ({
|
||||
name: `Min Confidence (${threshold})`,
|
||||
filter: (signal) => signal.confidence >= threshold
|
||||
}),
|
||||
|
||||
// Time of day filter
|
||||
tradingHours: (startHour = 9.5, endHour = 16): SignalFilter => ({
|
||||
name: `Trading Hours (${startHour}-${endHour})`,
|
||||
filter: (signal) => {
|
||||
const date = new Date(signal.timestamp);
|
||||
const hour = date.getUTCHours() + date.getUTCMinutes() / 60;
|
||||
return hour >= startHour && hour < endHour;
|
||||
}
|
||||
}),
|
||||
|
||||
// Trend alignment
|
||||
trendAlignment: (trendIndicator = 'sma200'): SignalFilter => ({
|
||||
name: `Trend Alignment (${trendIndicator})`,
|
||||
filter: (signal) => {
|
||||
const trend = signal.indicators[trendIndicator];
|
||||
const price = signal.indicators.price;
|
||||
if (!trend || !price) return true;
|
||||
|
||||
// Buy signals only above trend, sell signals only below
|
||||
if (signal.direction === 'buy') {
|
||||
return price > trend;
|
||||
} else if (signal.direction === 'sell') {
|
||||
return price < trend;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}),
|
||||
|
||||
// Volume confirmation
|
||||
volumeConfirmation: (multiplier = 1.5): SignalFilter => ({
|
||||
name: `Volume Confirmation (${multiplier}x)`,
|
||||
filter: (signal) => {
|
||||
const volume = signal.indicators.volume;
|
||||
const avgVolume = signal.indicators.avg_volume;
|
||||
if (!volume || !avgVolume) return true;
|
||||
|
||||
return volume >= avgVolume * multiplier;
|
||||
}
|
||||
})
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue