import { RSI, MACD, EMA, BollingerBands, Stochastic } from 'technicalindicators';
const TA_THRESHOLDS = {
RSI_OVERSOLD: 35,
RSI_OVERBOUGHT: 65,
STOCH_OVERSOLD: 20,
STOCH_OVERBOUGHT: 80,
BOLLINGER_OVERSOLD: 20,
BOLLINGER_OVERBOUGHT: 80,
MIN_CANDLES: 30
};
export function calculateRSI(closes, period = 14) {
if (closes.length < period + 1) {
return { value: null, signal: 'neutral', reason: 'Insufficient data' };
}
const values = closes.map(c => parseFloat(c));
const rsiValues = RSI.calculate({ values, period });
const current = rsiValues[rsiValues.length - 1];
if (current === undefined) {
return { value: null, signal: 'neutral', reason: 'Calculation failed' };
}
let signal = 'neutral';
let reason = `RSI at ${current.toFixed(2)}`;
if (current < TA_THRESHOLDS.RSI_OVERSOLD) {
signal = 'buy';
reason = `RSI oversold at ${current.toFixed(2)}`;
} else if (current > TA_THRESHOLDS.RSI_OVERBOUGHT) {
signal = 'sell';
reason = `RSI overbought at ${current.toFixed(2)}`;
}
return { value: current.toFixed(2), signal, reason };
}
export function calculateMACD(closes) {
if (closes.length < 35) {
return { value: null, signal: 'neutral', reason: 'Insufficient data' };
}
const values = closes.map(c => parseFloat(c));
const macdValues = MACD.calculate({
values,
fastPeriod: 12,
slowPeriod: 26,
signalPeriod: 9,
SimpleMAOscillator: false,
SimpleMASignal: false
});
const current = macdValues[macdValues.length - 1];
const previous = macdValues[macdValues.length - 2];
if (!current || current.MACD === undefined) {
return { value: null, signal: 'neutral', reason: 'Calculation failed' };
}
let signal = 'neutral';
let reason = `MACD: ${current.MACD.toFixed(4)}`;
if (previous) {
const prevDiff = previous.MACD - previous.signal;
const currDiff = current.MACD - current.signal;
if (prevDiff < 0 && currDiff > 0) {
signal = 'buy';
reason = 'MACD bullish crossover';
} else if (prevDiff > 0 && currDiff < 0) {
signal = 'sell';
reason = 'MACD bearish crossover';
}
}
return {
value: {
macd: current.MACD.toFixed(4),
signal: current.signal.toFixed(4),
histogram: current.histogram.toFixed(4)
},
signal,
reason
};
}
export function calculateBollinger(closes, period = 20) {
if (closes.length < period) {
return { value: null, signal: 'neutral', reason: 'Insufficient data' };
}
const values = closes.map(c => parseFloat(c));
const bbValues = BollingerBands.calculate({
values,
period,
stdDev: 2
});
const current = bbValues[bbValues.length - 1];
const price = parseFloat(closes[closes.length - 1]);
if (!current) {
return { value: null, signal: 'neutral', reason: 'Calculation failed' };
}
const bandwidth = ((current.upper - current.lower) / current.middle) * 100;
const position = ((price - current.lower) / (current.upper - current.lower)) * 100;
let signal = 'neutral';
let reason = `Price at ${position.toFixed(1)}% of band`;
if (position < TA_THRESHOLDS.BOLLINGER_OVERSOLD) {
signal = 'buy';
reason = 'Price near lower band (oversold)';
} else if (position > TA_THRESHOLDS.BOLLINGER_OVERBOUGHT) {
signal = 'sell';
reason = 'Price near upper band (overbought)';
}
return {
value: {
upper: current.upper.toFixed(4),
middle: current.middle.toFixed(4),
lower: current.lower.toFixed(4),
bandwidth: bandwidth.toFixed(2),
position: position.toFixed(1)
},
signal,
reason
};
}
export function calculateStochastic(highs, lows, closes) {
if (closes.length < 14) {
return { value: null, signal: 'neutral', reason: 'Insufficient data' };
}
const high = highs.map(h => parseFloat(h));
const low = lows.map(l => parseFloat(l));
const close = closes.map(c => parseFloat(c));
const stochValues = Stochastic.calculate({
high,
low,
close,
period: 14,
signalPeriod: 3
});
const current = stochValues[stochValues.length - 1];
if (!current) {
return { value: null, signal: 'neutral', reason: 'Calculation failed' };
}
let signal = 'neutral';
let reason = `Stochastic at ${current.k.toFixed(2)}`;
if (current.k < TA_THRESHOLDS.STOCH_OVERSOLD) {
signal = 'buy';
reason = `Stochastic oversold at ${current.k.toFixed(2)}`;
} else if (current.k > TA_THRESHOLDS.STOCH_OVERBOUGHT) {
signal = 'sell';
reason = `Stochastic overbought at ${current.k.toFixed(2)}`;
}
return {
value: { k: current.k.toFixed(2), d: current.d.toFixed(2) },
signal,
reason
};
}
export function calculateEMA(closes) {
if (closes.length < 21) {
return { value: null, signal: 'neutral', reason: 'Insufficient data' };
}
const values = closes.map(c => parseFloat(c));
const ema9 = EMA.calculate({ values, period: 9 });
const ema21 = EMA.calculate({ values, period: 21 });
const current9 = ema9[ema9.length - 1];
const current21 = ema21[ema21.length - 1];
const previous9 = ema9[ema9.length - 2];
const previous21 = ema21[ema21.length - 2];
if (!current9 || !current21) {
return { value: null, signal: 'neutral', reason: 'Calculation failed' };
}
let signal = 'neutral';
let reason = `EMA9: ${current9.toFixed(2)}, EMA21: ${current21.toFixed(2)}`;
if (previous9 < previous21 && current9 > current21) {
signal = 'buy';
reason = 'EMA9 crossed above EMA21 (bullish)';
} else if (previous9 > previous21 && current9 < current21) {
signal = 'sell';
reason = 'EMA9 crossed below EMA21 (bearish)';
} else if (current9 > current21) {
signal = 'buy';
reason = 'EMA9 above EMA21 (uptrend)';
} else {
signal = 'sell';
reason = 'EMA9 below EMA21 (downtrend)';
}
return {
value: { ema9: current9.toFixed(4), ema21: current21.toFixed(4) },
signal,
reason
};
}
export function analyzeTechnicals(ohlcv) {
const closes = ohlcv.map(c => c.close);
const highs = ohlcv.map(c => c.high);
const lows = ohlcv.map(c => c.low);
const rsi = calculateRSI(closes);
const macd = calculateMACD(closes);
const ema = calculateEMA(closes);
const bollinger = calculateBollinger(closes);
const stochastic = calculateStochastic(highs, lows, closes);
const signals = [rsi.signal, macd.signal, ema.signal, bollinger.signal, stochastic.signal];
const buySignals = signals.filter(s => s === 'buy').length;
const sellSignals = signals.filter(s => s === 'sell').length;
let action = 'hold';
let confidence = 0;
let reason = 'Mixed signals';
if (buySignals >= 3) {
action = 'buy';
confidence = (buySignals / 5) * 100;
reason = `${buySignals}/5 bullish indicators`;
} else if (sellSignals >= 3) {
action = 'sell';
confidence = (sellSignals / 5) * 100;
reason = `${sellSignals}/5 bearish indicators`;
}
return {
recommendation: { action, confidence: Math.round(confidence), reason },
indicators: { rsi, macd, ema, bollinger, stochastic },
currentPrice: closes[closes.length - 1]
};
}
export { TA_THRESHOLDS };