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

import type { OHLCV } from '@stock-bot/types';

/**
 * Simple Moving Average
 */
export function sma(values: number[], period: number): number[] {
  if (period > values.length) {
    return [];
  }

  const result: number[] = [];

  for (let i = period - 1; i < values.length; i++) {
    const sum = values.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0);
    result.push(sum / period);
  }

  return result;
}

/**
 * Exponential Moving Average
 */
export function ema(values: number[], period: number): number[] {
  if (period > values.length) {
    return [];
  }

  const result: number[] = [];
  const multiplier = 2 / (period + 1);

  // Start with SMA for first value
  let ema = values.slice(0, period).reduce((a, b) => a + b, 0) / period;
  result.push(ema);

  for (let i = period; i < values.length; i++) {
    const value = values[i];
    if (value !== undefined) {
      ema = value * multiplier + ema * (1 - multiplier);
      result.push(ema);
    }
  }

  return result;
}

/**
 * Relative Strength Index (RSI)
 */
export function rsi(prices: number[], period: number = 14): number[] {
  if (period >= prices.length) {
    return [];
  }

  const gains: number[] = [];
  const losses: number[] = [];

  // Calculate gains and losses
  for (let i = 1; i < prices.length; i++) {
    const current = prices[i];
    const previous = prices[i - 1];
    if (current !== undefined && previous !== undefined) {
      const change = current - previous;
      gains.push(change > 0 ? change : 0);
      losses.push(change < 0 ? Math.abs(change) : 0);
    }
  }

  const result: number[] = [];

  // Calculate RSI
  for (let i = period - 1; i < gains.length; i++) {
    const avgGain = gains.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0) / period;
    const avgLoss = losses.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0) / period;

    if (avgLoss === 0) {
      result.push(100);
    } else {
      const rs = avgGain / avgLoss;
      const rsiValue = 100 - 100 / (1 + rs);
      result.push(rsiValue);
    }
  }

  return result;
}

/**
 * Moving Average Convergence Divergence (MACD)
 */
export function macd(
  prices: number[],
  fastPeriod: number = 12,
  slowPeriod: number = 26,
  signalPeriod: number = 9
): { macd: number[]; signal: number[]; histogram: number[] } {
  const fastEMA = ema(prices, fastPeriod);
  const slowEMA = ema(prices, slowPeriod);

  const macdLine: number[] = [];
  const startIndex = slowPeriod - fastPeriod;

  for (let i = 0; i < fastEMA.length - startIndex; i++) {
    const fastValue = fastEMA[i + startIndex];
    const slowValue = slowEMA[i];
    if (fastValue !== undefined && slowValue !== undefined) {
      macdLine.push(fastValue - slowValue);
    }
  }

  const signalLine = ema(macdLine, signalPeriod);
  const histogram: number[] = [];

  const signalStartIndex = signalPeriod - 1;
  for (let i = 0; i < signalLine.length; i++) {
    const macdValue = macdLine[i + signalStartIndex];
    const signalValue = signalLine[i];
    if (macdValue !== undefined && signalValue !== undefined) {
      histogram.push(macdValue - signalValue);
    }
  }

  return {
    macd: macdLine,
    signal: signalLine,
    histogram: histogram,
  };
}

/**
 * Bollinger Bands
 */
export function bollingerBands(
  prices: number[],
  period: number = 20,
  standardDeviations: number = 2
): { upper: number[]; middle: number[]; lower: number[] } {
  const middle = sma(prices, period);
  const upper: number[] = [];
  const lower: number[] = [];

  for (let i = period - 1; i < prices.length; i++) {
    const slice = prices.slice(i - period + 1, i + 1);
    const mean = slice.reduce((a, b) => a + b, 0) / period;
    const variance = slice.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / period;
    const stdDev = Math.sqrt(variance);

    const middleValue = middle[i - period + 1];
    if (middleValue !== undefined) {
      upper.push(middleValue + standardDeviations * stdDev);
      lower.push(middleValue - standardDeviations * stdDev);
    }
  }

  return { upper, middle, lower };
}

/**
 * Average True Range (ATR)
 */
export function atr(ohlcv: OHLCV[], period: number = 14): number[] {
  if (period >= ohlcv.length) {
    return [];
  }

  const trueRanges: number[] = [];

  for (let i = 1; i < ohlcv.length; i++) {
    const current = ohlcv[i];
    const previous = ohlcv[i - 1];
    if (current && previous) {
      const high = current.high;
      const low = current.low;
      const prevClose = previous.close;

      const tr = Math.max(high - low, Math.abs(high - prevClose), Math.abs(low - prevClose));
      trueRanges.push(tr);
    }
  }

  return sma(trueRanges, period);
}

/**
 * On-Balance Volume (OBV)
 */
export function obv(ohlcv: OHLCV[]): number[] {
  if (ohlcv.length === 0) {
    return [];
  }

  const first = ohlcv[0];
  if (!first) {
    return [];
  }
  const result: number[] = [first.volume];

  for (let i = 1; i < ohlcv.length; i++) {
    const prev = ohlcv[i - 1];
    const curr = ohlcv[i];
    if (!prev || !curr) {
      continue;
    }

    const lastValue = result[result.length - 1];
    if (lastValue === undefined) {
      continue;
    }

    if (curr.close > prev.close) {
      result.push(lastValue + curr.volume);
    } else if (curr.close < prev.close) {
      result.push(lastValue - curr.volume);
    } else {
      result.push(lastValue);
    }
  }

  return result;
}

/**
 * Stochastic Oscillator
 */
export function stochastic(
  ohlcv: OHLCV[],
  kPeriod: number = 14,
  dPeriod: number = 3
): { k: number[]; d: number[] } {
  if (kPeriod >= ohlcv.length) {
    return { k: [], d: [] };
  }

  const kValues: number[] = [];

  for (let i = kPeriod - 1; i < ohlcv.length; i++) {
    const slice = ohlcv.slice(i - kPeriod + 1, i + 1);
    const highest = Math.max(...slice.map(d => d.high));
    const lowest = Math.min(...slice.map(d => d.low));
    const current = ohlcv[i];
    if (!current) {
      continue;
    }
    const currentClose = current.close;

    if (highest === lowest) {
      kValues.push(50); // Avoid division by zero
    } else {
      const kValue = ((currentClose - lowest) / (highest - lowest)) * 100;
      kValues.push(kValue);
    }
  }

  const dValues = sma(kValues, dPeriod);

  return { k: kValues, d: dValues };
}

/**
 * Williams %R
 */
export function williamsR(ohlcv: OHLCV[], period: number = 14): number[] {
  if (period >= ohlcv.length) {
    return [];
  }

  const result: number[] = [];

  for (let i = period - 1; i < ohlcv.length; i++) {
    const slice = ohlcv.slice(i - period + 1, i + 1);
    const highest = Math.max(...slice.map(d => d.high));
    const lowest = Math.min(...slice.map(d => d.low));
    const current = ohlcv[i];
    if (!current) {
      continue;
    }
    const currentClose = current.close;

    if (highest === lowest) {
      result.push(-50); // Avoid division by zero
    } else {
      const wrValue = ((highest - currentClose) / (highest - lowest)) * -100;
      result.push(wrValue);
    }
  }

  return result;
}

/**
 * Commodity Channel Index (CCI)
 */
export function cci(ohlcv: OHLCV[], period: number = 20): number[] {
  if (period >= ohlcv.length) {
    return [];
  }

  const typicalPrices = ohlcv.map(d => (d.high + d.low + d.close) / 3);
  const smaTP = sma(typicalPrices, period);
  const result: number[] = [];

  for (let i = 0; i < smaTP.length; i++) {
    const slice = typicalPrices.slice(i, i + period);
    const mean = smaTP[i];
    if (mean === undefined) {
      continue;
    }
    const meanDeviation = slice.reduce((sum, value) => sum + Math.abs(value - mean), 0) / period;
    const typicalPrice = typicalPrices[i + period - 1];
    if (typicalPrice === undefined) {
      continue;
    }

    if (meanDeviation === 0) {
      result.push(0);
    } else {
      const cciValue = (typicalPrice - mean) / (0.015 * meanDeviation);
      result.push(cciValue);
    }
  }

  return result;
}

/**
 * Money Flow Index (MFI)
 */
export function mfi(ohlcv: OHLCV[], period: number = 14): number[] {
  if (period >= ohlcv.length) {
    return [];
  }

  const typicalPrices = ohlcv.map(d => (d.high + d.low + d.close) / 3);
  const moneyFlows = ohlcv.map((d, i) => {
    const tp = typicalPrices[i];
    return tp !== undefined ? tp * d.volume : 0;
  });

  const result: number[] = [];

  for (let i = period; i < ohlcv.length; i++) {
    let positiveFlow = 0;
    let negativeFlow = 0;

    for (let j = i - period + 1; j <= i; j++) {
      if (j > 0) {
        const currentTP = typicalPrices[j];
        const prevTP = typicalPrices[j - 1];
        const currentMF = moneyFlows[j];
        if (currentTP !== undefined && prevTP !== undefined && currentMF !== undefined) {
          if (currentTP > prevTP) {
            positiveFlow += currentMF;
          } else if (currentTP < prevTP) {
            negativeFlow += currentMF;
          }
        }
      }
    }

    if (negativeFlow === 0) {
      result.push(100);
    } else {
      const mfiRatio = positiveFlow / negativeFlow;
      const mfiValue = 100 - 100 / (1 + mfiRatio);
      result.push(mfiValue);
    }
  }

  return result;
}

/**
 * Volume Weighted Moving Average (VWMA)
 */
export function vwma(ohlcv: OHLCV[], period: number = 20): number[] {
  if (period >= ohlcv.length) {
    return [];
  }

  const result: number[] = [];

  for (let i = period - 1; i < ohlcv.length; i++) {
    const slice = ohlcv.slice(i - period + 1, i + 1);

    let totalVolumePrice = 0;
    let totalVolume = 0;

    for (const candle of slice) {
      const typicalPrice = (candle.high + candle.low + candle.close) / 3;
      totalVolumePrice += typicalPrice * candle.volume;
      totalVolume += candle.volume;
    }

    const vwmaValue = totalVolume > 0 ? totalVolumePrice / totalVolume : 0;
    result.push(vwmaValue);
  }

  return result;
}

/**
 * Momentum
 */
export function momentum(prices: number[], period: number = 10): number[] {
  if (period >= prices.length) {
    return [];
  }

  const result: number[] = [];

  for (let i = period; i < prices.length; i++) {
    const current = prices[i];
    const previous = prices[i - period];
    if (current !== undefined && previous !== undefined) {
      const momentum = current - previous;
      result.push(momentum);
    }
  }

  return result;
}

/**
 * Rate of Change (ROC)
 */
export function roc(prices: number[], period: number = 10): number[] {
  if (period >= prices.length) {
    return [];
  }

  const result: number[] = [];

  for (let i = period; i < prices.length; i++) {
    const current = prices[i];
    const previous = prices[i - period];
    if (current !== undefined && previous !== undefined) {
      if (previous === 0) {
        result.push(0);
      } else {
        const rocValue = ((current - previous) / previous) * 100;
        result.push(rocValue);
      }
    }
  }

  return result;
}

/**
 * Accumulation/Distribution Line
 */
export function accumulationDistribution(ohlcv: OHLCV[]): number[] {
  if (ohlcv.length === 0) {
    return [];
  }

  const result: number[] = [];
  let adLine = 0;

  for (const candle of ohlcv) {
    if (candle.high === candle.low) {
      // Avoid division by zero
      result.push(adLine);
      continue;
    }

    const moneyFlowMultiplier =
      (candle.close - candle.low - (candle.high - candle.close)) / (candle.high - candle.low);
    const moneyFlowVolume = moneyFlowMultiplier * candle.volume;
    adLine += moneyFlowVolume;
    result.push(adLine);
  }

  return result;
}

/**
 * Aroon Indicator
 */
export function aroon(ohlcv: OHLCV[], period: number = 14): { up: number[]; down: number[] } {
  if (period >= ohlcv.length) {
    return { up: [], down: [] };
  }

  const up: number[] = [];
  const down: number[] = [];

  for (let i = period - 1; i < ohlcv.length; i++) {
    const slice = ohlcv.slice(i - period + 1, i + 1);

    // Find highest high and lowest low positions
    let highestIndex = 0;
    let lowestIndex = 0;

    for (let j = 1; j < slice.length; j++) {
      const current = slice[j];
      const highest = slice[highestIndex];
      const lowest = slice[lowestIndex];
      if (current && highest && lowest) {
        if (current.high > highest.high) {
          highestIndex = j;
        }
        if (current.low < lowest.low) {
          lowestIndex = j;
        }
      }
    }

    const aroonUp = ((period - 1 - highestIndex) / (period - 1)) * 100;
    const aroonDown = ((period - 1 - lowestIndex) / (period - 1)) * 100;

    up.push(aroonUp);
    down.push(aroonDown);
  }

  return { up, down };
}

/**
 * Average Directional Movement Index (ADX) and Directional Movement Indicators (DMI)
 */
export function adx(
  ohlcv: OHLCV[],
  period: number = 14
): { adx: number[]; plusDI: number[]; minusDI: number[] } {
  if (period >= ohlcv.length) {
    return { adx: [], plusDI: [], minusDI: [] };
  }

  const trueRanges: number[] = [];
  const plusDM: number[] = [];
  const minusDM: number[] = [];

  // Calculate True Range and Directional Movements
  for (let i = 1; i < ohlcv.length; i++) {
    const current = ohlcv[i];
    const previous = ohlcv[i - 1];
    if (!current || !previous) {
      continue;
    }

    // True Range
    const tr = Math.max(
      current.high - current.low,
      Math.abs(current.high - previous.close),
      Math.abs(current.low - previous.close)
    );
    trueRanges.push(tr);

    // Directional Movements
    const highDiff = current.high - previous.high;
    const lowDiff = previous.low - current.low;

    const plusDMValue = highDiff > lowDiff && highDiff > 0 ? highDiff : 0;
    const minusDMValue = lowDiff > highDiff && lowDiff > 0 ? lowDiff : 0;

    plusDM.push(plusDMValue);
    minusDM.push(minusDMValue);
  }

  // Calculate smoothed averages
  const atrValues = sma(trueRanges, period);
  const smoothedPlusDM = sma(plusDM, period);
  const smoothedMinusDM = sma(minusDM, period);

  const plusDI: number[] = [];
  const minusDI: number[] = [];
  const dx: number[] = [];

  // Calculate DI+ and DI-
  for (let i = 0; i < atrValues.length; i++) {
    const atr = atrValues[i];
    const plusDMSmoothed = smoothedPlusDM[i];
    const minusDMSmoothed = smoothedMinusDM[i];
    if (atr === undefined || plusDMSmoothed === undefined || minusDMSmoothed === undefined) {
      continue;
    }

    const diPlus = atr > 0 ? (plusDMSmoothed / atr) * 100 : 0;
    const diMinus = atr > 0 ? (minusDMSmoothed / atr) * 100 : 0;

    plusDI.push(diPlus);
    minusDI.push(diMinus);

    // Calculate DX
    const diSum = diPlus + diMinus;
    const dxValue = diSum > 0 ? (Math.abs(diPlus - diMinus) / diSum) * 100 : 0;
    dx.push(dxValue);
  }

  // Calculate ADX (smoothed DX)
  const adxValues = sma(dx, period);

  return {
    adx: adxValues,
    plusDI: plusDI.slice(period - 1),
    minusDI: minusDI.slice(period - 1),
  };
}

/**
 * Parabolic SAR
 */
export function parabolicSAR(ohlcv: OHLCV[], step: number = 0.02, maxStep: number = 0.2): number[] {
  if (ohlcv.length < 2) {
    return [];
  }

  const first = ohlcv[0];
  if (!first) {
    return [];
  }

  const result: number[] = [];
  let trend = 1; // 1 for uptrend, -1 for downtrend
  let acceleration = step;
  let extremePoint = first.high;
  let sar = first.low;

  result.push(sar);

  for (let i = 1; i < ohlcv.length; i++) {
    const curr = ohlcv[i];
    const prev = ohlcv[i - 1];
    if (!curr || !prev) {
      continue;
    }

    // Calculate new SAR
    sar = sar + acceleration * (extremePoint - sar);

    if (trend === 1) {
      // Uptrend
      if (curr.low <= sar) {
        // Trend reversal
        trend = -1;
        sar = extremePoint;
        extremePoint = curr.low;
        acceleration = step;
      } else {
        if (curr.high > extremePoint) {
          extremePoint = curr.high;
          acceleration = Math.min(acceleration + step, maxStep);
        }
        // Ensure SAR doesn't exceed previous lows
        const prevPrev = i > 1 ? ohlcv[i - 2] : null;
        sar = Math.min(sar, prev.low, prevPrev ? prevPrev.low : prev.low);
      }
    } else {
      // Downtrend
      if (curr.high >= sar) {
        // Trend reversal
        trend = 1;
        sar = extremePoint;
        extremePoint = curr.high;
        acceleration = step;
      } else {
        if (curr.low < extremePoint) {
          extremePoint = curr.low;
          acceleration = Math.min(acceleration + step, maxStep);
        }
        // Ensure SAR doesn't exceed previous highs
        const prevPrev = i > 1 ? ohlcv[i - 2] : null;
        sar = Math.max(sar, prev.high, prevPrev ? prevPrev.high : prev.high);
      }
    }

    result.push(sar);
  }

  return result;
}

/**
 * Chaikin Money Flow (CMF)
 */
export function chaikinMoneyFlow(ohlcv: OHLCV[], period: number = 20): number[] {
  if (period >= ohlcv.length) {
    return [];
  }

  const adValues: number[] = [];

  for (const candle of ohlcv) {
    if (candle.high === candle.low) {
      adValues.push(0);
    } else {
      const moneyFlowMultiplier =
        (candle.close - candle.low - (candle.high - candle.close)) / (candle.high - candle.low);
      const moneyFlowVolume = moneyFlowMultiplier * candle.volume;
      adValues.push(moneyFlowVolume);
    }
  }

  const result: number[] = [];

  for (let i = period - 1; i < ohlcv.length; i++) {
    const sumAD = adValues.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0);
    const sumVolume = ohlcv.slice(i - period + 1, i + 1).reduce((a, b) => a + b.volume, 0);

    if (sumVolume === 0) {
      result.push(0);
    } else {
      result.push(sumAD / sumVolume);
    }
  }

  return result;
}

/**
 * Pivot Points (Standard)
 */
export function pivotPoints(ohlcv: OHLCV[]): Array<{
  pivot: number;
  r1: number;
  r2: number;
  r3: number;
  s1: number;
  s2: number;
  s3: number;
}> {
  const result: Array<{
    pivot: number;
    r1: number;
    r2: number;
    r3: number;
    s1: number;
    s2: number;
    s3: number;
  }> = [];

  for (const candle of ohlcv) {
    const pivot = (candle.high + candle.low + candle.close) / 3;
    const r1 = 2 * pivot - candle.low;
    const s1 = 2 * pivot - candle.high;
    const r2 = pivot + (candle.high - candle.low);
    const s2 = pivot - (candle.high - candle.low);
    const r3 = candle.high + 2 * (pivot - candle.low);
    const s3 = candle.low - 2 * (candle.high - pivot);

    result.push({ pivot, r1, r2, r3, s1, s2, s3 });
  }

  return result;
}

/**
 * Ichimoku Cloud components
 */
export function ichimoku(
  ohlcv: OHLCV[],
  tenkanPeriod: number = 9,
  kijunPeriod: number = 26,
  senkouBPeriod: number = 52,
  _displacement: number = 26
): {
  tenkanSen: number[];
  kijunSen: number[];
  senkouSpanA: number[];
  senkouSpanB: number[];
  chikouSpan: number[];
} {
  const tenkanSen: number[] = [];
  const kijunSen: number[] = [];
  const senkouSpanA: number[] = [];
  const senkouSpanB: number[] = [];
  const chikouSpan: number[] = [];

  for (let i = 0; i < ohlcv.length; i++) {
    // Tenkan-sen (Conversion Line)
    if (i >= tenkanPeriod - 1) {
      const slice = ohlcv.slice(i - tenkanPeriod + 1, i + 1);
      const high = Math.max(...slice.map(d => d.high));
      const low = Math.min(...slice.map(d => d.low));
      tenkanSen.push((high + low) / 2);
    }

    // Kijun-sen (Base Line)
    if (i >= kijunPeriod - 1) {
      const slice = ohlcv.slice(i - kijunPeriod + 1, i + 1);
      const high = Math.max(...slice.map(d => d.high));
      const low = Math.min(...slice.map(d => d.low));
      kijunSen.push((high + low) / 2);
    }

    // Senkou Span A (Leading Span A)
    if (tenkanSen.length > 0 && kijunSen.length > 0) {
      const tenkanValue = tenkanSen[tenkanSen.length - 1];
      const kijunValue = kijunSen[kijunSen.length - 1];
      if (tenkanValue !== undefined && kijunValue !== undefined) {
        senkouSpanA.push((tenkanValue + kijunValue) / 2);
      }
    }

    // Senkou Span B (Leading Span B)
    if (i >= senkouBPeriod - 1) {
      const slice = ohlcv.slice(i - senkouBPeriod + 1, i + 1);
      const high = Math.max(...slice.map(d => d.high));
      const low = Math.min(...slice.map(d => d.low));
      senkouSpanB.push((high + low) / 2);
    }

    // Chikou Span (Lagging Span)
    chikouSpan.push(ohlcv[i]!.close);
  }

  return {
    tenkanSen,
    kijunSen,
    senkouSpanA,
    senkouSpanB,
    chikouSpan,
  };
}

/**
 * Ultimate Oscillator
 */
export function ultimateOscillator(
  ohlcv: OHLCV[],
  period1: number = 7,
  period2: number = 14,
  period3: number = 28
): number[] {
  if (ohlcv.length < Math.max(period1, period2, period3)) {
    return [];
  }

  const bp: number[] = []; // Buying Pressure
  const tr: number[] = []; // True Range

  // Calculate BP and TR
  for (let i = 0; i < ohlcv.length; i++) {
    const current = ohlcv[i]!;

    if (i === 0) {
      bp.push(current.close - current.low);
      tr.push(current.high - current.low);
    } else {
      const previous = ohlcv[i - 1]!;
      bp.push(current.close - Math.min(current.low, previous.close));
      tr.push(
        Math.max(
          current.high - current.low,
          Math.abs(current.high - previous.close),
          Math.abs(current.low - previous.close)
        )
      );
    }
  }

  const result: number[] = [];

  for (let i = Math.max(period1, period2, period3) - 1; i < ohlcv.length; i++) {
    const avg1 =
      bp.slice(i - period1 + 1, i + 1).reduce((a, b) => a + b, 0) /
      tr.slice(i - period1 + 1, i + 1).reduce((a, b) => a + b, 0);
    const avg2 =
      bp.slice(i - period2 + 1, i + 1).reduce((a, b) => a + b, 0) /
      tr.slice(i - period2 + 1, i + 1).reduce((a, b) => a + b, 0);
    const avg3 =
      bp.slice(i - period3 + 1, i + 1).reduce((a, b) => a + b, 0) /
      tr.slice(i - period3 + 1, i + 1).reduce((a, b) => a + b, 0);

    const uo = (100 * (4 * avg1 + 2 * avg2 + avg3)) / (4 + 2 + 1);
    result.push(uo);
  }

  return result;
}

/**
 * Ease of Movement (EOM)
 */
export function easeOfMovement(ohlcv: OHLCV[], period: number = 14): number[] {
  if (ohlcv.length < 2) {
    return [];
  }

  const emv: number[] = [];

  for (let i = 1; i < ohlcv.length; i++) {
    const current = ohlcv[i]!;
    const previous = ohlcv[i - 1]!;

    const distance = (current.high + current.low) / 2 - (previous.high + previous.low) / 2;
    const boxHeight = current.high - current.low;
    const volume = current.volume;

    if (boxHeight > 0 && volume > 0) {
      const emvValue = (distance * 1000000) / (volume / boxHeight);
      emv.push(emvValue);
    } else {
      emv.push(0);
    }
  }

  return sma(emv, period);
}

/**
 * Triple Exponential Moving Average (TEMA)
 */
export function tema(values: number[], period: number): number[] {
  const ema1 = ema(values, period);
  const ema2 = ema(ema1, period);
  const ema3 = ema(ema2, period);

  const result: number[] = [];
  const minLength = Math.min(ema1.length, ema2.length, ema3.length);

  for (let i = 0; i < minLength; i++) {
    const e1 = ema1[i];
    const e2 = ema2[i];
    const e3 = ema3[i];
    if (e1 !== undefined && e2 !== undefined && e3 !== undefined) {
      const temaValue = 3 * e1 - 3 * e2 + e3;
      result.push(temaValue);
    }
  }

  return result;
}

/**
 * Weighted Moving Average (WMA)
 */
export function wma(values: number[], period: number): number[] {
  if (period > values.length) {
    return [];
  }

  const result: number[] = [];
  const denominator = (period * (period + 1)) / 2;

  for (let i = period - 1; i < values.length; i++) {
    let weightedSum = 0;
    let weight = 1;

    for (let j = i - period + 1; j <= i; j++) {
      const value = values[j];
      if (value !== undefined) {
        weightedSum += value * weight;
        weight++;
      }
    }

    result.push(weightedSum / denominator);
  }

  return result;
}

/**
 * Keltner Channels
 */
export function keltnerChannels(
  ohlcv: OHLCV[],
  period: number = 20,
  multiplier: number = 2
): { upper: number[]; middle: number[]; lower: number[] } {
  const typicalPrices = ohlcv.map(d => (d.high + d.low + d.close) / 3);
  const middle = ema(typicalPrices, period);
  const atrValues = atr(ohlcv, period);

  const upper: number[] = [];
  const lower: number[] = [];

  const minLength = Math.min(middle.length, atrValues.length);
  for (let i = 0; i < minLength; i++) {
    const middleValue = middle[i];
    const atrValue = atrValues[i];
    if (middleValue !== undefined && atrValue !== undefined) {
      upper.push(middleValue + multiplier * atrValue);
      lower.push(middleValue - multiplier * atrValue);
    }
  }

  return { upper, middle, lower };
}

/**
 * Donchian Channels
 */
export function donchianChannels(
  ohlcv: OHLCV[],
  period: number = 20
): { upper: number[]; middle: number[]; lower: number[] } {
  if (period >= ohlcv.length) {
    return { upper: [], middle: [], lower: [] };
  }

  const upper: number[] = [];
  const lower: number[] = [];
  const middle: number[] = [];

  for (let i = period - 1; i < ohlcv.length; i++) {
    const slice = ohlcv.slice(i - period + 1, i + 1);
    const highest = Math.max(...slice.map(d => d.high));
    const lowest = Math.min(...slice.map(d => d.low));
    const middleValue = (highest + lowest) / 2;

    upper.push(highest);
    lower.push(lowest);
    middle.push(middleValue);
  }

  return { upper, middle, lower };
}

/**
 * Klinger Volume Oscillator
 */
export function klingerVolumeOscillator(
  ohlcv: OHLCV[],
  fastPeriod: number = 34,
  slowPeriod: number = 55,
  signalPeriod: number = 13
): { kvo: number[]; signal: number[] } {
  if (ohlcv.length < 2) {
    return { kvo: [], signal: [] };
  }

  const volumeForce: number[] = [];

  for (let i = 1; i < ohlcv.length; i++) {
    const current = ohlcv[i]!;
    const previous = ohlcv[i - 1]!;

    const typicalPrice = (current.high + current.low + current.close) / 3;
    const prevTypicalPrice = (previous.high + previous.low + previous.close) / 3;

    const trend = typicalPrice > prevTypicalPrice ? 1 : -1;
    const vf =
      current.volume *
      trend *
      Math.abs(
        (2 * (current.close - current.low - (current.high - current.close))) /
          (current.high - current.low)
      ) *
      100;

    volumeForce.push(vf);
  }

  const fastEMA = ema(volumeForce, fastPeriod);
  const slowEMA = ema(volumeForce, slowPeriod);

  const kvo: number[] = [];
  const minLength = Math.min(fastEMA.length, slowEMA.length);

  for (let i = 0; i < minLength; i++) {
    const fast = fastEMA[i];
    const slow = slowEMA[i];
    if (fast !== undefined && slow !== undefined) {
      kvo.push(fast - slow);
    }
  }

  const signal = ema(kvo, signalPeriod);

  return { kvo, signal };
}

/**
 * Price Oscillator (PPO)
 */
export function priceOscillator(
  prices: number[],
  fastPeriod: number = 12,
  slowPeriod: number = 26,
  signalPeriod: number = 9
): { ppo: number[]; signal: number[]; histogram: number[] } {
  const fastEMA = ema(prices, fastPeriod);
  const slowEMA = ema(prices, slowPeriod);

  const ppo: number[] = [];
  const minLength = Math.min(fastEMA.length, slowEMA.length);

  for (let i = 0; i < minLength; i++) {
    const fast = fastEMA[i];
    const slow = slowEMA[i];
    if (fast !== undefined && slow !== undefined && slow !== 0) {
      const ppoValue = ((fast - slow) / slow) * 100;
      ppo.push(ppoValue);
    }
  }

  const signal = ema(ppo, signalPeriod);
  const histogram: number[] = [];

  const histMinLength = Math.min(ppo.length, signal.length);
  for (let i = 0; i < histMinLength; i++) {
    const ppoValue = ppo[i];
    const signalValue = signal[i];
    if (ppoValue !== undefined && signalValue !== undefined) {
      histogram.push(ppoValue - signalValue);
    }
  }

  return { ppo, signal, histogram };
}

/**
 * Chande Momentum Oscillator (CMO)
 */
export function chandeMomentumOscillator(prices: number[], period: number = 14): number[] {
  if (period >= prices.length) {
    return [];
  }

  const gains: number[] = [];
  const losses: number[] = [];

  // Calculate gains and losses
  for (let i = 1; i < prices.length; i++) {
    const current = prices[i];
    const previous = prices[i - 1];
    if (current !== undefined && previous !== undefined) {
      const change = current - previous;
      gains.push(change > 0 ? change : 0);
      losses.push(change < 0 ? Math.abs(change) : 0);
    }
  }

  const result: number[] = [];

  for (let i = period - 1; i < gains.length; i++) {
    const sumGains = gains.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0);
    const sumLosses = losses.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0);

    const cmo = ((sumGains - sumLosses) / (sumGains + sumLosses)) * 100;
    result.push(isNaN(cmo) ? 0 : cmo);
  }

  return result;
}

/**
 * Stochastic RSI
 */
export function stochasticRSI(
  prices: number[],
  rsiPeriod: number = 14,
  stochPeriod: number = 14,
  smoothK: number = 3,
  smoothD: number = 3
): { k: number[]; d: number[] } {
  const rsiValues = rsi(prices, rsiPeriod);

  if (rsiValues.length < stochPeriod) {
    return { k: [], d: [] };
  }

  const stochRSI: number[] = [];

  for (let i = stochPeriod - 1; i < rsiValues.length; i++) {
    const slice = rsiValues.slice(i - stochPeriod + 1, i + 1);
    const highest = Math.max(...slice);
    const lowest = Math.min(...slice);
    const currentRSI = rsiValues[i]!;

    if (highest === lowest) {
      stochRSI.push(0);
    } else {
      const stochValue = ((currentRSI - lowest) / (highest - lowest)) * 100;
      stochRSI.push(stochValue);
    }
  }

  const k = sma(stochRSI, smoothK);
  const d = sma(k, smoothD);

  return { k, d };
}

/**
 * Vortex Indicator
 */
export function vortexIndicator(
  ohlcv: OHLCV[],
  period: number = 14
): { vi_plus: number[]; vi_minus: number[] } {
  if (ohlcv.length < period + 1) {
    return { vi_plus: [], vi_minus: [] };
  }

  const vm_plus: number[] = [];
  const vm_minus: number[] = [];
  const tr: number[] = [];

  for (let i = 1; i < ohlcv.length; i++) {
    const current = ohlcv[i]!;
    const previous = ohlcv[i - 1]!;

    // Vortex Movement
    vm_plus.push(Math.abs(current.high - previous.low));
    vm_minus.push(Math.abs(current.low - previous.high));

    // True Range
    const trValue = Math.max(
      current.high - current.low,
      Math.abs(current.high - previous.close),
      Math.abs(current.low - previous.close)
    );
    tr.push(trValue);
  }

  const vi_plus: number[] = [];
  const vi_minus: number[] = [];

  for (let i = period - 1; i < vm_plus.length; i++) {
    const sumVMPlus = vm_plus.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0);
    const sumVMMinus = vm_minus.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0);
    const sumTR = tr.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0);

    if (sumTR > 0) {
      vi_plus.push(sumVMPlus / sumTR);
      vi_minus.push(sumVMMinus / sumTR);
    } else {
      vi_plus.push(0);
      vi_minus.push(0);
    }
  }

  return { vi_plus, vi_minus };
}

/**
 * Balance of Power (BOP)
 */
export function balanceOfPower(ohlcv: OHLCV[]): number[] {
  const result: number[] = [];

  for (const candle of ohlcv) {
    if (candle.high === candle.low) {
      result.push(0);
    } else {
      const bop = (candle.close - candle.open) / (candle.high - candle.low);
      result.push(bop);
    }
  }

  return result;
}

/**
 * Trix (Triple Exponential Moving Average Oscillator)
 */
export function trix(prices: number[], period: number = 14): number[] {
  const ema1 = ema(prices, period);
  const ema2 = ema(ema1, period);
  const ema3 = ema(ema2, period);

  const result: number[] = [];

  for (let i = 1; i < ema3.length; i++) {
    const current = ema3[i];
    const previous = ema3[i - 1];
    if (current !== undefined && previous !== undefined && previous !== 0) {
      const trixValue = ((current - previous) / previous) * 10000;
      result.push(trixValue);
    }
  }

  return result;
}

/**
 * Mass Index
 */
export function massIndex(ohlcv: OHLCV[], period: number = 25): number[] {
  if (ohlcv.length < period) {
    return [];
  }

  // Calculate high-low ranges
  const ranges = ohlcv.map(candle => candle.high - candle.low);

  // Calculate 9-period EMA of ranges
  const ema9 = ema(ranges, 9);

  // Calculate 9-period EMA of the EMA (double smoothing)
  const emaEma9 = ema(ema9, 9);

  // Calculate ratio
  const ratios: number[] = [];
  const minLength = Math.min(ema9.length, emaEma9.length);

  for (let i = 0; i < minLength; i++) {
    const singleEMA = ema9[i];
    const doubleEMA = emaEma9[i];
    if (singleEMA !== undefined && doubleEMA !== undefined && doubleEMA !== 0) {
      ratios.push(singleEMA / doubleEMA);
    }
  }

  // Calculate 25-period sum of ratios
  const result: number[] = [];
  for (let i = period - 1; i < ratios.length; i++) {
    const sum = ratios.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0);
    result.push(sum);
  }

  return result;
}

/**
 * Coppock Curve
 */
export function coppockCurve(
  prices: number[],
  shortROC: number = 11,
  longROC: number = 14,
  wma: number = 10
): number[] {
  const roc1 = roc(prices, shortROC);
  const roc2 = roc(prices, longROC);

  const combined: number[] = [];
  const minLength = Math.min(roc1.length, roc2.length);

  for (let i = 0; i < minLength; i++) {
    const r1 = roc1[i];
    const r2 = roc2[i];
    if (r1 !== undefined && r2 !== undefined) {
      combined.push(r1 + r2);
    }
  }

  return sma(combined, wma); // Using SMA instead of WMA for simplicity
}
