stock-bot/apps/stock/orchestrator/tests/indicators.test.ts
2025-07-03 16:54:43 -04:00

195 lines
No EOL
7.4 KiB
TypeScript

import { TechnicalIndicators, IncrementalSMA, IncrementalEMA, IncrementalRSI } from '@stock-bot/core';
import { TechnicalAnalysis, IncrementalIndicators, SignalGenerator } from '../src/indicators/TechnicalAnalysis';
describe('Technical Analysis Library', () => {
let ta: TechnicalAnalysis;
let indicators: TechnicalIndicators;
beforeEach(() => {
ta = new TechnicalAnalysis();
indicators = new TechnicalIndicators();
});
describe('Simple Moving Average', () => {
it('should calculate SMA correctly', () => {
const values = [10, 12, 13, 14, 15, 16, 17, 18, 19, 20];
const sma = ta.sma(values, 5);
expect(sma).toHaveLength(6); // 10 values - 5 period + 1
expect(sma[0]).toBeCloseTo(12.8); // (10+12+13+14+15)/5
expect(sma[5]).toBeCloseTo(18); // (16+17+18+19+20)/5
});
it('should handle incremental SMA updates', () => {
const incSMA = new IncrementalSMA(3);
expect(incSMA.update(10)).toBeNull();
expect(incSMA.update(12)).toBeNull();
expect(incSMA.update(14)).toBeCloseTo(12); // (10+12+14)/3
expect(incSMA.update(16)).toBeCloseTo(14); // (12+14+16)/3
expect(incSMA.current()).toBeCloseTo(14);
});
});
describe('Exponential Moving Average', () => {
it('should calculate EMA correctly', () => {
const values = [10, 12, 13, 14, 15, 16, 17, 18, 19, 20];
const ema = ta.ema(values, 5);
expect(ema).toHaveLength(6);
expect(ema[0]).toBeGreaterThan(0);
expect(ema[ema.length - 1]).toBeGreaterThan(ema[0]);
});
});
describe('RSI', () => {
it('should calculate RSI correctly', () => {
const values = [
44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.42,
45.84, 46.08, 45.89, 46.03, 45.61, 46.28, 46.28, 46.00,
46.03, 46.41, 46.22, 45.64
];
const rsi = ta.rsi(values, 14);
expect(rsi).toHaveLength(7); // 20 values - 14 period + 1
expect(rsi[rsi.length - 1]).toBeGreaterThan(0);
expect(rsi[rsi.length - 1]).toBeLessThan(100);
});
it('should identify overbought/oversold conditions', () => {
// Trending up values should give high RSI
const uptrend = Array.from({ length: 20 }, (_, i) => 100 + i);
const rsiUp = ta.rsi(uptrend, 14);
expect(rsiUp[rsiUp.length - 1]).toBeGreaterThan(70);
// Trending down values should give low RSI
const downtrend = Array.from({ length: 20 }, (_, i) => 100 - i);
const rsiDown = ta.rsi(downtrend, 14);
expect(rsiDown[rsiDown.length - 1]).toBeLessThan(30);
});
});
describe('MACD', () => {
it('should calculate MACD components correctly', () => {
const values = Array.from({ length: 50 }, (_, i) => 100 + Math.sin(i * 0.1) * 10);
const macd = ta.macd(values);
expect(macd.macd).toHaveLength(39); // 50 - 26 + 1 - 9 + 1
expect(macd.signal).toHaveLength(39);
expect(macd.histogram).toHaveLength(39);
// Histogram should be the difference between MACD and signal
expect(macd.histogram[0]).toBeCloseTo(macd.macd[0] - macd.signal[0]);
});
});
describe('Bollinger Bands', () => {
it('should calculate bands correctly', () => {
const values = Array.from({ length: 30 }, (_, i) => 100 + Math.random() * 10);
const bb = ta.bollingerBands(values, 20, 2);
expect(bb.middle).toHaveLength(11); // 30 - 20 + 1
expect(bb.upper).toHaveLength(11);
expect(bb.lower).toHaveLength(11);
// Upper should be above middle, lower should be below
for (let i = 0; i < bb.middle.length; i++) {
expect(bb.upper[i]).toBeGreaterThan(bb.middle[i]);
expect(bb.lower[i]).toBeLessThan(bb.middle[i]);
}
});
});
describe('ATR', () => {
it('should calculate ATR correctly', () => {
const high = [48.70, 48.72, 48.90, 48.87, 48.82, 49.05, 49.20, 49.35, 49.92, 50.19];
const low = [47.79, 48.14, 48.39, 48.37, 48.24, 48.64, 48.94, 48.86, 49.50, 49.87];
const close = [48.16, 48.61, 48.75, 48.63, 48.74, 49.03, 49.07, 49.32, 49.91, 50.13];
const atr = ta.atr(high, low, close, 5);
expect(atr).toHaveLength(5); // 10 - 5 - 1 + 1
expect(atr.every(v => v > 0)).toBe(true);
});
});
describe('Stochastic', () => {
it('should calculate Stochastic correctly', () => {
const high = Array.from({ length: 20 }, () => Math.random() * 10 + 100);
const low = Array.from({ length: 20 }, (_, i) => high[i] - Math.random() * 5);
const close = Array.from({ length: 20 }, (_, i) => (high[i] + low[i]) / 2);
const stoch = ta.stochastic(high, low, close, 14, 3, 3);
expect(stoch.k.length).toBeGreaterThan(0);
expect(stoch.d.length).toBeGreaterThan(0);
// %K and %D should be between 0 and 100
expect(stoch.k.every(v => v >= 0 && v <= 100)).toBe(true);
expect(stoch.d.every(v => v >= 0 && v <= 100)).toBe(true);
});
});
describe('Signal Generator', () => {
it('should generate trading signals based on indicators', () => {
const generator = new SignalGenerator();
// Create synthetic price data
const prices = {
close: Array.from({ length: 50 }, (_, i) => 100 + Math.sin(i * 0.2) * 10),
high: Array.from({ length: 50 }, (_, i) => 102 + Math.sin(i * 0.2) * 10),
low: Array.from({ length: 50 }, (_, i) => 98 + Math.sin(i * 0.2) * 10),
volume: Array.from({ length: 50 }, () => 1000000)
};
const signal = generator.generateSignals('TEST', prices, Date.now());
expect(signal.symbol).toBe('TEST');
expect(['BUY', 'SELL', 'HOLD']).toContain(signal.action);
expect(signal.strength).toBeGreaterThanOrEqual(0);
expect(signal.strength).toBeLessThanOrEqual(1);
expect(signal.indicators).toBeDefined();
expect(signal.reason).toBeDefined();
});
});
describe('Incremental Indicators Manager', () => {
it('should manage multiple incremental indicators', () => {
const manager = new IncrementalIndicators();
manager.createSMA('fast', 10);
manager.createSMA('slow', 20);
manager.createRSI('rsi', 14);
// Update all indicators with same value
for (let i = 0; i < 25; i++) {
const value = 100 + i;
manager.update('fast', value);
manager.update('slow', value);
manager.update('rsi', value);
}
expect(manager.current('fast')).toBeDefined();
expect(manager.current('slow')).toBeDefined();
expect(manager.current('rsi')).toBeDefined();
// RSI should be high for uptrending values
const rsiValue = manager.current('rsi');
expect(rsiValue).toBeGreaterThan(70);
});
});
describe('Crossover Detection', () => {
it('should detect crossovers correctly', () => {
const series1 = [10, 11, 12, 13, 14];
const series2 = [12, 12, 12, 12, 12];
expect(TechnicalAnalysis.crossover(series1, series2)).toBe(true);
expect(TechnicalAnalysis.crossunder(series2, series1)).toBe(true);
const series3 = [15, 14, 13, 12, 11];
expect(TechnicalAnalysis.crossunder(series3, series2)).toBe(true);
expect(TechnicalAnalysis.crossover(series2, series3)).toBe(true);
});
});
});