110 lines
3.3 KiB
TypeScript
110 lines
3.3 KiB
TypeScript
import logger from '../utils/logger.js';
|
|
|
|
export enum SignalType {
|
|
BUY = 'BUY',
|
|
SELL = 'SELL',
|
|
NONE = 'NONE'
|
|
}
|
|
|
|
export interface StrategyResult {
|
|
signal: SignalType;
|
|
ema: number;
|
|
rsi: number;
|
|
changed: boolean;
|
|
}
|
|
|
|
export class DirectionTracker {
|
|
private lastSignal: SignalType = SignalType.NONE;
|
|
|
|
/**
|
|
* Advanced Strategy: EMA + RSI
|
|
* BUY: Price > EMA-20 AND RSI < 70 (not overbought)
|
|
* SELL: Price < EMA-20 AND RSI > 30 (not oversold)
|
|
*
|
|
* Includes a state machine to prevent duplicate signals.
|
|
*/
|
|
public calculateDirection(candles: any[]): StrategyResult {
|
|
// Need enough data for RSI (14) and EMA (20)
|
|
if (candles.length < 20) {
|
|
return { signal: SignalType.NONE, ema: 0, rsi: 0, changed: false };
|
|
}
|
|
|
|
const latestCandle = candles[candles.length - 1];
|
|
const latestClose = latestCandle.close;
|
|
|
|
// 1. Calculate EMA-20
|
|
const ema20 = this.calculateEMA(candles.map(c => c.close), 20);
|
|
|
|
// 2. Calculate RSI-14
|
|
const rsi14 = this.calculateRSI(candles.map(c => c.close), 14);
|
|
|
|
// 3. Signal Logic
|
|
let currentSignal = SignalType.NONE;
|
|
|
|
if (latestClose > ema20 && rsi14 < 70) {
|
|
currentSignal = SignalType.BUY;
|
|
} else if (latestClose < ema20 && rsi14 > 30) {
|
|
currentSignal = SignalType.SELL;
|
|
}
|
|
|
|
// 4. State Machine (Check if signal changed to avoid duplicates)
|
|
const changed = currentSignal !== SignalType.NONE && currentSignal !== this.lastSignal;
|
|
|
|
if (changed) {
|
|
logger.info(`🚨 New Strategy Signal: ${currentSignal} (EMA: ${ema20.toFixed(2)}, RSI: ${rsi14.toFixed(2)})`);
|
|
this.lastSignal = currentSignal;
|
|
}
|
|
|
|
return {
|
|
signal: currentSignal,
|
|
ema: ema20,
|
|
rsi: rsi14,
|
|
changed
|
|
};
|
|
}
|
|
|
|
private calculateEMA(data: number[], period: number): number {
|
|
if (data.length === 0) return 0;
|
|
const k = 2 / (period + 1);
|
|
let ema = data[0] || 0;
|
|
for (let i = 1; i < data.length; i++) {
|
|
const current = data[i] || 0;
|
|
ema = current * k + ema * (1 - k);
|
|
}
|
|
return ema;
|
|
}
|
|
|
|
private calculateRSI(data: number[], period: number): number {
|
|
if (data.length <= period) return 50; // Default if not enough data
|
|
|
|
let gains = 0;
|
|
let losses = 0;
|
|
|
|
// First average
|
|
for (let i = 1; i <= period; i++) {
|
|
const diff = data[i] - data[i - 1];
|
|
if (diff >= 0) gains += diff;
|
|
else losses -= diff;
|
|
}
|
|
|
|
let avgGain = gains / period;
|
|
let avgLoss = losses / period;
|
|
|
|
// Smoothing
|
|
for (let i = period + 1; i < data.length; i++) {
|
|
const diff = data[i] - data[i - 1];
|
|
if (diff >= 0) {
|
|
avgGain = (avgGain * (period - 1) + diff) / period;
|
|
avgLoss = (avgLoss * (period - 1)) / period;
|
|
} else {
|
|
avgGain = (avgGain * (period - 1)) / period;
|
|
avgLoss = (avgLoss * (period - 1) - diff) / period;
|
|
}
|
|
}
|
|
|
|
if (avgLoss === 0) return 100;
|
|
const rs = avgGain / avgLoss;
|
|
return 100 - (100 / (1 + rs));
|
|
}
|
|
}
|