stock-bot/libs/utils/src/calculations/technical-indicators.ts

1322 lines
33 KiB
TypeScript

/**
* Technical Indicators
* Comprehensive set of technical analysis indicators
*/
import type { OHLCV } from '@stock-bot/types';
/**
* Simple Moving Average
*/
export function sma(values: number[], period: number): number[] {
if (period > values.length) {
return [];
}
const result: number[] = [];
for (let i = period - 1; i < values.length; i++) {
const sum = values.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0);
result.push(sum / period);
}
return result;
}
/**
* Exponential Moving Average
*/
export function ema(values: number[], period: number): number[] {
if (period > values.length) {
return [];
}
const result: number[] = [];
const multiplier = 2 / (period + 1);
// Start with SMA for first value
let ema = values.slice(0, period).reduce((a, b) => a + b, 0) / period;
result.push(ema);
for (let i = period; i < values.length; i++) {
const value = values[i];
if (value !== undefined) {
ema = value * multiplier + ema * (1 - multiplier);
result.push(ema);
}
}
return result;
}
/**
* Relative Strength Index (RSI)
*/
export function rsi(prices: number[], period: number = 14): number[] {
if (period >= prices.length) {
return [];
}
const gains: number[] = [];
const losses: number[] = [];
// Calculate gains and losses
for (let i = 1; i < prices.length; i++) {
const current = prices[i];
const previous = prices[i - 1];
if (current !== undefined && previous !== undefined) {
const change = current - previous;
gains.push(change > 0 ? change : 0);
losses.push(change < 0 ? Math.abs(change) : 0);
}
}
const result: number[] = [];
// Calculate RSI
for (let i = period - 1; i < gains.length; i++) {
const avgGain = gains.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0) / period;
const avgLoss = losses.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0) / period;
if (avgLoss === 0) {
result.push(100);
} else {
const rs = avgGain / avgLoss;
const rsiValue = 100 - 100 / (1 + rs);
result.push(rsiValue);
}
}
return result;
}
/**
* Moving Average Convergence Divergence (MACD)
*/
export function macd(
prices: number[],
fastPeriod: number = 12,
slowPeriod: number = 26,
signalPeriod: number = 9
): { macd: number[]; signal: number[]; histogram: number[] } {
const fastEMA = ema(prices, fastPeriod);
const slowEMA = ema(prices, slowPeriod);
const macdLine: number[] = [];
const startIndex = slowPeriod - fastPeriod;
for (let i = 0; i < fastEMA.length - startIndex; i++) {
const fastValue = fastEMA[i + startIndex];
const slowValue = slowEMA[i];
if (fastValue !== undefined && slowValue !== undefined) {
macdLine.push(fastValue - slowValue);
}
}
const signalLine = ema(macdLine, signalPeriod);
const histogram: number[] = [];
const signalStartIndex = signalPeriod - 1;
for (let i = 0; i < signalLine.length; i++) {
const macdValue = macdLine[i + signalStartIndex];
const signalValue = signalLine[i];
if (macdValue !== undefined && signalValue !== undefined) {
histogram.push(macdValue - signalValue);
}
}
return {
macd: macdLine,
signal: signalLine,
histogram: histogram,
};
}
/**
* Bollinger Bands
*/
export function bollingerBands(
prices: number[],
period: number = 20,
standardDeviations: number = 2
): { upper: number[]; middle: number[]; lower: number[] } {
const middle = sma(prices, period);
const upper: number[] = [];
const lower: number[] = [];
for (let i = period - 1; i < prices.length; i++) {
const slice = prices.slice(i - period + 1, i + 1);
const mean = slice.reduce((a, b) => a + b, 0) / period;
const variance = slice.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / period;
const stdDev = Math.sqrt(variance);
const middleValue = middle[i - period + 1];
if (middleValue !== undefined) {
upper.push(middleValue + standardDeviations * stdDev);
lower.push(middleValue - standardDeviations * stdDev);
}
}
return { upper, middle, lower };
}
/**
* Average True Range (ATR)
*/
export function atr(ohlcv: OHLCV[], period: number = 14): number[] {
if (period >= ohlcv.length) {
return [];
}
const trueRanges: number[] = [];
for (let i = 1; i < ohlcv.length; i++) {
const current = ohlcv[i];
const previous = ohlcv[i - 1];
if (current && previous) {
const high = current.high;
const low = current.low;
const prevClose = previous.close;
const tr = Math.max(high - low, Math.abs(high - prevClose), Math.abs(low - prevClose));
trueRanges.push(tr);
}
}
return sma(trueRanges, period);
}
/**
* On-Balance Volume (OBV)
*/
export function obv(ohlcv: OHLCV[]): number[] {
if (ohlcv.length === 0) {
return [];
}
const first = ohlcv[0];
if (!first) {
return [];
}
const result: number[] = [first.volume];
for (let i = 1; i < ohlcv.length; i++) {
const prev = ohlcv[i - 1];
const curr = ohlcv[i];
if (!prev || !curr) {
continue;
}
const lastValue = result[result.length - 1];
if (lastValue === undefined) {
continue;
}
if (curr.close > prev.close) {
result.push(lastValue + curr.volume);
} else if (curr.close < prev.close) {
result.push(lastValue - curr.volume);
} else {
result.push(lastValue);
}
}
return result;
}
/**
* Stochastic Oscillator
*/
export function stochastic(
ohlcv: OHLCV[],
kPeriod: number = 14,
dPeriod: number = 3
): { k: number[]; d: number[] } {
if (kPeriod >= ohlcv.length) {
return { k: [], d: [] };
}
const kValues: number[] = [];
for (let i = kPeriod - 1; i < ohlcv.length; i++) {
const slice = ohlcv.slice(i - kPeriod + 1, i + 1);
const highest = Math.max(...slice.map(d => d.high));
const lowest = Math.min(...slice.map(d => d.low));
const current = ohlcv[i];
if (!current) {
continue;
}
const currentClose = current.close;
if (highest === lowest) {
kValues.push(50); // Avoid division by zero
} else {
const kValue = ((currentClose - lowest) / (highest - lowest)) * 100;
kValues.push(kValue);
}
}
const dValues = sma(kValues, dPeriod);
return { k: kValues, d: dValues };
}
/**
* Williams %R
*/
export function williamsR(ohlcv: OHLCV[], period: number = 14): number[] {
if (period >= ohlcv.length) {
return [];
}
const result: number[] = [];
for (let i = period - 1; i < ohlcv.length; i++) {
const slice = ohlcv.slice(i - period + 1, i + 1);
const highest = Math.max(...slice.map(d => d.high));
const lowest = Math.min(...slice.map(d => d.low));
const current = ohlcv[i];
if (!current) {
continue;
}
const currentClose = current.close;
if (highest === lowest) {
result.push(-50); // Avoid division by zero
} else {
const wrValue = ((highest - currentClose) / (highest - lowest)) * -100;
result.push(wrValue);
}
}
return result;
}
/**
* Commodity Channel Index (CCI)
*/
export function cci(ohlcv: OHLCV[], period: number = 20): number[] {
if (period >= ohlcv.length) {
return [];
}
const typicalPrices = ohlcv.map(d => (d.high + d.low + d.close) / 3);
const smaTP = sma(typicalPrices, period);
const result: number[] = [];
for (let i = 0; i < smaTP.length; i++) {
const slice = typicalPrices.slice(i, i + period);
const mean = smaTP[i];
if (mean === undefined) {
continue;
}
const meanDeviation = slice.reduce((sum, value) => sum + Math.abs(value - mean), 0) / period;
const typicalPrice = typicalPrices[i + period - 1];
if (typicalPrice === undefined) {
continue;
}
if (meanDeviation === 0) {
result.push(0);
} else {
const cciValue = (typicalPrice - mean) / (0.015 * meanDeviation);
result.push(cciValue);
}
}
return result;
}
/**
* Money Flow Index (MFI)
*/
export function mfi(ohlcv: OHLCV[], period: number = 14): number[] {
if (period >= ohlcv.length) {
return [];
}
const typicalPrices = ohlcv.map(d => (d.high + d.low + d.close) / 3);
const moneyFlows = ohlcv.map((d, i) => {
const tp = typicalPrices[i];
return tp !== undefined ? tp * d.volume : 0;
});
const result: number[] = [];
for (let i = period; i < ohlcv.length; i++) {
let positiveFlow = 0;
let negativeFlow = 0;
for (let j = i - period + 1; j <= i; j++) {
if (j > 0) {
const currentTP = typicalPrices[j];
const prevTP = typicalPrices[j - 1];
const currentMF = moneyFlows[j];
if (currentTP !== undefined && prevTP !== undefined && currentMF !== undefined) {
if (currentTP > prevTP) {
positiveFlow += currentMF;
} else if (currentTP < prevTP) {
negativeFlow += currentMF;
}
}
}
}
if (negativeFlow === 0) {
result.push(100);
} else {
const mfiRatio = positiveFlow / negativeFlow;
const mfiValue = 100 - 100 / (1 + mfiRatio);
result.push(mfiValue);
}
}
return result;
}
/**
* Volume Weighted Moving Average (VWMA)
*/
export function vwma(ohlcv: OHLCV[], period: number = 20): number[] {
if (period >= ohlcv.length) {
return [];
}
const result: number[] = [];
for (let i = period - 1; i < ohlcv.length; i++) {
const slice = ohlcv.slice(i - period + 1, i + 1);
let totalVolumePrice = 0;
let totalVolume = 0;
for (const candle of slice) {
const typicalPrice = (candle.high + candle.low + candle.close) / 3;
totalVolumePrice += typicalPrice * candle.volume;
totalVolume += candle.volume;
}
const vwmaValue = totalVolume > 0 ? totalVolumePrice / totalVolume : 0;
result.push(vwmaValue);
}
return result;
}
/**
* Momentum
*/
export function momentum(prices: number[], period: number = 10): number[] {
if (period >= prices.length) {
return [];
}
const result: number[] = [];
for (let i = period; i < prices.length; i++) {
const current = prices[i];
const previous = prices[i - period];
if (current !== undefined && previous !== undefined) {
const momentum = current - previous;
result.push(momentum);
}
}
return result;
}
/**
* Rate of Change (ROC)
*/
export function roc(prices: number[], period: number = 10): number[] {
if (period >= prices.length) {
return [];
}
const result: number[] = [];
for (let i = period; i < prices.length; i++) {
const current = prices[i];
const previous = prices[i - period];
if (current !== undefined && previous !== undefined) {
if (previous === 0) {
result.push(0);
} else {
const rocValue = ((current - previous) / previous) * 100;
result.push(rocValue);
}
}
}
return result;
}
/**
* Accumulation/Distribution Line
*/
export function accumulationDistribution(ohlcv: OHLCV[]): number[] {
if (ohlcv.length === 0) {
return [];
}
const result: number[] = [];
let adLine = 0;
for (const candle of ohlcv) {
if (candle.high === candle.low) {
// Avoid division by zero
result.push(adLine);
continue;
}
const moneyFlowMultiplier =
(candle.close - candle.low - (candle.high - candle.close)) / (candle.high - candle.low);
const moneyFlowVolume = moneyFlowMultiplier * candle.volume;
adLine += moneyFlowVolume;
result.push(adLine);
}
return result;
}
/**
* Aroon Indicator
*/
export function aroon(ohlcv: OHLCV[], period: number = 14): { up: number[]; down: number[] } {
if (period >= ohlcv.length) {
return { up: [], down: [] };
}
const up: number[] = [];
const down: number[] = [];
for (let i = period - 1; i < ohlcv.length; i++) {
const slice = ohlcv.slice(i - period + 1, i + 1);
// Find highest high and lowest low positions
let highestIndex = 0;
let lowestIndex = 0;
for (let j = 1; j < slice.length; j++) {
const current = slice[j];
const highest = slice[highestIndex];
const lowest = slice[lowestIndex];
if (current && highest && lowest) {
if (current.high > highest.high) {
highestIndex = j;
}
if (current.low < lowest.low) {
lowestIndex = j;
}
}
}
const aroonUp = ((period - 1 - highestIndex) / (period - 1)) * 100;
const aroonDown = ((period - 1 - lowestIndex) / (period - 1)) * 100;
up.push(aroonUp);
down.push(aroonDown);
}
return { up, down };
}
/**
* Average Directional Movement Index (ADX) and Directional Movement Indicators (DMI)
*/
export function adx(
ohlcv: OHLCV[],
period: number = 14
): { adx: number[]; plusDI: number[]; minusDI: number[] } {
if (period >= ohlcv.length) {
return { adx: [], plusDI: [], minusDI: [] };
}
const trueRanges: number[] = [];
const plusDM: number[] = [];
const minusDM: number[] = [];
// Calculate True Range and Directional Movements
for (let i = 1; i < ohlcv.length; i++) {
const current = ohlcv[i];
const previous = ohlcv[i - 1];
if (!current || !previous) {continue;}
// True Range
const tr = Math.max(
current.high - current.low,
Math.abs(current.high - previous.close),
Math.abs(current.low - previous.close)
);
trueRanges.push(tr);
// Directional Movements
const highDiff = current.high - previous.high;
const lowDiff = previous.low - current.low;
const plusDMValue = highDiff > lowDiff && highDiff > 0 ? highDiff : 0;
const minusDMValue = lowDiff > highDiff && lowDiff > 0 ? lowDiff : 0;
plusDM.push(plusDMValue);
minusDM.push(minusDMValue);
}
// Calculate smoothed averages
const atrValues = sma(trueRanges, period);
const smoothedPlusDM = sma(plusDM, period);
const smoothedMinusDM = sma(minusDM, period);
const plusDI: number[] = [];
const minusDI: number[] = [];
const dx: number[] = [];
// Calculate DI+ and DI-
for (let i = 0; i < atrValues.length; i++) {
const atr = atrValues[i];
const plusDMSmoothed = smoothedPlusDM[i];
const minusDMSmoothed = smoothedMinusDM[i];
if (atr === undefined || plusDMSmoothed === undefined || minusDMSmoothed === undefined) {continue;}
const diPlus = atr > 0 ? (plusDMSmoothed / atr) * 100 : 0;
const diMinus = atr > 0 ? (minusDMSmoothed / atr) * 100 : 0;
plusDI.push(diPlus);
minusDI.push(diMinus);
// Calculate DX
const diSum = diPlus + diMinus;
const dxValue = diSum > 0 ? (Math.abs(diPlus - diMinus) / diSum) * 100 : 0;
dx.push(dxValue);
}
// Calculate ADX (smoothed DX)
const adxValues = sma(dx, period);
return {
adx: adxValues,
plusDI: plusDI.slice(period - 1),
minusDI: minusDI.slice(period - 1),
};
}
/**
* Parabolic SAR
*/
export function parabolicSAR(
ohlcv: OHLCV[],
step: number = 0.02,
maxStep: number = 0.2
): number[] {
if (ohlcv.length < 2) {
return [];
}
const first = ohlcv[0];
if (!first) {return [];}
const result: number[] = [];
let trend = 1; // 1 for uptrend, -1 for downtrend
let acceleration = step;
let extremePoint = first.high;
let sar = first.low;
result.push(sar);
for (let i = 1; i < ohlcv.length; i++) {
const curr = ohlcv[i];
const prev = ohlcv[i - 1];
if (!curr || !prev) {continue;}
// Calculate new SAR
sar = sar + acceleration * (extremePoint - sar);
if (trend === 1) {
// Uptrend
if (curr.low <= sar) {
// Trend reversal
trend = -1;
sar = extremePoint;
extremePoint = curr.low;
acceleration = step;
} else {
if (curr.high > extremePoint) {
extremePoint = curr.high;
acceleration = Math.min(acceleration + step, maxStep);
}
// Ensure SAR doesn't exceed previous lows
const prevPrev = i > 1 ? ohlcv[i - 2] : null;
sar = Math.min(sar, prev.low, prevPrev ? prevPrev.low : prev.low);
}
} else {
// Downtrend
if (curr.high >= sar) {
// Trend reversal
trend = 1;
sar = extremePoint;
extremePoint = curr.high;
acceleration = step;
} else {
if (curr.low < extremePoint) {
extremePoint = curr.low;
acceleration = Math.min(acceleration + step, maxStep);
}
// Ensure SAR doesn't exceed previous highs
const prevPrev = i > 1 ? ohlcv[i - 2] : null;
sar = Math.max(sar, prev.high, prevPrev ? prevPrev.high : prev.high);
}
}
result.push(sar);
}
return result;
}
/**
* Chaikin Money Flow (CMF)
*/
export function chaikinMoneyFlow(ohlcv: OHLCV[], period: number = 20): number[] {
if (period >= ohlcv.length) {
return [];
}
const adValues: number[] = [];
for (const candle of ohlcv) {
if (candle.high === candle.low) {
adValues.push(0);
} else {
const moneyFlowMultiplier =
(candle.close - candle.low - (candle.high - candle.close)) / (candle.high - candle.low);
const moneyFlowVolume = moneyFlowMultiplier * candle.volume;
adValues.push(moneyFlowVolume);
}
}
const result: number[] = [];
for (let i = period - 1; i < ohlcv.length; i++) {
const sumAD = adValues.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0);
const sumVolume = ohlcv.slice(i - period + 1, i + 1).reduce((a, b) => a + b.volume, 0);
if (sumVolume === 0) {
result.push(0);
} else {
result.push(sumAD / sumVolume);
}
}
return result;
}
/**
* Pivot Points (Standard)
*/
export function pivotPoints(ohlcv: OHLCV[]): Array<{
pivot: number;
r1: number;
r2: number;
r3: number;
s1: number;
s2: number;
s3: number;
}> {
const result: Array<{
pivot: number;
r1: number;
r2: number;
r3: number;
s1: number;
s2: number;
s3: number;
}> = [];
for (const candle of ohlcv) {
const pivot = (candle.high + candle.low + candle.close) / 3;
const r1 = 2 * pivot - candle.low;
const s1 = 2 * pivot - candle.high;
const r2 = pivot + (candle.high - candle.low);
const s2 = pivot - (candle.high - candle.low);
const r3 = candle.high + 2 * (pivot - candle.low);
const s3 = candle.low - 2 * (candle.high - pivot);
result.push({ pivot, r1, r2, r3, s1, s2, s3 });
}
return result;
}
/**
* Ichimoku Cloud components
*/
export function ichimoku(
ohlcv: OHLCV[],
tenkanPeriod: number = 9,
kijunPeriod: number = 26,
senkouBPeriod: number = 52,
_displacement: number = 26
): {
tenkanSen: number[];
kijunSen: number[];
senkouSpanA: number[];
senkouSpanB: number[];
chikouSpan: number[];
} {
const tenkanSen: number[] = [];
const kijunSen: number[] = [];
const senkouSpanA: number[] = [];
const senkouSpanB: number[] = [];
const chikouSpan: number[] = [];
for (let i = 0; i < ohlcv.length; i++) {
// Tenkan-sen (Conversion Line)
if (i >= tenkanPeriod - 1) {
const slice = ohlcv.slice(i - tenkanPeriod + 1, i + 1);
const high = Math.max(...slice.map(d => d.high));
const low = Math.min(...slice.map(d => d.low));
tenkanSen.push((high + low) / 2);
}
// Kijun-sen (Base Line)
if (i >= kijunPeriod - 1) {
const slice = ohlcv.slice(i - kijunPeriod + 1, i + 1);
const high = Math.max(...slice.map(d => d.high));
const low = Math.min(...slice.map(d => d.low));
kijunSen.push((high + low) / 2);
}
// Senkou Span A (Leading Span A)
if (tenkanSen.length > 0 && kijunSen.length > 0) {
const tenkanValue = tenkanSen[tenkanSen.length - 1];
const kijunValue = kijunSen[kijunSen.length - 1];
if (tenkanValue !== undefined && kijunValue !== undefined) {
senkouSpanA.push((tenkanValue + kijunValue) / 2);
}
}
// Senkou Span B (Leading Span B)
if (i >= senkouBPeriod - 1) {
const slice = ohlcv.slice(i - senkouBPeriod + 1, i + 1);
const high = Math.max(...slice.map(d => d.high));
const low = Math.min(...slice.map(d => d.low));
senkouSpanB.push((high + low) / 2);
}
// Chikou Span (Lagging Span)
chikouSpan.push(ohlcv[i]!.close);
}
return {
tenkanSen,
kijunSen,
senkouSpanA,
senkouSpanB,
chikouSpan,
};
}
/**
* Ultimate Oscillator
*/
export function ultimateOscillator(
ohlcv: OHLCV[],
period1: number = 7,
period2: number = 14,
period3: number = 28
): number[] {
if (ohlcv.length < Math.max(period1, period2, period3)) {
return [];
}
const bp: number[] = []; // Buying Pressure
const tr: number[] = []; // True Range
// Calculate BP and TR
for (let i = 0; i < ohlcv.length; i++) {
const current = ohlcv[i]!;
if (i === 0) {
bp.push(current.close - current.low);
tr.push(current.high - current.low);
} else {
const previous = ohlcv[i - 1]!;
bp.push(current.close - Math.min(current.low, previous.close));
tr.push(Math.max(
current.high - current.low,
Math.abs(current.high - previous.close),
Math.abs(current.low - previous.close)
));
}
}
const result: number[] = [];
for (let i = Math.max(period1, period2, period3) - 1; i < ohlcv.length; i++) {
const avg1 = bp.slice(i - period1 + 1, i + 1).reduce((a, b) => a + b, 0) /
tr.slice(i - period1 + 1, i + 1).reduce((a, b) => a + b, 0);
const avg2 = bp.slice(i - period2 + 1, i + 1).reduce((a, b) => a + b, 0) /
tr.slice(i - period2 + 1, i + 1).reduce((a, b) => a + b, 0);
const avg3 = bp.slice(i - period3 + 1, i + 1).reduce((a, b) => a + b, 0) /
tr.slice(i - period3 + 1, i + 1).reduce((a, b) => a + b, 0);
const uo = 100 * ((4 * avg1) + (2 * avg2) + avg3) / (4 + 2 + 1);
result.push(uo);
}
return result;
}
/**
* Ease of Movement (EOM)
*/
export function easeOfMovement(ohlcv: OHLCV[], period: number = 14): number[] {
if (ohlcv.length < 2) {
return [];
}
const emv: number[] = [];
for (let i = 1; i < ohlcv.length; i++) {
const current = ohlcv[i]!;
const previous = ohlcv[i - 1]!;
const distance = ((current.high + current.low) / 2) - ((previous.high + previous.low) / 2);
const boxHeight = current.high - current.low;
const volume = current.volume;
if (boxHeight > 0 && volume > 0) {
const emvValue = (distance * 1000000) / (volume / boxHeight);
emv.push(emvValue);
} else {
emv.push(0);
}
}
return sma(emv, period);
}
/**
* Triple Exponential Moving Average (TEMA)
*/
export function tema(values: number[], period: number): number[] {
const ema1 = ema(values, period);
const ema2 = ema(ema1, period);
const ema3 = ema(ema2, period);
const result: number[] = [];
const minLength = Math.min(ema1.length, ema2.length, ema3.length);
for (let i = 0; i < minLength; i++) {
const e1 = ema1[i];
const e2 = ema2[i];
const e3 = ema3[i];
if (e1 !== undefined && e2 !== undefined && e3 !== undefined) {
const temaValue = 3 * e1 - 3 * e2 + e3;
result.push(temaValue);
}
}
return result;
}
/**
* Weighted Moving Average (WMA)
*/
export function wma(values: number[], period: number): number[] {
if (period > values.length) {
return [];
}
const result: number[] = [];
const denominator = (period * (period + 1)) / 2;
for (let i = period - 1; i < values.length; i++) {
let weightedSum = 0;
let weight = 1;
for (let j = i - period + 1; j <= i; j++) {
const value = values[j];
if (value !== undefined) {
weightedSum += value * weight;
weight++;
}
}
result.push(weightedSum / denominator);
}
return result;
}
/**
* Keltner Channels
*/
export function keltnerChannels(
ohlcv: OHLCV[],
period: number = 20,
multiplier: number = 2
): { upper: number[]; middle: number[]; lower: number[] } {
const typicalPrices = ohlcv.map(d => (d.high + d.low + d.close) / 3);
const middle = ema(typicalPrices, period);
const atrValues = atr(ohlcv, period);
const upper: number[] = [];
const lower: number[] = [];
const minLength = Math.min(middle.length, atrValues.length);
for (let i = 0; i < minLength; i++) {
const middleValue = middle[i];
const atrValue = atrValues[i];
if (middleValue !== undefined && atrValue !== undefined) {
upper.push(middleValue + multiplier * atrValue);
lower.push(middleValue - multiplier * atrValue);
}
}
return { upper, middle, lower };
}
/**
* Donchian Channels
*/
export function donchianChannels(
ohlcv: OHLCV[],
period: number = 20
): { upper: number[]; middle: number[]; lower: number[] } {
if (period >= ohlcv.length) {
return { upper: [], middle: [], lower: [] };
}
const upper: number[] = [];
const lower: number[] = [];
const middle: number[] = [];
for (let i = period - 1; i < ohlcv.length; i++) {
const slice = ohlcv.slice(i - period + 1, i + 1);
const highest = Math.max(...slice.map(d => d.high));
const lowest = Math.min(...slice.map(d => d.low));
const middleValue = (highest + lowest) / 2;
upper.push(highest);
lower.push(lowest);
middle.push(middleValue);
}
return { upper, middle, lower };
}
/**
* Klinger Volume Oscillator
*/
export function klingerVolumeOscillator(
ohlcv: OHLCV[],
fastPeriod: number = 34,
slowPeriod: number = 55,
signalPeriod: number = 13
): { kvo: number[]; signal: number[] } {
if (ohlcv.length < 2) {
return { kvo: [], signal: [] };
}
const volumeForce: number[] = [];
for (let i = 1; i < ohlcv.length; i++) {
const current = ohlcv[i]!;
const previous = ohlcv[i - 1]!;
const typicalPrice = (current.high + current.low + current.close) / 3;
const prevTypicalPrice = (previous.high + previous.low + previous.close) / 3;
const trend = typicalPrice > prevTypicalPrice ? 1 : -1;
const vf = current.volume * trend * Math.abs((2 * ((current.close - current.low) - (current.high - current.close))) / (current.high - current.low)) * 100;
volumeForce.push(vf);
}
const fastEMA = ema(volumeForce, fastPeriod);
const slowEMA = ema(volumeForce, slowPeriod);
const kvo: number[] = [];
const minLength = Math.min(fastEMA.length, slowEMA.length);
for (let i = 0; i < minLength; i++) {
const fast = fastEMA[i];
const slow = slowEMA[i];
if (fast !== undefined && slow !== undefined) {
kvo.push(fast - slow);
}
}
const signal = ema(kvo, signalPeriod);
return { kvo, signal };
}
/**
* Price Oscillator (PPO)
*/
export function priceOscillator(
prices: number[],
fastPeriod: number = 12,
slowPeriod: number = 26,
signalPeriod: number = 9
): { ppo: number[]; signal: number[]; histogram: number[] } {
const fastEMA = ema(prices, fastPeriod);
const slowEMA = ema(prices, slowPeriod);
const ppo: number[] = [];
const minLength = Math.min(fastEMA.length, slowEMA.length);
for (let i = 0; i < minLength; i++) {
const fast = fastEMA[i];
const slow = slowEMA[i];
if (fast !== undefined && slow !== undefined && slow !== 0) {
const ppoValue = ((fast - slow) / slow) * 100;
ppo.push(ppoValue);
}
}
const signal = ema(ppo, signalPeriod);
const histogram: number[] = [];
const histMinLength = Math.min(ppo.length, signal.length);
for (let i = 0; i < histMinLength; i++) {
const ppoValue = ppo[i];
const signalValue = signal[i];
if (ppoValue !== undefined && signalValue !== undefined) {
histogram.push(ppoValue - signalValue);
}
}
return { ppo, signal, histogram };
}
/**
* Chande Momentum Oscillator (CMO)
*/
export function chandeMomentumOscillator(prices: number[], period: number = 14): number[] {
if (period >= prices.length) {
return [];
}
const gains: number[] = [];
const losses: number[] = [];
// Calculate gains and losses
for (let i = 1; i < prices.length; i++) {
const current = prices[i];
const previous = prices[i - 1];
if (current !== undefined && previous !== undefined) {
const change = current - previous;
gains.push(change > 0 ? change : 0);
losses.push(change < 0 ? Math.abs(change) : 0);
}
}
const result: number[] = [];
for (let i = period - 1; i < gains.length; i++) {
const sumGains = gains.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0);
const sumLosses = losses.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0);
const cmo = ((sumGains - sumLosses) / (sumGains + sumLosses)) * 100;
result.push(isNaN(cmo) ? 0 : cmo);
}
return result;
}
/**
* Stochastic RSI
*/
export function stochasticRSI(
prices: number[],
rsiPeriod: number = 14,
stochPeriod: number = 14,
smoothK: number = 3,
smoothD: number = 3
): { k: number[]; d: number[] } {
const rsiValues = rsi(prices, rsiPeriod);
if (rsiValues.length < stochPeriod) {
return { k: [], d: [] };
}
const stochRSI: number[] = [];
for (let i = stochPeriod - 1; i < rsiValues.length; i++) {
const slice = rsiValues.slice(i - stochPeriod + 1, i + 1);
const highest = Math.max(...slice);
const lowest = Math.min(...slice);
const currentRSI = rsiValues[i]!;
if (highest === lowest) {
stochRSI.push(0);
} else {
const stochValue = ((currentRSI - lowest) / (highest - lowest)) * 100;
stochRSI.push(stochValue);
}
}
const k = sma(stochRSI, smoothK);
const d = sma(k, smoothD);
return { k, d };
}
/**
* Vortex Indicator
*/
export function vortexIndicator(
ohlcv: OHLCV[],
period: number = 14
): { vi_plus: number[]; vi_minus: number[] } {
if (ohlcv.length < period + 1) {
return { vi_plus: [], vi_minus: [] };
}
const vm_plus: number[] = [];
const vm_minus: number[] = [];
const tr: number[] = [];
for (let i = 1; i < ohlcv.length; i++) {
const current = ohlcv[i]!;
const previous = ohlcv[i - 1]!;
// Vortex Movement
vm_plus.push(Math.abs(current.high - previous.low));
vm_minus.push(Math.abs(current.low - previous.high));
// True Range
const trValue = Math.max(
current.high - current.low,
Math.abs(current.high - previous.close),
Math.abs(current.low - previous.close)
);
tr.push(trValue);
}
const vi_plus: number[] = [];
const vi_minus: number[] = [];
for (let i = period - 1; i < vm_plus.length; i++) {
const sumVMPlus = vm_plus.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0);
const sumVMMinus = vm_minus.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0);
const sumTR = tr.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0);
if (sumTR > 0) {
vi_plus.push(sumVMPlus / sumTR);
vi_minus.push(sumVMMinus / sumTR);
} else {
vi_plus.push(0);
vi_minus.push(0);
}
}
return { vi_plus, vi_minus };
}
/**
* Balance of Power (BOP)
*/
export function balanceOfPower(ohlcv: OHLCV[]): number[] {
const result: number[] = [];
for (const candle of ohlcv) {
if (candle.high === candle.low) {
result.push(0);
} else {
const bop = (candle.close - candle.open) / (candle.high - candle.low);
result.push(bop);
}
}
return result;
}
/**
* Trix (Triple Exponential Moving Average Oscillator)
*/
export function trix(prices: number[], period: number = 14): number[] {
const ema1 = ema(prices, period);
const ema2 = ema(ema1, period);
const ema3 = ema(ema2, period);
const result: number[] = [];
for (let i = 1; i < ema3.length; i++) {
const current = ema3[i];
const previous = ema3[i - 1];
if (current !== undefined && previous !== undefined && previous !== 0) {
const trixValue = ((current - previous) / previous) * 10000;
result.push(trixValue);
}
}
return result;
}
/**
* Mass Index
*/
export function massIndex(ohlcv: OHLCV[], period: number = 25): number[] {
if (ohlcv.length < period) {
return [];
}
// Calculate high-low ranges
const ranges = ohlcv.map(candle => candle.high - candle.low);
// Calculate 9-period EMA of ranges
const ema9 = ema(ranges, 9);
// Calculate 9-period EMA of the EMA (double smoothing)
const emaEma9 = ema(ema9, 9);
// Calculate ratio
const ratios: number[] = [];
const minLength = Math.min(ema9.length, emaEma9.length);
for (let i = 0; i < minLength; i++) {
const singleEMA = ema9[i];
const doubleEMA = emaEma9[i];
if (singleEMA !== undefined && doubleEMA !== undefined && doubleEMA !== 0) {
ratios.push(singleEMA / doubleEMA);
}
}
// Calculate 25-period sum of ratios
const result: number[] = [];
for (let i = period - 1; i < ratios.length; i++) {
const sum = ratios.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0);
result.push(sum);
}
return result;
}
/**
* Coppock Curve
*/
export function coppockCurve(
prices: number[],
shortROC: number = 11,
longROC: number = 14,
wma: number = 10
): number[] {
const roc1 = roc(prices, shortROC);
const roc2 = roc(prices, longROC);
const combined: number[] = [];
const minLength = Math.min(roc1.length, roc2.length);
for (let i = 0; i < minLength; i++) {
const r1 = roc1[i];
const r2 = roc2[i];
if (r1 !== undefined && r2 !== undefined) {
combined.push(r1 + r2);
}
}
return sma(combined, wma); // Using SMA instead of WMA for simplicity
}