stock-bot/libs/utils/src/calculations/technical-indicators.ts
2025-06-13 13:38:02 -04:00

2445 lines
62 KiB
TypeScript

/**
* Technical Indicators
* Comprehensive set of technical analysis indicators
*/
import { OHLCVData } from './index';
/**
* 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++) {
ema = values[i] * 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 change = prices[i] - prices[i - 1];
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++) {
macdLine.push(fastEMA[i + startIndex] - slowEMA[i]);
}
const signalLine = ema(macdLine, signalPeriod);
const histogram: number[] = [];
const signalStartIndex = signalPeriod - 1;
for (let i = 0; i < signalLine.length; i++) {
histogram.push(macdLine[i + signalStartIndex] - signalLine[i]);
}
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];
upper.push(middleValue + standardDeviations * stdDev);
lower.push(middleValue - standardDeviations * stdDev);
}
return { upper, middle, lower };
}
/**
* Average True Range (ATR)
*/
export function atr(ohlcv: OHLCVData[], period: number = 14): number[] {
if (period >= ohlcv.length) {
return [];
}
const trueRanges: number[] = [];
for (let i = 1; i < ohlcv.length; i++) {
const high = ohlcv[i].high;
const low = ohlcv[i].low;
const prevClose = ohlcv[i - 1].close;
const tr = Math.max(high - low, Math.abs(high - prevClose), Math.abs(low - prevClose));
trueRanges.push(tr);
}
return sma(trueRanges, period);
}
/**
* Stochastic Oscillator
*/
export function stochastic(
ohlcv: OHLCVData[],
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 currentClose = ohlcv[i].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: OHLCVData[], 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 currentClose = ohlcv[i].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: OHLCVData[], 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];
const meanDeviation = slice.reduce((sum, value) => sum + Math.abs(value - mean), 0) / period;
if (meanDeviation === 0) {
result.push(0);
} else {
const cciValue = (typicalPrices[i + period - 1] - mean) / (0.015 * meanDeviation);
result.push(cciValue);
}
}
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 momentum = prices[i] - prices[i - period];
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++) {
if (prices[i - period] === 0) {
result.push(0);
} else {
const rocValue = ((prices[i] - prices[i - period]) / prices[i - period]) * 100;
result.push(rocValue);
}
}
return result;
}
/**
* Money Flow Index (MFI)
*/
export function mfi(ohlcv: OHLCVData[], 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) => typicalPrices[i] * d.volume);
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) {
if (typicalPrices[j] > typicalPrices[j - 1]) {
positiveFlow += moneyFlows[j];
} else if (typicalPrices[j] < typicalPrices[j - 1]) {
negativeFlow += moneyFlows[j];
}
}
}
if (negativeFlow === 0) {
result.push(100);
} else {
const mfiRatio = positiveFlow / negativeFlow;
const mfiValue = 100 - 100 / (1 + mfiRatio);
result.push(mfiValue);
}
}
return result;
}
/**
* On-Balance Volume (OBV)
*/
export function obv(ohlcv: OHLCVData[]): number[] {
if (ohlcv.length === 0) {
return [];
}
const result: number[] = [ohlcv[0].volume];
for (let i = 1; i < ohlcv.length; i++) {
const prev = ohlcv[i - 1];
const curr = ohlcv[i];
if (curr.close > prev.close) {
result.push(result[result.length - 1] + curr.volume);
} else if (curr.close < prev.close) {
result.push(result[result.length - 1] - curr.volume);
} else {
result.push(result[result.length - 1]);
}
}
return result;
}
/**
* Accumulation/Distribution Line
*/
export function accumulationDistribution(ohlcv: OHLCVData[]): 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;
}
/**
* Chaikin Money Flow (CMF)
*/
export function chaikinMoneyFlow(ohlcv: OHLCVData[], 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;
}
/**
* Parabolic SAR
*/
export function parabolicSAR(
ohlcv: OHLCVData[],
step: number = 0.02,
maxStep: number = 0.2
): number[] {
if (ohlcv.length < 2) {
return [];
}
const result: number[] = [];
let trend = 1; // 1 for uptrend, -1 for downtrend
let acceleration = step;
let extremePoint = ohlcv[0].high;
let sar = ohlcv[0].low;
result.push(sar);
for (let i = 1; i < ohlcv.length; i++) {
const curr = ohlcv[i];
const prev = ohlcv[i - 1];
// 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
sar = Math.min(sar, prev.low, i > 1 ? ohlcv[i - 2].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
sar = Math.max(sar, prev.high, i > 1 ? ohlcv[i - 2].high : prev.high);
}
}
result.push(sar);
}
return result;
}
/**
* Aroon Indicator
*/
export function aroon(ohlcv: OHLCVData[], 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++) {
if (slice[j].high > slice[highestIndex].high) {
highestIndex = j;
}
if (slice[j].low < slice[lowestIndex].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: OHLCVData[],
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];
// 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 diPlus = atrValues[i] > 0 ? (smoothedPlusDM[i] / atrValues[i]) * 100 : 0;
const diMinus = atrValues[i] > 0 ? (smoothedMinusDM[i] / atrValues[i]) * 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),
};
}
/**
* Volume Weighted Moving Average (VWMA)
*/
export function vwma(ohlcv: OHLCVData[], 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;
}
/**
* Pivot Points (Standard)
*/
export function pivotPoints(ohlcv: OHLCVData[]): Array<{
pivot: number;
resistance1: number;
resistance2: number;
resistance3: number;
support1: number;
support2: number;
support3: number;
}> {
if (ohlcv.length === 0) {
return [];
}
const result: Array<{
pivot: number;
resistance1: number;
resistance2: number;
resistance3: number;
support1: number;
support2: number;
support3: number;
}> = [];
for (let i = 0; i < ohlcv.length; i++) {
const candle = ohlcv[i];
// Calculate pivot point
const pivot = (candle.high + candle.low + candle.close) / 3;
// Calculate resistance and support levels
const resistance1 = 2 * pivot - candle.low;
const support1 = 2 * pivot - candle.high;
const resistance2 = pivot + (candle.high - candle.low);
const support2 = pivot - (candle.high - candle.low);
const resistance3 = candle.high + 2 * (pivot - candle.low);
const support3 = candle.low - 2 * (candle.high - pivot);
result.push({
pivot,
resistance1,
resistance2,
resistance3,
support1,
support2,
support3,
});
}
return result;
}
/**
* Ichimoku Cloud
*/
export function ichimokuCloud(
ohlcv: OHLCVData[],
tenkanSenPeriod: number = 9,
kijunSenPeriod: number = 26,
senkouSpanBPeriod: number = 52
): {
tenkanSen: number[];
kijunSen: number[];
senkouSpanA: number[];
senkouSpanB: number[];
chikouSpan: number[];
} {
const { high, low, close } = {
high: ohlcv.map(item => item.high),
low: ohlcv.map(item => item.low),
close: ohlcv.map(item => item.close),
};
const tenkanSen = calculateTenkanSen(high, low, tenkanSenPeriod);
const kijunSen = calculateKijunSen(high, low, kijunSenPeriod);
const senkouSpanA = calculateSenkouSpanA(tenkanSen, kijunSen);
const senkouSpanB = calculateSenkouSpanB(high, low, senkouSpanBPeriod);
const chikouSpan = calculateChikouSpan(close, kijunSenPeriod);
return {
tenkanSen,
kijunSen,
senkouSpanA,
senkouSpanB,
chikouSpan,
};
function calculateTenkanSen(high: number[], low: number[], period: number): number[] {
const tenkanSen: number[] = [];
for (let i = period - 1; i < high.length; i++) {
const sliceHigh = high.slice(i - period + 1, i + 1);
const sliceLow = low.slice(i - period + 1, i + 1);
const highestHigh = Math.max(...sliceHigh);
const lowestLow = Math.min(...sliceLow);
tenkanSen.push((highestHigh + lowestLow) / 2);
}
return tenkanSen;
}
function calculateKijunSen(high: number[], low: number[], period: number): number[] {
const kijunSen: number[] = [];
for (let i = period - 1; i < high.length; i++) {
const sliceHigh = high.slice(i - period + 1, i + 1);
const sliceLow = low.slice(i - period + 1, i + 1);
const highestHigh = Math.max(...sliceHigh);
const lowestLow = Math.min(...sliceLow);
kijunSen.push((highestHigh + lowestLow) / 2);
}
return kijunSen;
}
function calculateSenkouSpanA(tenkanSen: number[], kijunSen: number[]): number[] {
const senkouSpanA: number[] = [];
for (let i = 0; i < tenkanSen.length; i++) {
senkouSpanA.push((tenkanSen[i] + kijunSen[i]) / 2);
}
return senkouSpanA;
}
function calculateSenkouSpanB(high: number[], low: number[], period: number): number[] {
const senkouSpanB: number[] = [];
for (let i = period - 1; i < high.length; i++) {
const sliceHigh = high.slice(i - period + 1, i + 1);
const sliceLow = low.slice(i - period + 1, i + 1);
const highestHigh = Math.max(...sliceHigh);
const lowestLow = Math.min(...sliceLow);
senkouSpanB.push((highestHigh + lowestLow) / 2);
}
return senkouSpanB;
}
function calculateChikouSpan(close: number[], period: number): number[] {
const chikouSpan: number[] = [];
for (let i = 0; i < close.length - period; i++) {
chikouSpan.push(close[i]);
}
return chikouSpan;
}
}
/**
* Keltner Channels
*/
export function keltnerChannels(
ohlcv: OHLCVData[],
period: number = 20,
multiplier: number = 2
): {
upper: number[];
middle: number[];
lower: number[];
} {
const atrValues = atr(ohlcv, period);
const middle = sma(
ohlcv.map(item => (item.high + item.low + item.close) / 3),
period
);
const upper: number[] = [];
const lower: number[] = [];
for (let i = 0; i < middle.length; i++) {
upper.push(middle[i] + multiplier * atrValues[i]);
lower.push(middle[i] - multiplier * atrValues[i]);
}
return {
upper,
middle,
lower,
};
}
/**
* Donchian Channels
*/
export function donchianChannels(
ohlcv: OHLCVData[],
period: number = 20
): {
upper: number[];
middle: number[];
lower: number[];
} {
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 highestHigh = Math.max(...slice.map(item => item.high));
const lowestLow = Math.min(...slice.map(item => item.low));
upper.push(highestHigh);
lower.push(lowestLow);
middle.push((highestHigh + lowestLow) / 2);
}
return {
upper,
middle,
lower,
};
}
/**
* Elder-Ray Index
*/
export function elderRay(
ohlcv: OHLCVData[],
period: number = 13
): {
bullPower: number[];
bearPower: number[];
} {
const closePrices = ohlcv.map(item => item.close);
const emaValues = ema(closePrices, period);
const bullPower: number[] = [];
const bearPower: number[] = [];
// Adjust the indexing to ensure we're matching the correct EMA value with each candle
for (let i = period - 1; i < ohlcv.length; i++) {
// Using the proper index for the EMA values which are aligned with closePrices
// Since ema() returns values starting from the period-th element
const emaIndex = i - (period - 1);
if (emaIndex >= 0 && emaIndex < emaValues.length) {
bullPower.push(ohlcv[i].high - emaValues[emaIndex]);
bearPower.push(ohlcv[i].low - emaValues[emaIndex]);
}
}
return {
bullPower,
bearPower,
};
}
/**
* Force Index
*/
export function forceIndex(ohlcv: OHLCVData[], period: number = 13): number[] {
const forceIndexValues: number[] = [];
for (let i = 1; i < ohlcv.length; i++) {
const change = ohlcv[i].close - ohlcv[i - 1].close;
const volume = ohlcv[i].volume;
forceIndexValues.push(change * volume);
}
const smaValues = sma(forceIndexValues, period);
return smaValues;
}
/**
* Moving Average Envelope
*/
export function movingAverageEnvelope(
prices: number[],
period: number = 20,
percentage: number = 0.05
): {
upper: number[];
lower: number[];
middle: number[];
} {
const middle = sma(prices, period);
const upper: number[] = middle.map(value => value * (1 + percentage));
const lower: number[] = middle.map(value => value * (1 - percentage));
return {
upper,
lower,
middle,
};
}
/**
* High-Low Index
*/
export function highLowIndex(ohlcv: OHLCVData[], period: number = 14): number[] {
const highLowIndexValues: number[] = [];
for (let i = period; i < ohlcv.length; i++) {
let newHighs = 0;
let newLows = 0;
for (let j = i - period; j <= i; j++) {
if (ohlcv[j].close === Math.max(...ohlcv.slice(i - period, i + 1).map(item => item.close))) {
newHighs++;
}
if (ohlcv[j].close === Math.min(...ohlcv.slice(i - period, i + 1).map(item => item.close))) {
newLows++;
}
}
highLowIndexValues.push(((newHighs - newLows) / (newHighs + newLows)) * 100);
}
return highLowIndexValues;
}
/**
* Coppock Curve
*/
export function coppockCurve(
prices: number[],
longPeriod: number = 14,
shortPeriod: number = 11,
weightedMovingAveragePeriod: number = 10
): number[] {
const rocLong = roc(prices, longPeriod);
const rocShort = roc(prices, shortPeriod);
const sumROC: number[] = rocLong.map((value, index) => value + rocShort[index]);
return sma(sumROC, weightedMovingAveragePeriod);
}
/**
* Ease of Movement (EMV)
*/
export function easeOfMovement(ohlcv: OHLCVData[], period: number = 14): number[] {
const emv: number[] = [];
for (let i = 1; i < ohlcv.length; i++) {
const distance =
(ohlcv[i].high + ohlcv[i].low) / 2 - (ohlcv[i - 1].high + ohlcv[i - 1].low) / 2;
const boxRatio = ohlcv[i].volume / 100000000 / (ohlcv[i].high - ohlcv[i].low); // Scale volume to avoid very small numbers
emv.push(distance / boxRatio);
}
return sma(emv, period);
}
/**
* Mass Index
*/
export function massIndex(
ohlcv: OHLCVData[],
period: number = 9,
emaPeriod: number = 25
): number[] {
const singleEma: number[] = ema(
ohlcv.map(item => item.high - item.low),
emaPeriod
);
const doubleEma: number[] = ema(singleEma, emaPeriod);
const massIndexValues: number[] = [];
for (let i = period; i < doubleEma.length; i++) {
let sum = 0;
for (let j = i - period; j < i; j++) {
sum += singleEma[j] / doubleEma[j];
}
massIndexValues.push(sum);
}
return massIndexValues;
}
/**
* Ultimate Oscillator
*/
export function ultimateOscillator(
ohlcv: OHLCVData[],
shortPeriod: number = 7,
mediumPeriod: number = 14,
longPeriod: number = 28
): number[] {
const ultimateOscillatorValues: number[] = [];
for (let i = longPeriod; i < ohlcv.length; i++) {
let trueRangeSum = 0;
let buyingPressureSum = 0;
for (let j = i; j > 0 && j >= i - longPeriod; j--) {
const trueRange = Math.max(
ohlcv[j].high - ohlcv[j].low,
Math.abs(ohlcv[j].high - ohlcv[j - 1].close),
Math.abs(ohlcv[j].low - ohlcv[j - 1].close)
);
const buyingPressure = ohlcv[j].close - Math.min(ohlcv[j].low, ohlcv[j - 1].close);
trueRangeSum += trueRange;
buyingPressureSum += buyingPressure;
}
const ultimateOscillatorValue =
(100 *
((4 * buyingPressureSum) / trueRangeSum +
(2 * buyingPressureSum) / trueRangeSum +
buyingPressureSum / trueRangeSum)) /
7;
ultimateOscillatorValues.push(ultimateOscillatorValue);
}
return ultimateOscillatorValues;
}
/**
* Schaff Trend Cycle (STC)
*/
export function schaffTrendCycle(
prices: number[],
period: number = 10,
fastMAPeriod: number = 23,
slowMAPeriod: number = 50
): number[] {
const macdValues = macd(prices, fastMAPeriod, slowMAPeriod);
const maxValue = Math.max(...macdValues.macd);
const minValue = Math.min(...macdValues.macd);
const kValues: number[] = macdValues.macd.map(
value => ((value - minValue) / (maxValue - minValue)) * 100
);
const dValues: number[] = sma(kValues, period);
return dValues;
}
/**
* Hilbert Transform - Instantaneous Trendline
*/
export function hilbertTransformInstantaneousTrendline(prices: number[]): number[] {
// This is a placeholder. A full Hilbert Transform implementation is complex.
// Requires significantly more code and signal processing knowledge.
// Returning a simple moving average as a substitute.
return sma(prices, 20);
}
/**
* Relative Volatility Index (RVI)
*/
export function relativeVolatilityIndex(ohlcv: OHLCVData[], period: number = 14): number[] {
const rviValues: number[] = [];
for (let i = period; i < ohlcv.length; i++) {
let highCloseSum = 0;
let lowCloseSum = 0;
for (let j = i; j > 0 && j >= i - period; j--) {
highCloseSum += Math.pow(ohlcv[j].high - ohlcv[j].close, 2);
lowCloseSum += Math.pow(ohlcv[j].low - ohlcv[j].close, 2);
}
const highCloseStdDev = Math.sqrt(highCloseSum / period);
const lowCloseStdDev = Math.sqrt(lowCloseSum / period);
const rviValue = (100 * highCloseStdDev) / (highCloseStdDev + lowCloseStdDev);
rviValues.push(rviValue);
}
return rviValues;
}
/**
* Chande Momentum Oscillator (CMO)
*/
export function chandeMomentumOscillator(prices: number[], period: number = 14): number[] {
const cmoValues: number[] = [];
for (let i = period; i < prices.length; i++) {
let sumOfGains = 0;
let sumOfLosses = 0;
for (let j = i; j > 0 && j >= i - period; j--) {
const change = prices[j] - prices[j - 1];
if (change > 0) {
sumOfGains += change;
} else {
sumOfLosses += Math.abs(change);
}
}
const cmoValue = (100 * (sumOfGains - sumOfLosses)) / (sumOfGains + sumOfLosses);
cmoValues.push(cmoValue);
}
return cmoValues;
}
/**
* Detrended Price Oscillator (DPO)
*/
export function detrendedPriceOscillator(prices: number[], period: number = 20): number[] {
const dpoValues: number[] = [];
const smaValues = sma(prices, period);
for (let i = period; i < prices.length; i++) {
const dpoValue = prices[i - Math.floor(period / 2) - 1] - smaValues[i - period];
dpoValues.push(dpoValue);
}
return dpoValues;
}
/**
* Fractal Chaos Bands
*/
export function fractalChaosBands(
ohlcv: OHLCVData[],
period: number = 20
): { upper: number[]; lower: number[] } {
const upper: number[] = [];
const lower: number[] = [];
for (let i = period; i < ohlcv.length; i++) {
const slice = ohlcv.slice(i - period + 1, i + 1);
const highestHigh = Math.max(...slice.map(item => item.high));
const lowestLow = Math.min(...slice.map(item => item.low));
upper.push(highestHigh);
lower.push(lowestLow);
}
return {
upper,
lower,
};
}
/**
* Know Sure Thing (KST) Oscillator
*/
export function knowSureThing(
prices: number[],
rocPeriod1: number = 10,
rocPeriod2: number = 15,
rocPeriod3: number = 20,
rocPeriod4: number = 30,
smaPeriod1: number = 10,
smaPeriod2: number = 10,
smaPeriod3: number = 10,
smaPeriod4: number = 15
): number[] {
const roc1 = roc(prices, rocPeriod1);
const roc2 = roc(prices, rocPeriod2);
const roc3 = roc(prices, rocPeriod3);
const roc4 = roc(prices, rocPeriod4);
const sma1 = sma(roc1, smaPeriod1);
const sma2 = sma(roc2, smaPeriod2);
const sma3 = sma(roc3, smaPeriod3);
const sma4 = sma(roc4, smaPeriod4);
const kstValues: number[] = [];
for (let i = 0; i < sma1.length; i++) {
const kstValue = sma1[i] + sma2[i] + sma3[i] + sma4[i];
kstValues.push(kstValue);
}
return kstValues;
}
/**
* Percentage Price Oscillator (PPO)
*/
export function percentagePriceOscillator(
prices: number[],
fastPeriod: number = 12,
slowPeriod: number = 26
): number[] {
const fastEMA = ema(prices, fastPeriod);
const slowEMA = ema(prices, slowPeriod);
const ppoValues: number[] = [];
for (let i = 0; i < fastEMA.length; i++) {
const ppoValue = ((fastEMA[i] - slowEMA[i]) / slowEMA[i]) * 100;
ppoValues.push(ppoValue);
}
return ppoValues;
}
/**
* Price Volume Trend (PVT)
*/
export function priceVolumeTrend(ohlcv: OHLCVData[]): number[] {
const pvtValues: number[] = [0]; // Initialize with 0
for (let i = 1; i < ohlcv.length; i++) {
const change = (ohlcv[i].close - ohlcv[i - 1].close) / ohlcv[i - 1].close;
const pvtValue = pvtValues[i - 1] + change * ohlcv[i].volume;
pvtValues.push(pvtValue);
}
return pvtValues;
}
/**
* Q Stick
*/
export function qStick(ohlcv: OHLCVData[], period: number = 10): number[] {
const qStickValues: number[] = [];
for (let i = period; i < ohlcv.length; i++) {
let sum = 0;
for (let j = i; j > 0 && j >= i - period; j--) {
sum += ohlcv[j].close - ohlcv[j].open;
}
qStickValues.push(sum / period);
}
return qStickValues;
}
/**
* TRIX (Triple Exponentially Smoothed Average)
*/
export function trix(prices: number[], period: number = 18): number[] {
const ema1 = ema(prices, period);
const ema2 = ema(ema1, period);
const ema3 = ema(ema2, period);
const trixValues: number[] = [];
for (let i = 1; i < ema3.length; i++) {
const trixValue = ((ema3[i] - ema3[i - 1]) / ema3[i - 1]) * 100;
trixValues.push(trixValue);
}
return trixValues;
}
/**
* Vertical Horizontal Filter (VHF)
*/
export function verticalHorizontalFilter(ohlcv: OHLCVData[], period: number = 28): number[] {
const vhfValues: number[] = [];
for (let i = period; i < ohlcv.length; i++) {
const slice = ohlcv.slice(i - period + 1, i + 1);
const highestHigh = Math.max(...slice.map(item => item.high));
const lowestLow = Math.min(...slice.map(item => item.low));
const closeChanges: number[] = [];
for (let j = 1; j < slice.length; j++) {
closeChanges.push(Math.abs(slice[j].close - slice[j - 1].close));
}
const sumOfCloseChanges = closeChanges.reduce((a, b) => a + b, 0);
const vhfValue = (highestHigh - lowestLow) / sumOfCloseChanges;
vhfValues.push(vhfValue);
}
return vhfValues;
}
/**
* Volume Rate of Change (VROC)
*/
export function volumeRateOfChange(ohlcv: OHLCVData[], period: number = 10): number[] {
const vrocValues: number[] = [];
for (let i = period; i < ohlcv.length; i++) {
if (ohlcv[i - period].volume === 0) {
vrocValues.push(0); // Avoid division by zero
} else {
const vrocValue =
((ohlcv[i].volume - ohlcv[i - period].volume) / ohlcv[i - period].volume) * 100;
vrocValues.push(vrocValue);
}
}
return vrocValues;
}
/**
* Average True Range Trailing Stops
* Calculates trailing stop levels based on ATR
*/
export function atrTrailingStops(
ohlcv: OHLCVData[],
period: number = 14,
multiplier: number = 3
): {
longStop: number[];
shortStop: number[];
} {
const atrValues = atr(ohlcv, period);
const longStop: number[] = [];
const shortStop: number[] = [];
for (let i = period; i < ohlcv.length; i++) {
longStop.push(ohlcv[i].low - multiplier * atrValues[i - period]);
shortStop.push(ohlcv[i].high + multiplier * atrValues[i - period]);
}
return {
longStop,
shortStop,
};
}
/**
* Elder's Force Index
* Measures the strength of a trend by combining price and volume
*/
export function eldersForceIndex(ohlcv: OHLCVData[], period: number = 13): number[] {
const forceIndexValues: number[] = [];
for (let i = 1; i < ohlcv.length; i++) {
const change = ohlcv[i].close - ohlcv[i - 1].close;
const volume = ohlcv[i].volume;
forceIndexValues.push(change * volume);
}
return ema(forceIndexValues, period);
}
/**
* Ultimate Oscillator
*/
export function trueStrengthIndex(
prices: number[],
longPeriod: number = 25,
shortPeriod: number = 13,
signalPeriod: number = 9
): number[] {
const priceChanges: number[] = [];
for (let i = 1; i < prices.length; i++) {
priceChanges.push(prices[i] - prices[i - 1]);
}
const smoothedMomentum = ema(priceChanges, shortPeriod);
const doubleSmoothedMomentum = ema(smoothedMomentum, longPeriod);
const absoluteMomentum = priceChanges.map(Math.abs);
const smoothedAbsoluteMomentum = ema(absoluteMomentum, shortPeriod);
const doubleSmoothedAbsoluteMomentum = ema(smoothedAbsoluteMomentum, longPeriod);
const tsiValues: number[] = [];
for (let i = longPeriod; i < prices.length - 1; i++) {
tsiValues.push(
(doubleSmoothedMomentum[i - longPeriod] / doubleSmoothedAbsoluteMomentum[i - longPeriod]) *
100
);
}
return tsiValues;
}
/**
* Money Flow Multiplier
* Calculates the Money Flow Multiplier
*/
export function moneyFlowMultiplier(ohlcv: OHLCVData[]): number[] {
return ohlcv.map(
candle =>
(candle.close - candle.low - (candle.high - candle.close)) / (candle.high - candle.low)
);
}
/**
* Positive Volume Index (PVI)
*/
export function positiveVolumeIndex(ohlcv: OHLCVData[], initialValue: number = 1000): number[] {
const pviValues: number[] = [initialValue];
for (let i = 1; i < ohlcv.length; i++) {
if (ohlcv[i].volume > ohlcv[i - 1].volume) {
const change = (ohlcv[i].close - ohlcv[i - 1].close) / ohlcv[i - 1].close;
pviValues.push(pviValues[i - 1] + pviValues[i - 1] * change);
} else {
pviValues.push(pviValues[i - 1]);
}
}
return pviValues;
}
/**
* Negative Volume Index (NVI)
*/
export function negativeVolumeIndex(ohlcv: OHLCVData[], initialValue: number = 1000): number[] {
const nviValues: number[] = [initialValue];
for (let i = 1; i < ohlcv.length; i++) {
if (ohlcv[i].volume < ohlcv[i - 1].volume) {
const change = (ohlcv[i].close - ohlcv[i - 1].close) / ohlcv[i - 1].close;
nviValues.push(nviValues[i - 1] + nviValues[i - 1] * change);
} else {
nviValues.push(nviValues[i - 1]);
}
}
return nviValues;
}
/**
* Typical Price
* Calculates the typical price for each period
*/
export function typicalPrice(ohlcv: OHLCVData[]): number[] {
return ohlcv.map(candle => (candle.high + candle.low + candle.close) / 3);
}
/**
* Median Price
* Calculates the median price for each period
*/
export function medianPrice(ohlcv: OHLCVData[]): number[] {
return ohlcv.map(candle => (candle.high + candle.low) / 2);
}
/**
* On Balance Volume Mean (OBV Mean)
* Calculates the mean of the On Balance Volume (OBV) values.
*/
export function onBalanceVolumeMean(ohlcv: OHLCVData[], period: number = 14): number[] {
const obvValues = obv(ohlcv);
return sma(obvValues, period);
}
/**
* Kaufman's Adaptive Moving Average (KAMA)
*/
export function kama(
prices: number[],
period: number = 10,
fastPeriod: number = 2,
slowPeriod: number = 30
): number[] {
const kamaValues: number[] = [];
if (prices.length <= period) {
return kamaValues;
}
// Calculate the initial KAMA using SMA
const firstSMA = prices.slice(0, period).reduce((sum, price) => sum + price, 0) / period;
let kama = firstSMA;
kamaValues.push(kama);
// Constants for the calculation
const fastConst = 2 / (fastPeriod + 1);
const slowConst = 2 / (slowPeriod + 1);
for (let i = period; i < prices.length; i++) {
// Calculate direction - the numerator of the efficiency ratio
const direction = Math.abs(prices[i] - prices[i - period]);
// Calculate volatility - the denominator of the efficiency ratio
let volatility = 0;
for (let j = i - period + 1; j <= i; j++) {
volatility += Math.abs(prices[j] - prices[j - 1]);
}
// Calculate efficiency ratio (ER)
// Handle the case where volatility is zero to avoid division by zero
const er = volatility === 0 ? 1 : Math.min(direction / volatility, 1);
// Calculate smoothing constant (SC)
const sc = Math.pow(er * (fastConst - slowConst) + slowConst, 2);
// Calculate KAMA
kama = kama + sc * (prices[i] - kama);
kamaValues.push(kama);
}
return kamaValues;
}
/**
* DeMarker
*/
export function deMarker(ohlcv: OHLCVData[], period: number = 14): number[] {
const deMax: number[] = [];
const deMin: number[] = [];
for (let i = 1; i < ohlcv.length; i++) {
deMax.push(ohlcv[i].high > ohlcv[i - 1].high ? ohlcv[i].high - ohlcv[i - 1].high : 0);
deMin.push(ohlcv[i].low < ohlcv[i - 1].low ? ohlcv[i - 1].low - ohlcv[i].low : 0);
}
const sumDeMax = sma(deMax, period);
const sumDeMin = sma(deMin, period);
const deMarkerValues: number[] = [];
for (let i = period; i < ohlcv.length; i++) {
deMarkerValues.push(sumDeMax[i - period] / (sumDeMax[i - period] + sumDeMin[i - period]));
}
return deMarkerValues;
}
/**
* Elder's SafeZone Stops
*/
export function eldersSafeZoneStops(
ohlcv: OHLCVData[],
atrPeriod: number = 20,
percentageRisk: number = 2
): { longStop: number[]; shortStop: number[] } {
const atrValues = atr(ohlcv, atrPeriod);
const longStop: number[] = [];
const shortStop: number[] = [];
for (let i = atrPeriod; i < ohlcv.length; i++) {
longStop.push(ohlcv[i].low - atrValues[i - atrPeriod] * (percentageRisk / 100));
shortStop.push(ohlcv[i].high + atrValues[i - atrPeriod] * (percentageRisk / 100));
}
return {
longStop,
shortStop,
};
}
/**
* Projection Oscillator
*/
export function projectionOscillator(ohlcv: OHLCVData[], period: number = 14): number[] {
const projectionOscillatorValues: number[] = [];
for (let i = period; i < ohlcv.length; i++) {
let highestHigh = ohlcv[i - period].high;
let lowestLow = ohlcv[i - period].low;
for (let j = i - period; j < i; j++) {
if (ohlcv[j].high > highestHigh) {
highestHigh = ohlcv[j].high;
}
if (ohlcv[j].low < lowestLow) {
lowestLow = ohlcv[j].low;
}
}
const projectionOscillatorValue =
((ohlcv[i].close - lowestLow) / (highestHigh - lowestLow)) * 100;
projectionOscillatorValues.push(projectionOscillatorValue);
}
return projectionOscillatorValues;
}
/**
* Twiggs Money Flow
*/
export function twiggsMoneyFlow(ohlcv: OHLCVData[]): number[] {
const twiggsMoneyFlowValues: number[] = [];
for (let i = 0; i < ohlcv.length; i++) {
const moneyFlowVolume =
ohlcv[i].volume *
((ohlcv[i].close - ohlcv[i].low - (ohlcv[i].high - ohlcv[i].close)) /
(ohlcv[i].high - ohlcv[i].low));
twiggsMoneyFlowValues.push(moneyFlowVolume);
}
return twiggsMoneyFlowValues;
}
/**
* Relative Strength
* Compares the performance of one asset to another
*/
export function relativeStrength(
prices1: number[],
prices2: number[],
period: number = 14
): number[] {
const rsValues: number[] = [];
const sma1 = sma(prices1, period);
const sma2 = sma(prices2, period);
for (let i = 0; i < sma1.length; i++) {
rsValues.push(sma1[i] / sma2[i]);
}
return rsValues;
}
/**
* Correlation Coefficient
* Measures the statistical relationship between two assets
*/
export function correlationCoefficient(
prices1: number[],
prices2: number[],
period: number = 14
): number[] {
const correlationValues: number[] = [];
for (let i = period; i < prices1.length; i++) {
const slice1 = prices1.slice(i - period, i);
const slice2 = prices2.slice(i - period, i);
const mean1 = slice1.reduce((a, b) => a + b, 0) / period;
const mean2 = slice2.reduce((a, b) => a + b, 0) / period;
let sumXY = 0;
let sumX2 = 0;
let sumY2 = 0;
for (let j = 0; j < period; j++) {
sumXY += (slice1[j] - mean1) * (slice2[j] - mean2);
sumX2 += Math.pow(slice1[j] - mean1, 2);
sumY2 += Math.pow(slice2[j] - mean2, 2);
}
const correlation = sumXY / (Math.sqrt(sumX2) * Math.sqrt(sumY2));
correlationValues.push(correlation);
}
return correlationValues;
}
/**
* Coppock Range
* Calculates the range between high and low Coppock values
*/
export function coppockRange(
prices: number[],
longPeriod: number = 14,
shortPeriod: number = 11,
wmaPeriod: number = 10
): { high: number[]; low: number[] } {
const coppockValues = coppockCurve(prices, longPeriod, shortPeriod, wmaPeriod);
const highValues: number[] = [];
const lowValues: number[] = [];
for (let i = 1; i < coppockValues.length; i++) {
highValues.push(Math.max(coppockValues[i], coppockValues[i - 1]));
lowValues.push(Math.min(coppockValues[i], coppockValues[i - 1]));
}
return {
high: highValues,
low: lowValues,
};
}
/**
* Chaikin Oscillator
* Calculates the difference between two moving averages of the Accumulation/Distribution Line
*/
export function chaikinOscillator(
ohlcv: OHLCVData[],
fastPeriod: number = 3,
slowPeriod: number = 10
): number[] {
const adlValues = accumulationDistribution(ohlcv);
const fastMA = ema(adlValues, fastPeriod);
const slowMA = ema(adlValues, slowPeriod);
const chaikinOscillatorValues: number[] = [];
for (let i = 0; i < fastMA.length; i++) {
chaikinOscillatorValues.push(fastMA[i] - slowMA[i]);
}
return chaikinOscillatorValues;
}
/**
* Prime Number Oscillator
* Uses prime numbers to create an oscillator
*/
export function primeNumberOscillator(prices: number[], period: number = 14): number[] {
const primeNumbers = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43]; // First 14 prime numbers
const pnoValues: number[] = [];
for (let i = period; i < prices.length; i++) {
let sum = 0;
for (let j = 0; j < period; j++) {
sum += prices[i - j] * primeNumbers[j];
}
pnoValues.push(sum);
}
return pnoValues;
}
/**
* Fractal Efficiency
* Measures the efficiency of price movement based on fractal dimension
*/
export function fractalEfficiency(ohlcv: OHLCVData[], period: number = 20): number[] {
const fractalEfficiencyValues: number[] = [];
for (let i = period; i < ohlcv.length; i++) {
let netDistance = 0;
for (let j = i; j > i - period; j--) {
netDistance += Math.sqrt(Math.pow(ohlcv[j].close - ohlcv[j - 1].close, 2));
}
const straightLineDistance = Math.sqrt(Math.pow(ohlcv[i].close - ohlcv[i - period].close, 2));
const fractalEfficiencyValue = straightLineDistance / netDistance;
fractalEfficiencyValues.push(fractalEfficiencyValue);
}
return fractalEfficiencyValues;
}
/**
* Market Facilitation Index (MFI)
*/
export function marketFacilitationIndex(ohlcv: OHLCVData[]): number[] {
const mfiValues: number[] = [];
for (let i = 0; i < ohlcv.length; i++) {
const range = ohlcv[i].high - ohlcv[i].low;
const mfiValue = range / ohlcv[i].volume;
mfiValues.push(mfiValue);
}
return mfiValues;
}
/**
* Elder-Disk
* Combination of Elder-Ray and Force Index
*/
export function elderDisk(ohlcv: OHLCVData[], period: number = 13): number[] {
const { bullPower, bearPower } = elderRay(ohlcv, period);
const forceIndexValues = forceIndex(ohlcv, period);
const elderDiskValues: number[] = [];
for (let i = 0; i < bullPower.length; i++) {
elderDiskValues.push(bullPower[i] + bearPower[i] + forceIndexValues[i]);
}
return elderDiskValues;
}
/**
* Relative Vigor Index (RVI)
*/
export function relativeVigorIndex(ohlcv: OHLCVData[], period: number = 10): number[] {
const rviValues: number[] = [];
for (let i = period; i < ohlcv.length; i++) {
let sumNumerator = 0;
let sumDenominator = 0;
for (let j = i; j > i - period; j--) {
sumNumerator += (ohlcv[j].close - ohlcv[j].open) * (ohlcv[j].high - ohlcv[j].low);
sumDenominator += (ohlcv[j].high - ohlcv[j].low) * (ohlcv[j].high - ohlcv[j].low);
}
const rviValue = sumDenominator !== 0 ? sumNumerator / sumDenominator : 0;
rviValues.push(rviValue);
}
return rviValues;
}
/**
* Balance of Power (BOP)
*/
export function balanceOfPower(ohlcv: OHLCVData[]): number[] {
const bopValues: number[] = [];
for (let i = 0; i < ohlcv.length; i++) {
const range = ohlcv[i].high - ohlcv[i].low;
const bopValue = range !== 0 ? (ohlcv[i].close - ohlcv[i].open) / range : 0;
bopValues.push(bopValue);
}
return bopValues;
}
/**
* Stochastic RSI
* Combines Stochastic Oscillator and RSI to provide overbought/oversold signals
*/
export function stochasticRSI(
prices: number[],
rsiPeriod: number = 14,
stochasticPeriod: number = 14,
smoothPeriod: number = 3
): { k: number[]; d: number[] } {
const rsiValues = rsi(prices, rsiPeriod);
return stochastic(
rsiValues.map(rsi => ({ high: rsi, low: rsi, close: rsi, open: rsi, volume: 0 }) as OHLCVData),
stochasticPeriod,
smoothPeriod
);
}
/**
* StochRSI Fast
*/
export function stochRSIFast(
prices: number[],
rsiPeriod: number = 14,
stochasticPeriod: number = 14
): { k: number[]; d: number[] } {
const rsiValues = rsi(prices, rsiPeriod);
return stochastic(
rsiValues.map(rsi => ({ high: rsi, low: rsi, close: rsi, open: rsi, volume: 0 }) as OHLCVData),
stochasticPeriod,
1
);
}
/**
* StochRSI Full
*/
export function stochRSIFull(
prices: number[],
rsiPeriod: number = 14,
stochasticPeriod: number = 14,
kSmoothPeriod: number = 3,
dSmoothPeriod: number = 3
): { k: number[]; d: number[] } {
const rsiValues = rsi(prices, rsiPeriod);
const { k } = stochastic(
rsiValues.map(rsi => ({ high: rsi, low: rsi, close: rsi, open: rsi, volume: 0 }) as OHLCVData),
stochasticPeriod,
kSmoothPeriod
);
const d = sma(k, dSmoothPeriod);
return { k, d };
}
/**
* Normalized Average True Range (NATR)
*/
export function normalizedAverageTrueRange(ohlcv: OHLCVData[], period: number = 14): number[] {
const atrValues = atr(ohlcv, period);
const natrValues: number[] = [];
for (let i = 0; i < atrValues.length; i++) {
natrValues.push((atrValues[i] / ohlcv[i].close) * 100);
}
return natrValues;
}
/**
* Pretty Good Oscillator (PGO)
*/
export function prettyGoodOscillator(ohlcv: OHLCVData[], period: number = 14): number[] {
const pgoValues: number[] = [];
for (let i = period; i < ohlcv.length; i++) {
let sumHighLow = 0;
let sumCloseOpen = 0;
for (let j = i; j > i - period; j--) {
sumHighLow += ohlcv[j].high - ohlcv[j].low;
sumCloseOpen += ohlcv[j].close - ohlcv[j].open;
}
const pgoValue = sumHighLow !== 0 ? sumCloseOpen / sumHighLow : 0;
pgoValues.push(pgoValue);
}
return pgoValues;
}
/**
* Intraday Intensity Index (III)
*/
export function intradayIntensityIndex(ohlcv: OHLCVData[]): number[] {
const iiiValues: number[] = [];
for (let i = 0; i < ohlcv.length; i++) {
const volume = ohlcv[i].volume;
const range = ohlcv[i].high - ohlcv[i].low;
const iiiValue =
range !== 0 ? ((2 * ohlcv[i].close - ohlcv[i].high - ohlcv[i].low) / range) * volume : 0;
iiiValues.push(iiiValue);
}
return iiiValues;
}
/**
* Money Flow Chaikin A/D Oscillator
* Uses the Chaikin A/D line to create an oscillator
*/
export function moneyFlowChaikinOscillator(
ohlcv: OHLCVData[],
fastPeriod: number = 3,
slowPeriod: number = 10
): number[] {
const adlValues = accumulationDistribution(ohlcv);
const fastMA = ema(adlValues, fastPeriod);
const slowMA = ema(adlValues, slowPeriod);
const moneyFlowChaikinOscillatorValues: number[] = [];
for (let i = 0; i < fastMA.length; i++) {
moneyFlowChaikinOscillatorValues.push(fastMA[i] - slowMA[i]);
}
return moneyFlowChaikinOscillatorValues;
}
/**
* Elder's Thermometer
* Uses high and low prices to gauge market temperature
*/
export function eldersThermometer(ohlcv: OHLCVData[], period: number = 20): number[] {
const eldersThermometerValues: number[] = [];
for (let i = period; i < ohlcv.length; i++) {
let sumOfHighs = 0;
let sumOfLows = 0;
for (let j = i; j > i - period; j--) {
sumOfHighs += ohlcv[j].high;
sumOfLows += ohlcv[j].low;
}
const averageHigh = sumOfHighs / period;
const averageLow = sumOfLows / period;
const thermometerValue = averageHigh - averageLow;
eldersThermometerValues.push(thermometerValue);
}
return eldersThermometerValues;
}
/**
* High-Low Range
* Calculates the range between high and low prices
*/
export function highLowRange(ohlcv: OHLCVData[]): number[] {
return ohlcv.map(candle => candle.high - candle.low);
}
/**
* Typical Price Range
* Calculates the range of typical prices
*/
export function typicalPriceRange(ohlcv: OHLCVData[]): number[] {
const typicalPrices = typicalPrice(ohlcv);
const typicalPriceRangeValues: number[] = [];
for (let i = 1; i < typicalPrices.length; i++) {
typicalPriceRangeValues.push(typicalPrices[i] - typicalPrices[i - 1]);
}
return typicalPriceRangeValues;
}
/**
* Median Price Range
* Calculates the range of median prices
*/
export function medianPriceRange(ohlcv: OHLCVData[]): number[] {
const medianPrices = medianPrice(ohlcv);
const medianPriceRangeValues: number[] = [];
for (let i = 1; i < medianPrices.length; i++) {
medianPriceRangeValues.push(medianPrices[i] - medianPrices[i - 1]);
}
return medianPriceRangeValues;
}
/**
* Center of Gravity
*/
export function centerOfGravity(prices: number[], period: number = 10): number[] {
const cogValues: number[] = [];
for (let i = period; i < prices.length; i++) {
let weightedSum = 0;
let sumOfWeights = 0;
for (let j = 1; j <= period; j++) {
weightedSum += j * prices[i - period + j];
sumOfWeights += j;
}
const cogValue = weightedSum / sumOfWeights;
cogValues.push(cogValue);
}
return cogValues;
}
/**
* Linear Regression Indicator
*/
export function linearRegressionIndicator(prices: number[], period: number = 14): number[] {
const lriValues: number[] = [];
if (prices.length < period) {
return lriValues;
}
for (let i = period; i < prices.length; i++) {
const slice = prices.slice(i - period, i);
// Calculate means for normalization (increases numerical stability)
const meanX = (period + 1) / 2; // Mean of 1,2,3,...,period
let meanY = 0;
for (let j = 0; j < period; j++) {
meanY += slice[j];
}
meanY /= period;
// Calculate covariance and variance with normalized data
let covariance = 0;
let variance = 0;
for (let j = 0; j < period; j++) {
const xDiff = j + 1 - meanX;
const yDiff = slice[j] - meanY;
covariance += xDiff * yDiff;
variance += xDiff * xDiff;
}
// Avoid division by zero
const slope = variance !== 0 ? covariance / variance : 0;
const intercept = meanY - slope * meanX;
// Calculate the predicted value at the end of the period
const lriValue = slope * period + intercept;
lriValues.push(lriValue);
}
return lriValues;
}
/**
* Standard Deviation
* Calculates the standard deviation of a set of values
*/
export function standardDeviation(prices: number[], period: number = 20): number[] {
const stdDevValues: number[] = [];
const smaValues = sma(prices, period);
for (let i = period - 1; i < prices.length; i++) {
const slice = prices.slice(i - period + 1, i + 1);
const mean = smaValues[i - period + 1];
let sumOfSquaredDifferences = 0;
for (const price of slice) {
sumOfSquaredDifferences += Math.pow(price - mean, 2);
}
const variance = sumOfSquaredDifferences / period;
const stdDevValue = Math.sqrt(variance);
stdDevValues.push(stdDevValue);
}
return stdDevValues;
}
/**
* Chaikin A/D Range
* Calculates the range of the Chaikin A/D line
*/
export function chaikinADRange(ohlcv: OHLCVData[]): number[] {
const adValues = accumulationDistribution(ohlcv);
const adRangeValues: number[] = [];
for (let i = 1; i < adValues.length; i++) {
adRangeValues.push(adValues[i] - adValues[i - 1]);
}
return adRangeValues;
}
/**
* Volume Oscillator
* Compares two moving averages of volume
*/
export function volumeOscillator(
ohlcv: OHLCVData[],
fastPeriod: number = 5,
slowPeriod: number = 10
): number[] {
const volumes = ohlcv.map(candle => candle.volume);
const fastMA = sma(volumes, fastPeriod);
const slowMA = sma(volumes, slowPeriod);
const volumeOscillatorValues: number[] = [];
for (let i = 0; i < fastMA.length; i++) {
volumeOscillatorValues.push(((fastMA[i] - slowMA[i]) / slowMA[i]) * 100);
}
return volumeOscillatorValues;
}
/**
* Money Flow Index Range
* Calculates the range of the Money Flow Index
*/
export function moneyFlowIndexRange(ohlcv: OHLCVData[], period: number = 14): number[] {
const mfiValues = mfi(ohlcv, period);
const mfiRangeValues: number[] = [];
for (let i = 1; i < mfiValues.length; i++) {
mfiRangeValues.push(mfiValues[i] - mfiValues[i - 1]);
}
return mfiRangeValues;
}
/**
* On Balance Volume Oscillator
* Calculates the oscillator of the On Balance Volume
*/
export function onBalanceVolumeOscillator(
ohlcv: OHLCVData[],
fastPeriod: number = 5,
slowPeriod: number = 10
): number[] {
const obvValues = obv(ohlcv);
const fastMA = sma(obvValues, fastPeriod);
const slowMA = sma(obvValues, slowPeriod);
const obvOscillatorValues: number[] = [];
for (let i = 0; i < fastMA.length; i++) {
obvOscillatorValues.push(((fastMA[i] - slowMA[i]) / slowMA[i]) * 100);
}
return obvOscillatorValues;
}
/**
* Klinger Oscillator
*/
export function klingerOscillator(
ohlcv: OHLCVData[],
fastPeriod: number = 34,
slowPeriod: number = 55
): number[] {
if (ohlcv.length < 2) {
return [];
}
// Calculate volume force
const volumeForce: number[] = [];
for (let i = 1; i < ohlcv.length; i++) {
const current = ohlcv[i];
const previous = ohlcv[i - 1];
// Calculate typical prices
const typicalPriceCurrent = (current.high + current.low + current.close) / 3;
const typicalPricePrevious = (previous.high + previous.low + previous.close) / 3;
// Determine trend
const trend = typicalPriceCurrent > typicalPricePrevious ? 1 : -1;
// Calculate volume force
const force = trend * ohlcv[i].volume * Math.abs(typicalPriceCurrent - typicalPricePrevious);
volumeForce.push(force);
}
// Calculate fast and slow EMAs of the volume force
const fastEMA = ema(volumeForce, fastPeriod);
const slowEMA = ema(volumeForce, slowPeriod);
// Calculate Klinger Oscillator
const klingerOscillatorValues: number[] = [];
// Both EMAs should have the same starting point
const startIndex = Math.abs(fastEMA.length - slowEMA.length);
const shorterEMA = fastEMA.length < slowEMA.length ? fastEMA : slowEMA;
const longerEMA = fastEMA.length < slowEMA.length ? slowEMA : fastEMA;
for (let i = 0; i < shorterEMA.length; i++) {
if (fastEMA.length < slowEMA.length) {
klingerOscillatorValues.push(shorterEMA[i] - longerEMA[i + startIndex]);
} else {
klingerOscillatorValues.push(longerEMA[i + startIndex] - shorterEMA[i]);
}
}
return klingerOscillatorValues;
}
/**
* Directional Movement Index (DMI)
*/
export function directionalMovementIndex(
ohlcv: OHLCVData[],
period: number = 14
): { plusDI: number[]; minusDI: number[] } {
const { plusDI, minusDI } = adx(ohlcv, period);
return { plusDI, minusDI };
}
/**
* Elder's Cloud
*/
export function eldersCloud(
ohlcv: OHLCVData[],
period: number = 20
): { upper: number[]; lower: number[] } {
const emaValues = ema(
ohlcv.map(item => item.close),
period
);
const atrValues = atr(ohlcv, period);
const upper: number[] = [];
const lower: number[] = [];
for (let i = 0; i < emaValues.length; i++) {
upper.push(emaValues[i] + atrValues[i]);
lower.push(emaValues[i] - atrValues[i]);
}
return {
upper,
lower,
};
}
/**
* Ultimate Moving Average (UMA)
*/
export function ultimateMovingAverage(
prices: number[],
fastPeriod: number = 7,
mediumPeriod: number = 14,
slowPeriod: number = 28
): number[] {
const fastMA = sma(prices, fastPeriod);
const mediumMA = sma(prices, mediumPeriod);
const slowMA = sma(prices, slowPeriod);
const umaValues: number[] = [];
for (let i = 0; i < fastMA.length; i++) {
umaValues.push((fastMA[i] + mediumMA[i] + slowMA[i]) / 3);
}
return umaValues;
}
/**
* Rainbow Oscillator
*/
export function rainbowOscillator(
prices: number[],
numberOfMAs: number = 7,
periodIncrement: number = 5
): number[] {
const maValues: number[][] = [];
for (let i = 1; i <= numberOfMAs; i++) {
maValues.push(sma(prices, i * periodIncrement));
}
const rainbowOscillatorValues: number[] = [];
for (let i = 0; i < maValues[0].length; i++) {
let sum = 0;
for (let j = 0; j < numberOfMAs; j++) {
sum += maValues[j][i];
}
rainbowOscillatorValues.push(sum / numberOfMAs);
}
return rainbowOscillatorValues;
}
/**
* Guppy Multiple Moving Average (GMMA)
*/
export function guppyMultipleMovingAverage(
prices: number[],
shortTermPeriods: number[] = [3, 5, 8, 10, 12, 15],
longTermPeriods: number[] = [30, 35, 40, 45, 50, 60]
): { shortTermMAs: number[][]; longTermMAs: number[][] } {
const shortTermMAs: number[][] = [];
const longTermMAs: number[][] = [];
for (const period of shortTermPeriods) {
shortTermMAs.push(sma(prices, period));
}
for (const period of longTermPeriods) {
longTermMAs.push(sma(prices, period));
}
return { shortTermMAs, longTermMAs };
}
/**
* Historical Volatility
*/
export function historicalVolatility(prices: number[], period: number = 20): number[] {
const logReturns: number[] = [];
for (let i = 1; i < prices.length; i++) {
logReturns.push(Math.log(prices[i] / prices[i - 1]));
}
const stdDevs = standardDeviation(logReturns, period);
const historicalVolatilityValues: number[] = [];
for (const stdDev of stdDevs) {
historicalVolatilityValues.push(stdDev * Math.sqrt(252)); // Annualize
}
return historicalVolatilityValues;
}
/**
* Donchian Width
*/
export function donchianWidth(ohlcv: OHLCVData[], period: number = 20): number[] {
const { upper, lower } = donchianChannels(ohlcv, period);
const donchianWidthValues: number[] = [];
for (let i = 0; i < upper.length; i++) {
donchianWidthValues.push(upper[i] - lower[i]);
}
return donchianWidthValues;
}
/**
* Chandelier Exit
*/
export function chandelierExit(
ohlcv: OHLCVData[],
period: number = 22,
multiplier: number = 3
): { long: number[]; short: number[] } {
const atrValues = atr(ohlcv, period);
const long: number[] = [];
const short: number[] = [];
for (let i = period; i < ohlcv.length; i++) {
const slice = ohlcv.slice(i - period, i);
const highestHigh = Math.max(...slice.map(item => item.high));
const lowestLow = Math.min(...slice.map(item => item.low));
long.push(highestHigh - multiplier * atrValues[i - period]);
short.push(lowestLow + multiplier * atrValues[i - period]);
}
return { long, short };
}
/**
* Projection Bands
*/
export function projectionBands(
ohlcv: OHLCVData[],
period: number = 14,
stdDevMultiplier: number = 2
): { upper: number[]; lower: number[] } {
const projectionOscillatorValues = projectionOscillator(ohlcv, period);
const stdDevValues = standardDeviation(projectionOscillatorValues, period);
const upper: number[] = [];
const lower: number[] = [];
for (let i = 0; i < projectionOscillatorValues.length; i++) {
upper.push(projectionOscillatorValues[i] + stdDevMultiplier * stdDevValues[i]);
lower.push(projectionOscillatorValues[i] - stdDevMultiplier * stdDevValues[i]);
}
return { upper, lower };
}
/**
* Range Action Verification Index (RAVI)
*/
export function rangeActionVerificationIndex(
prices: number[],
longPeriod: number = 65,
shortPeriod: number = 10
): number[] {
const longMA = sma(prices, longPeriod);
const shortMA = sma(prices, shortPeriod);
const raviValues: number[] = [];
for (let i = 0; i < longMA.length; i++) {
raviValues.push(((shortMA[i] - longMA[i]) / longMA[i]) * 100);
}
return raviValues;
}
/**
* Momentum from Current Price
* Calculates momentum using the current price and a previous price. Reduces lag compared to using moving averages.
*/
export function momentumFromCurrentPrice(prices: number[], period: number = 10): number[] {
const result: number[] = [];
for (let i = period; i < prices.length; i++) {
const momentum = prices[i] - prices[i - period];
result.push(momentum);
}
return result;
}
/**
* Rate of Change from Current Price (ROC)
* Calculates ROC using the current price.
*/
export function rocFromCurrentPrice(prices: number[], period: number = 10): number[] {
const result: number[] = [];
for (let i = period; i < prices.length; i++) {
if (prices[i - period] === 0) {
result.push(0);
} else {
const rocValue = ((prices[i] - prices[i - period]) / prices[i - period]) * 100;
result.push(rocValue);
}
}
return result;
}