1322 lines
33 KiB
TypeScript
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
|
|
}
|