2445 lines
62 KiB
TypeScript
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;
|
|
}
|