trading-platform-backend-v2/src/modules/trading/services/indicators.service.ts
rckrdmrd e45591a0ef feat: Initial commit - Trading Platform Backend
NestJS backend with:
- Authentication (JWT)
- WebSocket real-time support
- ML integration services
- Payments module
- User management

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 04:28:47 -06:00

539 lines
15 KiB
TypeScript

/**
* Technical Indicators Service
* Calculates common technical analysis indicators
*/
import { marketService } from './market.service';
import type { Interval } from './binance.service';
// ============================================================================
// Types
// ============================================================================
export interface OHLCV {
time: number;
open: number;
high: number;
low: number;
close: number;
volume: number;
}
export interface SMAResult {
time: number;
value: number;
}
export interface EMAResult {
time: number;
value: number;
}
export interface RSIResult {
time: number;
value: number;
}
export interface MACDResult {
time: number;
macd: number;
signal: number;
histogram: number;
}
export interface StochasticResult {
time: number;
k: number;
d: number;
}
export interface BollingerBandsResult {
time: number;
upper: number;
middle: number;
lower: number;
bandwidth: number;
}
export interface ATRResult {
time: number;
value: number;
}
export interface VWAPResult {
time: number;
value: number;
}
export interface IchimokuResult {
time: number;
tenkanSen: number;
kijunSen: number;
senkouSpanA: number;
senkouSpanB: number;
chikouSpan: number;
}
export interface IndicatorParams {
symbol: string;
interval: Interval;
period?: number;
limit?: number;
}
export interface MACDParams extends IndicatorParams {
fastPeriod?: number;
slowPeriod?: number;
signalPeriod?: number;
}
export interface BollingerParams extends IndicatorParams {
stdDev?: number;
}
export interface StochasticParams extends IndicatorParams {
kPeriod?: number;
dPeriod?: number;
smoothK?: number;
}
// ============================================================================
// Indicators Service
// ============================================================================
class IndicatorsService {
/**
* Convert klines to OHLCV format
* CandlestickData already has numeric values
*/
private klinesToOHLCV(klines: unknown[]): OHLCV[] {
return (klines as Record<string, unknown>[]).map((k) => ({
time: k.time as number,
open: k.open as number,
high: k.high as number,
low: k.low as number,
close: k.close as number,
volume: k.volume as number,
}));
}
/**
* Fetch OHLCV data for calculations
*/
private async getOHLCV(symbol: string, interval: Interval, limit: number): Promise<OHLCV[]> {
const klines = await marketService.getKlines(symbol, interval, { limit });
return this.klinesToOHLCV(klines);
}
// ==========================================================================
// Moving Averages
// ==========================================================================
/**
* Calculate Simple Moving Average (SMA)
*/
async getSMA(params: IndicatorParams): Promise<SMAResult[]> {
const { symbol, interval, period = 20, limit = 100 } = params;
const ohlcv = await this.getOHLCV(symbol, interval, limit + period);
return this.calculateSMA(ohlcv, period);
}
private calculateSMA(data: OHLCV[], period: number): SMAResult[] {
const result: SMAResult[] = [];
for (let i = period - 1; i < data.length; i++) {
let sum = 0;
for (let j = 0; j < period; j++) {
sum += data[i - j].close;
}
result.push({
time: data[i].time,
value: sum / period,
});
}
return result;
}
/**
* Calculate Exponential Moving Average (EMA)
*/
async getEMA(params: IndicatorParams): Promise<EMAResult[]> {
const { symbol, interval, period = 20, limit = 100 } = params;
const ohlcv = await this.getOHLCV(symbol, interval, limit + period);
return this.calculateEMA(ohlcv, period);
}
private calculateEMA(data: OHLCV[], period: number): EMAResult[] {
const result: EMAResult[] = [];
const multiplier = 2 / (period + 1);
// Start with SMA for first EMA value
let sum = 0;
for (let i = 0; i < period; i++) {
sum += data[i].close;
}
let ema = sum / period;
result.push({ time: data[period - 1].time, value: ema });
// Calculate EMA for remaining data
for (let i = period; i < data.length; i++) {
ema = (data[i].close - ema) * multiplier + ema;
result.push({ time: data[i].time, value: ema });
}
return result;
}
// ==========================================================================
// Oscillators
// ==========================================================================
/**
* Calculate Relative Strength Index (RSI)
*/
async getRSI(params: IndicatorParams): Promise<RSIResult[]> {
const { symbol, interval, period = 14, limit = 100 } = params;
const ohlcv = await this.getOHLCV(symbol, interval, limit + period + 1);
return this.calculateRSI(ohlcv, period);
}
private calculateRSI(data: OHLCV[], period: number): RSIResult[] {
const result: RSIResult[] = [];
const changes: number[] = [];
// Calculate price changes
for (let i = 1; i < data.length; i++) {
changes.push(data[i].close - data[i - 1].close);
}
// Calculate initial average gains and losses
let avgGain = 0;
let avgLoss = 0;
for (let i = 0; i < period; i++) {
if (changes[i] > 0) {
avgGain += changes[i];
} else {
avgLoss += Math.abs(changes[i]);
}
}
avgGain /= period;
avgLoss /= period;
// First RSI value
let rs = avgLoss === 0 ? 100 : avgGain / avgLoss;
let rsi = 100 - 100 / (1 + rs);
result.push({ time: data[period].time, value: rsi });
// Calculate RSI for remaining data using Wilder's smoothing
for (let i = period; i < changes.length; i++) {
const change = changes[i];
const gain = change > 0 ? change : 0;
const loss = change < 0 ? Math.abs(change) : 0;
avgGain = (avgGain * (period - 1) + gain) / period;
avgLoss = (avgLoss * (period - 1) + loss) / period;
rs = avgLoss === 0 ? 100 : avgGain / avgLoss;
rsi = 100 - 100 / (1 + rs);
result.push({ time: data[i + 1].time, value: rsi });
}
return result;
}
/**
* Calculate MACD (Moving Average Convergence Divergence)
*/
async getMACD(params: MACDParams): Promise<MACDResult[]> {
const {
symbol,
interval,
fastPeriod = 12,
slowPeriod = 26,
signalPeriod = 9,
limit = 100,
} = params;
const ohlcv = await this.getOHLCV(symbol, interval, limit + slowPeriod + signalPeriod);
return this.calculateMACD(ohlcv, fastPeriod, slowPeriod, signalPeriod);
}
private calculateMACD(
data: OHLCV[],
fastPeriod: number,
slowPeriod: number,
signalPeriod: number
): MACDResult[] {
const result: MACDResult[] = [];
// Calculate EMAs
const fastEMA = this.calculateEMAFromClose(data.map((d) => d.close), fastPeriod);
const slowEMA = this.calculateEMAFromClose(data.map((d) => d.close), slowPeriod);
// Calculate MACD line
const macdLine: number[] = [];
const startIndex = slowPeriod - 1;
for (let i = startIndex; i < data.length; i++) {
const fastIndex = i - (slowPeriod - fastPeriod);
if (fastIndex >= 0 && fastIndex < fastEMA.length) {
macdLine.push(fastEMA[fastIndex] - slowEMA[i - startIndex]);
}
}
// Calculate signal line (EMA of MACD)
const signalLine = this.calculateEMAFromClose(macdLine, signalPeriod);
// Build result
for (let i = signalPeriod - 1; i < macdLine.length; i++) {
const dataIndex = startIndex + i;
const macd = macdLine[i];
const signal = signalLine[i - (signalPeriod - 1)];
result.push({
time: data[dataIndex].time,
macd,
signal,
histogram: macd - signal,
});
}
return result;
}
private calculateEMAFromClose(closes: number[], period: number): number[] {
const result: number[] = [];
const multiplier = 2 / (period + 1);
let sum = 0;
for (let i = 0; i < period; i++) {
sum += closes[i];
}
let ema = sum / period;
result.push(ema);
for (let i = period; i < closes.length; i++) {
ema = (closes[i] - ema) * multiplier + ema;
result.push(ema);
}
return result;
}
/**
* Calculate Stochastic Oscillator
*/
async getStochastic(params: StochasticParams): Promise<StochasticResult[]> {
const { symbol, interval, kPeriod = 14, dPeriod = 3, smoothK = 3, limit = 100 } = params;
const ohlcv = await this.getOHLCV(symbol, interval, limit + kPeriod + smoothK + dPeriod);
return this.calculateStochastic(ohlcv, kPeriod, dPeriod, smoothK);
}
private calculateStochastic(
data: OHLCV[],
kPeriod: number,
dPeriod: number,
smoothK: number
): StochasticResult[] {
const result: StochasticResult[] = [];
const rawK: number[] = [];
// Calculate raw %K
for (let i = kPeriod - 1; i < data.length; i++) {
let highest = -Infinity;
let lowest = Infinity;
for (let j = 0; j < kPeriod; j++) {
highest = Math.max(highest, data[i - j].high);
lowest = Math.min(lowest, data[i - j].low);
}
const k = highest === lowest ? 50 : ((data[i].close - lowest) / (highest - lowest)) * 100;
rawK.push(k);
}
// Smooth %K
const smoothedK = this.calculateSMAFromValues(rawK, smoothK);
// Calculate %D (SMA of smoothed %K)
const percentD = this.calculateSMAFromValues(smoothedK, dPeriod);
// Build result
const resultStart = kPeriod + smoothK + dPeriod - 3;
for (let i = 0; i < percentD.length; i++) {
const dataIndex = resultStart + i;
result.push({
time: data[dataIndex].time,
k: smoothedK[i + dPeriod - 1],
d: percentD[i],
});
}
return result;
}
private calculateSMAFromValues(values: number[], period: number): number[] {
const result: number[] = [];
for (let i = period - 1; i < values.length; i++) {
let sum = 0;
for (let j = 0; j < period; j++) {
sum += values[i - j];
}
result.push(sum / period);
}
return result;
}
// ==========================================================================
// Volatility Indicators
// ==========================================================================
/**
* Calculate Bollinger Bands
*/
async getBollingerBands(params: BollingerParams): Promise<BollingerBandsResult[]> {
const { symbol, interval, period = 20, stdDev = 2, limit = 100 } = params;
const ohlcv = await this.getOHLCV(symbol, interval, limit + period);
return this.calculateBollingerBands(ohlcv, period, stdDev);
}
private calculateBollingerBands(data: OHLCV[], period: number, stdDev: number): BollingerBandsResult[] {
const result: BollingerBandsResult[] = [];
for (let i = period - 1; i < data.length; i++) {
const closes: number[] = [];
for (let j = 0; j < period; j++) {
closes.push(data[i - j].close);
}
// Calculate SMA (middle band)
const middle = closes.reduce((a, b) => a + b, 0) / period;
// Calculate standard deviation
const squaredDiffs = closes.map((c) => Math.pow(c - middle, 2));
const variance = squaredDiffs.reduce((a, b) => a + b, 0) / period;
const sd = Math.sqrt(variance);
// Calculate bands
const upper = middle + stdDev * sd;
const lower = middle - stdDev * sd;
const bandwidth = ((upper - lower) / middle) * 100;
result.push({
time: data[i].time,
upper,
middle,
lower,
bandwidth,
});
}
return result;
}
/**
* Calculate Average True Range (ATR)
*/
async getATR(params: IndicatorParams): Promise<ATRResult[]> {
const { symbol, interval, period = 14, limit = 100 } = params;
const ohlcv = await this.getOHLCV(symbol, interval, limit + period + 1);
return this.calculateATR(ohlcv, period);
}
private calculateATR(data: OHLCV[], period: number): ATRResult[] {
const result: ATRResult[] = [];
const trueRanges: number[] = [];
// Calculate True Range
for (let i = 1; i < data.length; i++) {
const high = data[i].high;
const low = data[i].low;
const prevClose = data[i - 1].close;
const tr = Math.max(high - low, Math.abs(high - prevClose), Math.abs(low - prevClose));
trueRanges.push(tr);
}
// Calculate initial ATR (simple average)
let atr = 0;
for (let i = 0; i < period; i++) {
atr += trueRanges[i];
}
atr /= period;
result.push({ time: data[period].time, value: atr });
// Calculate ATR using Wilder's smoothing
for (let i = period; i < trueRanges.length; i++) {
atr = (atr * (period - 1) + trueRanges[i]) / period;
result.push({ time: data[i + 1].time, value: atr });
}
return result;
}
// ==========================================================================
// Volume Indicators
// ==========================================================================
/**
* Calculate Volume Weighted Average Price (VWAP)
*/
async getVWAP(params: IndicatorParams): Promise<VWAPResult[]> {
const { symbol, interval, limit = 100 } = params;
const ohlcv = await this.getOHLCV(symbol, interval, limit);
return this.calculateVWAP(ohlcv);
}
private calculateVWAP(data: OHLCV[]): VWAPResult[] {
const result: VWAPResult[] = [];
let cumulativeTPV = 0;
let cumulativeVolume = 0;
for (const candle of data) {
const typicalPrice = (candle.high + candle.low + candle.close) / 3;
cumulativeTPV += typicalPrice * candle.volume;
cumulativeVolume += candle.volume;
const vwap = cumulativeVolume > 0 ? cumulativeTPV / cumulativeVolume : typicalPrice;
result.push({ time: candle.time, value: vwap });
}
return result;
}
// ==========================================================================
// All-in-One
// ==========================================================================
/**
* Get multiple indicators for a symbol
*/
async getAllIndicators(
symbol: string,
interval: Interval,
limit: number = 100
): Promise<{
sma20: SMAResult[];
sma50: SMAResult[];
ema12: EMAResult[];
ema26: EMAResult[];
rsi: RSIResult[];
macd: MACDResult[];
bollinger: BollingerBandsResult[];
atr: ATRResult[];
}> {
// Fetch all data needed (max period is 50 for SMA + limit)
const ohlcv = await this.getOHLCV(symbol, interval, limit + 60);
const [sma20, sma50, ema12, ema26, rsi, macd, bollinger, atr] = await Promise.all([
Promise.resolve(this.calculateSMA(ohlcv, 20)),
Promise.resolve(this.calculateSMA(ohlcv, 50)),
Promise.resolve(this.calculateEMA(ohlcv, 12)),
Promise.resolve(this.calculateEMA(ohlcv, 26)),
Promise.resolve(this.calculateRSI(ohlcv, 14)),
Promise.resolve(this.calculateMACD(ohlcv, 12, 26, 9)),
Promise.resolve(this.calculateBollingerBands(ohlcv, 20, 2)),
Promise.resolve(this.calculateATR(ohlcv, 14)),
]);
return { sma20, sma50, ema12, ema26, rsi, macd, bollinger, atr };
}
}
export const indicatorsService = new IndicatorsService();