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>
539 lines
15 KiB
TypeScript
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();
|