/**
 * Technical Indicators
 * Comprehensive set of technical analysis indicators
 */

import type { OHLCV, HasOHLC } 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);
}

/**
 * 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<T extends HasOHLC>(ohlcv: T[], 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 currentBar = ohlcv[i];
    if (!currentBar) continue;
    const currentClose = currentBar.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<T extends HasOHLC>(ohlcv: T[], 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;
}

/**
 * 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;
}

/**
 * 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) => 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) {
        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;
}

/**
 * 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;
}

/**
 * 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;
}

/**
 * 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;
}

/**
 * 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;
}

/**
 * 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),
  };
}

/**
 * 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;
}

/**
 * Pivot Points (Standard)
 */
export function pivotPoints(ohlcv: OHLCV[]): 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: OHLCV[],
  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: OHLCV[],
  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: OHLCV[],
  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: OHLCV[],
  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: OHLCV[], 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: OHLCV[], 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: OHLCV[], 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: OHLCV[],
  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: OHLCV[],
  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: OHLCV[], 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: OHLCV[],
  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: OHLCV[]): 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: OHLCV[], 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: OHLCV[], 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: OHLCV[], 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: OHLCV[],
  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: OHLCV[], 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: OHLCV[]): 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: OHLCV[], 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: OHLCV[], 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: OHLCV[]): 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: OHLCV[]): 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: OHLCV[], 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: OHLCV[], 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: OHLCV[],
  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: OHLCV[], 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: OHLCV[]): 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: OHLCV[],
  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: OHLCV[], 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: OHLCV[]): 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: OHLCV[], 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: OHLCV[], 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: OHLCV[]): 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 OHLCV),
    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 OHLCV),
    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 OHLCV),
    stochasticPeriod,
    kSmoothPeriod
  );
  const d = sma(k, dSmoothPeriod);
  return { k, d };
}

/**
 *  Normalized Average True Range (NATR)
 */
export function normalizedAverageTrueRange(ohlcv: OHLCV[], 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: OHLCV[], 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: OHLCV[]): 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: OHLCV[],
  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: OHLCV[], 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: OHLCV[]): number[] {
  return ohlcv.map(candle => candle.high - candle.low);
}

/**
 *  Typical Price Range
 *  Calculates the range of typical prices
 */
export function typicalPriceRange(ohlcv: OHLCV[]): 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: OHLCV[]): 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: OHLCV[]): 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: OHLCV[],
  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: OHLCV[], 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: OHLCV[],
  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: OHLCV[],
  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: OHLCV[],
  period: number = 14
): { plusDI: number[]; minusDI: number[] } {
  const { plusDI, minusDI } = adx(ohlcv, period);
  return { plusDI, minusDI };
}

/**
 *  Elder's Cloud
 */
export function eldersCloud(
  ohlcv: OHLCV[],
  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: OHLCV[], 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: OHLCV[],
  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: OHLCV[],
  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;
}
