import sys
import numpy as np
cimport numpy as cnp
cimport cython
cnp.import_array()
from collections import namedtuple
from cython.cimports.libc.math import sqrt

cdef class ATR:
    cdef Delay yesterday_close
    cdef Delay last_tr
    cdef double window

    def __init__(self, float window = 14, bint fillna=True):
        self.window = window
        self.yesterday_close = Delay(1)
        self.last_tr = Delay(1)

    cpdef double update(self, double close, double high, double low):
        cdef prev_close = self.yesterday_close.update(close)
        cdef double tr = max(
            high - low,
            abs(prev_close - high),
            abs(prev_close-low))
        cdef double prior_tr = self.last_tr.update(tr)
        return (prior_tr * (self.window-1) + tr) / self.window

cdef class ATRP:
    cdef ATR atr

    def __init__(self, float window=14, bint fillna=True):
        self.atr = ATR(window, fillna)

    cpdef double update(self, double close, double high, double low):
        return self.atr.update(close, high, low) / close


cdef class AwesomeOscillator:
    cdef SMA oscillator_1
    cdef SMA oscillator_2
    cdef long counter
    cdef int window

    def __init__(self, int window_1 = 34, int window_2 = 5, bint fillna = True):
        self.oscillator_1 = SMA(window=window_1, fillna=True)
        self.oscillator_2 = SMA(window=window_2, fillna=True)
        self.counter = 0
        self.window = max(window_1, window_2) if not fillna else 0

    cpdef double update(self, double high, double low):
        self.counter += 1

        if high < low:
            high, low = low, high


        cdef double median = (high + low) * 0.5


        cdef double retval = self.oscillator_2.update(median) - self.oscillator_1.update(median)


        if self.counter <= self.window:
            return np.nan
        return retval

    cpdef batch(self, high, low):
        mid = (high + low) * 0.5
        return self.oscillator_1.batch(mid) - self.oscillator_2.batch(mid)


cdef class Delay:
    cdef int index
    cdef int max
    cdef bint fillna
    cdef double[:] buffer_view
    cdef object buffer
    cdef bint first

    def __init__(self, int window=1, bint fillna=True):
        self.index = 0
        self.max = window
        self.fillna = fillna
        self.buffer = np.zeros(self.max)
        self.buffer_view = self.buffer
        self.first = True

    cpdef double update(self, double value):
        cdef int idx
        if self.first:
            self.first = False
            self.buffer[:] = 0 if self.fillna else np.nan
        retval = self.buffer_view[self.index]
        self.buffer_view[self.index] = value
        self.index += 1
        if self.index  == self.max:
            self.index = 0
        return retval

    cpdef double peek(self):
        return self.buffer[self.index]



cdef class EMA:
    """EMA - Exponential moving average

    Args:
      window(int): decay rate
      fillna(bool): if True, fill nan values
    """

    cdef bint first_pass
    cdef long index
    cdef int window
    cdef bint fillna
    cdef double last_value
    cdef double weighted_multiplier
    cdef double inverted_multiplier

    def __init__(self, double window, bint fillna=False):
        self.first_pass = True
        self.index = 0
        self.window = max(int(window), 1)
        self.fillna = fillna
        self.last_value = 0
        self.weighted_multiplier = 2.0 / (1.0 + float(window))
        self.inverted_multiplier = 1 - self.weighted_multiplier

    cpdef double update(self, double value):
        if self.first_pass and self.index == 0:
            self.last_value = value
        self.last_value = self.weighted_multiplier * value + self.last_value * self.inverted_multiplier
        self.index += 1
        if self.index == self.window:
            self.first_pass = False

        if self.first_pass and not self.fillna:
            return np.nan

        return self.last_value

    cpdef batch(self, input):
        cdef long i
        cdef long j = input.shape[0]
        cdef double l = self.last_value
        retval = np.empty(input.shape[0], dtype=np.double)

        cdef double[:] input_view = input
        cdef double[:] output_view = retval

        if self.fillna:
            for i in range(j):
                output_view[i] = l = self.weighted_multiplier * input_view[i] + l * self.inverted_multiplier
            self.last_value = l
            self.index += input.shape[0]
        else:
            for i in range(j):
                l = self.weighted_multiplier * input_view[i] + l * self.inverted_multiplier
                self.index += 1
                if self.index < self.window:
                    output_view[i] = np.nan
                else:
                    output_view[i] = l
            self.last_value = l
        return retval

cdef class EWMA:
    """EWMA - Exponentailly Weighted Moving Average

    EWMA (Exponentially Weighted Moving Average) is a statistical
    technique that smooths time series data by applying exponentially
    decreasing weights to past observations, giving more importance to
    recent values. It’s commonly used in forecasting and process
    control to detect trends or shifts over time.

    Args:
      window(int): decay rate
      fillna(bool): if True, fill nan values
    """

    cdef double alpha
    cdef double last
    cdef bint first

    def __init__(self, alpha=None, span=None, com=None, bint fillna=False):
        if sum(x is not None for x in  (alpha, span, com)) != 1:
            raise ValueError("You must define one of alpha, span or com");
        if alpha is not None:
            self.alpha = alpha
        elif span is not None:
            self.alpha = 2 / (span + 1)
        elif com is not None:
            self.alpha = 1 / (com + 1)

        self.first = True

        if 0 < self.alpha <= 1:
            return
        raise ValueError("EWMA's alpha parameter must be in the range 0<⍺<=1")

    cpdef double update(self, double value):
        if self.first:
            self.last = value
            self.first = False
        else:
            self.last = self.alpha * value + (1 - self.alpha) * self.last
        return self.last


cdef class Kama():
    """
    Args:
      window: (10) n period
      pow1: (2) number of periods for the fastest EMA constant
      pow2: (30) number of periods for the slowest EMA constant
      fillna: fill in nan values
    """

    cdef Delay vol
    cdef Delay window
    cdef Summation den
    cdef double _pow1
    cdef double _pow2
    cdef bint first
    cdef Delay kama

    def __init__(self, int window=10, int fast_ema=2, int slow_ema=30, fillna=True):

        self.vol = Delay(1)
        self.window = Delay(window)
        self.den = Summation(window)
        self._pow1 = fast_ema
        self._pow2 = slow_ema
        self.first = True
        self.kama = Delay(1)

    cpdef double update(self, double close):
        cdef double vol = abs(close - self.vol.update(close))
        cdef double er_num = abs(close - self.window.update(close))
        cdef double er_den = self.den.update(vol)

        cdef double efficiency_ratio = 0 if er_den == 0 else er_num / er_den
        cdef double smoothing_constant = (
            (
                efficiency_ratio * (2.0 / (self._pow1 + 1) - 2.0 / (self._pow2 + 1.0))
                + 2 / (self._pow2 + 1.0)
            )
            ** 2.0)

        if np.isnan(smoothing_constant):
            return np.nan
        elif self.first:
            self.kama.update(close)
            self.first = False
            return close
        cdef double peek = self.kama.peek()
        cdef double retval = peek + smoothing_constant * (close - peek)
        self.kama.update(retval)
        return retval

    cpdef batch(self, input):
        cdef long i
        retval = np.empty(input.shape[0], dtype=np.double)

        cdef double[:] input_view = input
        cdef double[:] output_view = retval

        for i in range(input.shape[0]):
            output_view[i] = self.update(input_view[i])
        return retval

cdef class KeltnerChannel():
    """KeltnerChannel - Keltner Channels
    Keltner Channels are a trend-following tool that helps spot
    potential reversals by tracking price breakouts and the direction
    of the channel. When the market is moving sideways, they can also
    indicate overbought or oversold conditions.

    Args:
      window: n period
      fillna: True/False
      multiplier: Defaults to 2
    """
    cdef EWMA middle
    cdef ATR atr
    cdef double multiplier
    cdef bint start

    def __init__(self, span=20, window_atr=20, fillna=False, multiplier = 2):
        self.atr = ATR(window_atr)
        self.middle = EWMA(span=span)
        self.multiplier = multiplier
        self.start = True

    def update(self, double close, double high, double low):
        cdef double atr = self.atr.update(close, high, low)
        cdef double tp = self.middle.update(close)
        return (
            tp,
            tp + (self.multiplier * atr),
            tp - (self.multiplier * atr)
        )


cdef class KeltnerChannelOriginal():
    """KeltnerChannel - Keltner Channels
    Keltner Channels are a trend-following tool that helps spot
    potential reversals by tracking price breakouts and the direction
    of the channel. When the market is moving sideways, they can also
    indicate overbought or oversold conditions.

    Args:
      window: n period
      window_atr: n atr period.  Only used if origin is False
      fillna: True/False
      multiplier: Defaults to 2
    """

    cdef int window
    cdef bint fillna
    cdef double multiplier

    cdef SMA high
    cdef SMA middle
    cdef SMA low

    def __init__(self, window=20,  fillna=False):
        self.high = SMA(window, fillna=fillna)
        self.middle = SMA(window, fillna=fillna)
        self.low = SMA(window, fillna=fillna)

    cpdef update(self, double close, double high, double low):
        return (
            self.middle.update((high + close + low) / 3.0),
            self.high.update((high * 4 + close - 2 * low) / 3.0),
            self.low.update((close + 4 * low - 2*high) / 3.0)
            )


cdef class MACD():
    """MCAD - Moving average convergence divergence.
    https://en.wikipedia.org/wiki/MACD

    MACD(a, b, c)

    Args:
      a, b, c: The EMA period parameters
    """

    cdef EMA a
    cdef EMA b
    cdef EMA c
    cdef long counter
    cdef bint fillna
    cdef int window

    def __init__(self, int a=12, int b=26, int c=9, bint fillna=False):
        """
        https://school.stockcharts.com/doku.php?id=technical_indicators:moving_average_convergence_divergence_macd
        """
        self.a = EMA(window=a, fillna=True)
        self.b = EMA(window=b, fillna=True)
        self.c = EMA(window=c, fillna=fillna)
        self.counter = 0
        self.fillna = fillna
        self.window = max(a,b) +c

    cpdef double update(self, double value):
        cdef double retval
        try:
            retval = self.c.update(self.a.update(value) - self.b.update(value))
            if not self.fillna and self.counter < self.window:
                return np.nan
            return retval
        finally:
            self.counter += 1

    cpdef batch(self, input):
        cdef long i
        retval = np.empty(input.shape[0], dtype=np.double)

        cdef double[:] input_view = input
        cdef double[:] output_view = retval

        for i in range(input.shape[0]):
            output_view[i] = self.update(input_view[i])
        return retval


cdef class MassIndex():
    cdef EMA single
    cdef EMA double
    cdef Summation summation
    cdef long counter
    cdef int window
    cdef bint fillna

    def __init__(self, int single=9, int double=9, int summation=25, bint fillna=False):
        self.single = EMA(window=single, fillna=True)
        self.double = EMA(window=double, fillna=True)
        self.summation = Summation(window=summation, fillna=True)
        self.counter = 0
        self.window = max(single, double) + summation
        self.fillna = fillna

    cpdef double update(self, double high, double low):
        cdef double retval
        cdef double single
        cdef double double_
        try:
            if high < low:
                high, low = low, high

            single = self.single.update(high-low)
            double_ = self.double.update(single)

            retval = self.summation.update(single/double_)
            if not self.fillna and self.counter < self.window:
                return np.nan
            return retval
        finally:
            self.counter += 1



cdef class SMA():
    """SMA - Simple Moving Average

    Args:
      window(int): n period
      fillna(bool): if True, fill nan values
    """

    cdef bint first_pass
    cdef object history
    cdef double[:] history_view
    cdef int index
    cdef int window
    cdef bint fillna
    cdef double tally
    cdef double _stored_mean
    cdef double _mean

    def __init__(self, int window, bint fillna=False):
        self.history = np.zeros(window)
        self.history_view = self.history
        self.first_pass = True
        self.index = 0
        self.window = window
        self.fillna = fillna
        self.tally = 0
        self._mean = float('nan')

    def length(self):
        return self.window

    cpdef double mean(self):
        return self._stored_mean

    @cython.boundscheck(False) # turn off bounds-checking for entire function
    @cython.wraparound(False)
    cpdef double update(self, double value):
        self.tally -= self.history_view[self.index]
        self.tally += value
        self.history_view[self.index] = value

        self.index += 1

        if self.index == self.window:
            self.index = 0
            self.first_pass = False

        if self.first_pass:
            if not self.fillna:
                self._mean = np.nan
                return self._mean
            self._mean = self.tally / self.index
            return self._mean
        self._mean = self.tally/self.window
        return self._mean

    cpdef batch(self, input):
        cdef long i
        retval = np.empty(input.shape[0], dtype=np.double)

        cdef double[:] input_view = input
        cdef double[:] output_view = retval

        for i in range(input.shape[0]):
            output_view[i] = self.update(input_view[i])
        return retval



cdef struct PercentagePriceResponse:
    double ppo
    double signal
    double histogram


cdef class PercentagePrice():
    cdef EMA oscillator_1
    cdef EMA oscillator_2
    cdef EMA oscillator_3
    cdef int window
    cdef long counter

    def __init__(self, double window_1=12, double window_2=26, double window_3=9, fillna=False):
        self.oscillator_1 = EMA(window=window_1, fillna=True)
        self.oscillator_2 = EMA(window=window_2, fillna=True)
        self.oscillator_3 = EMA(window=window_3, fillna=True)
        self.window = int(max(window_1, window_2, window_3)) if not fillna else 0
        self.counter = 0

    cpdef PercentagePriceResponse update(self, double close):
        self.counter += 1

        cdef double o26 = self.oscillator_2.update(close)

        retval = PercentagePriceResponse()
        retval.ppo = ((self.oscillator_1.update(close) - o26)  / o26) * 100
        retval.signal = self.oscillator_3.update(retval.ppo)
        retval.histogram = retval.ppo - retval.signal

        return retval

    cpdef batch(self, close):
        cdef int j = close.shape[0]
        cdef int i
        ppo = np.empty(input.shape[0], dtype=np.double)
        signal = np.empty(input.shape[0], dtype=np.double)
        histogram = np.empty(input.shape[0], dtype=np.double)

        cdef double[:] input_view=close
        cdef double[:] ppo_view=ppo
        cdef double[:] signal_view=signal
        cdef double[:] histogram_view=histogram

        for i in range(j):
            output = self.update(input_view[i])
            ppo_view[i] = output.ppo
            signal_view[i] = output.signal
            histogram_view[i] = output.histogram

        return {'ppo': ppo, 'signal': signal,
                'histogram':histogram}


cdef struct PercentageVolumeResponse:
    double pvo
    double signal
    double histogram


cdef class PercentageVolume:
    """PercentageVolume: The Percentage Volume Oscillator (PVO) is a
    momentum indicator for volume. It calculates the difference
    between two volume-based moving averages as a percentage of the
    larger moving average.  It is similar to the MACD and the
    Percentage Price Oscillator (PPO).

    https://school.stockcharts.com/doku.php?id=technical_indicators:percentage_volume_oscillator_pvo
    """

    cdef EMA oscillator_1
    cdef EMA oscillator_2
    cdef EMA signal
    cdef long counter

    def __init__(self, double window_1=12, double window_2=26, double signal=9, fillna=True):
        self.oscillator_1 = EMA(window=window_1, fillna=True)
        self.oscillator_2 = EMA(window=window_2, fillna=True)
        self.signal = EMA(window=signal, fillna=True)

        if fillna:
            self.counter = 0
        else:
            self.counter = -int(max(window_1, window_2))

    cpdef PercentageVolumeResponse update(self, double volume):
        self.counter += 1

        cdef double ema_2 = self.oscillator_2.update(volume)

        cdef double pvo = 100 * (self.oscillator_1.update(volume) - ema_2) / (ema_2)
        cdef double signal = self.signal.update(pvo)
        cdef double histogram = pvo - signal

        cdef PercentageVolumeResponse retval

        if self.counter < 0:
            retval.pvo = np.nan
            retval.signal = np.nan
            retval.histogram = np.nan
        else:
            retval.pvo = pvo
            retval.signal = signal
            retval.histogram = histogram

        return retval

    cpdef batch(self, volume):
        cdef int j = volume.shape[0]
        cdef int i

        pvos = np.empty(j, dtype=np.double)
        signals = np.empty(j, dtype=np.double)
        histograms = np.empty(j, dtype=np.double)

        cdef double[:] input_view=volume
        cdef double[:] pvo_view=pvos
        cdef double[:] signal_view=signals
        cdef double[:] histogram_view=histograms

        cdef double pvo = 0
        cdef double signal = 0
        cdef double histogram = 0
        cdef double ema_2 = 0

        for i in range(j):
            self.counter += 1
            ema_2 = self.oscillator_2.update(input_view[i])
            pvo = 100 * (self.oscillator_1.update(input_view[i]) - ema_2) / ema_2
            signal = self.signal.update(pvo)

            if self.counter < 0:
                pvo_view[i] = np.nan
                signal_view[i] = np.nan
                histogram_view[i] = np.nan
            else:
                pvo_view[i] = pvo
                signal_view[i] = signal
                histogram_view[i] = pvo - signal

        return {'pvo': pvos, 'signal': signals,
                'histogram':histograms}


cdef class ROC:
    """ROC:

    Args:
      window: Window size
      fillna:
    """

    cdef Delay close
    cdef int window
    cdef bint fillna
    cdef long counter

    def __init__(self, int window, bint fillna=True):
        self.close = Delay(window=window, fillna=True)
        self.window = window
        self.fillna = fillna
        self.counter = 0

    cpdef double update(self, double close):
        cdef double close_ago = self.close.update(close)
        if not self.fillna and self.counter < self.window:
            self.counter += 1
            return np.nan
        if close_ago == 0:
            self.counter += 1
            return 0
        self.counter += 1
        return  ((close - close_ago) / (close_ago)) * 100

    cpdef batch(self, close):
        cdef int j = close.shape[0]
        cdef int i
        cdef double close_ago

        retval = np.empty(close.shape[0])
        cdef double[:] input_view = close
        cdef double[:] output_view = retval

        for i in range(j):
            close_ago = self.close.update(input_view[i])
            if not self.fillna and self.counter < self.window:
                output_view[i] = np.nan
            elif close_ago == 0:
                output_view[i] = 0
            else:
                output_view[i] = 100 * ((input_view[i] - close_ago) / (close_ago))
            self.counter += 1

        return retval

cdef class RSI:
    """RSI: The Relative Strength Index (RSI)
    """

    cdef bint fillna
    cdef int window
    cdef long counter
    cdef double prev
    cdef double high
    cdef double low

    def __init__(self, int window=14, bint fillna=True):
        self.fillna = fillna
        self.window = window
        self.counter = 0
        self.prev = 0
        self.high = 0
        self.low = 0

    cpdef double update(self, double value):
        try:
            if self.counter == 0:
                self.prev = value
                if self.fillna:
                    return 50
                else:
                    return np.nan
            elif self.counter <= self.window:
                if value < self.prev:
                    self.low = (self.low * (self.counter-1) +  self.prev - value) / self.counter
                elif value > self.prev:
                    self.high = (self.high * (self.counter-1) + value - self.prev) / self.counter
                if not self.fillna:
                    return np.nan
                elif self.low == 0:
                    return 100
                else:
                    return 100 - (100 / (1 + (self.high / self.window) / (self.low / self.window)))
            else:
                if value < self.prev:
                    self.low = (self.low * (self.window - 1) + self.prev - value) / self.window
                elif self.low == 0:
                    return 100
                elif value > self.prev:
                    self.high = (self.high * (self.window-1) + value - self.prev) / self.window
                return 100 - (100 / (1 + (self.high / self.window) / (self.low / self.window)))
        finally:
            self.prev = value
            self.counter += 1


cdef class Summation:
    """Summation - Summation acorss a fixed window.

    Args:
      window(int): n period
      fillna(bool): if True, fill nan values
    """

    cdef bint first_pass
    cdef object history
    cdef double[:] history_view
    cdef int index
    cdef int window
    cdef bint fillna
    cdef double tally

    def __init__(self, int window, bint fillna=True):
        self.history = np.zeros(window)
        self.history_view = self.history
        self.first_pass = True
        self.index = 0
        self.window = window
        self.fillna = fillna
        self.tally = 0

    @cython.boundscheck(False) # turn off bounds-checking for entire function
    @cython.wraparound(False)
    cpdef double update(self, double value):
        self.tally -= self.history_view[self.index]
        self.tally += value
        self.history_view[self.index] = value

        self.index += 1

        if self.index == self.window:
            self.index = 0
            self.first_pass = False

        if self.first_pass:
            if not self.fillna:
                return np.nan
            return self.tally
        return self.tally

    cpdef batch(self, input):
        cdef long i
        retval = np.empty(input.shape[0], dtype=np.double)

        cdef double[:] input_view = input
        cdef double[:] output_view = retval

        for i in range(input.shape[0]):
            output_view[i] = self.update(input_view[i])
        return retval


cdef class High:
    """High - Return the highest value in a window.

    Args:
      window(int): n period
      fillna(bool): if True, fill nan values
    """

    cdef bint first_pass
    cdef int window_size
    cdef int offset
    cdef object window
    cdef double[:] window_view
    cdef bint fillna

    def __init__(self, int window, bint fillna=True):
        self.window = np.zeros(window)
        self.window_view = self.window
        self.first_pass = True
        self.offset = 0
        self.window_size = window
        self.fillna = fillna

    @cython.boundscheck(False)
    @cython.wraparound(False)
    cpdef double update(self, double value):
        cdef double max
        cdef int i

        self.window_view[self.offset] = value

        if self.first_pass:
            if not self.fillna:
                self.offset += 1
                if self.offset >= self.window_size:
                    self.first_pass = False
                    self.offset = 0
                return np.nan
            max = self.window_view[0]
            for i in range(1, self.offset+1):
                if self.window_view[i] > max:
                    max = self.window_view[i]
            self.offset += 1
            if self.offset >= self.window_size:
                self.first_pass = False
                self.offset = 0
            return max
        max = self.window_view[0]
        for i in range(1, self.window_size):
            if self.window_view[i] > max:
                max = self.window_view[i]
        self.offset += 1
        if self.offset >= self.window_size:
            self.offset = 0
        return max


cdef class Low:
    """Low - Return the lowest value in a window.

    Args:
      window(int): n period
      fillna(bool): if True, fill nan values
    """

    cdef bint first_pass
    cdef int window_size
    cdef int offset
    cdef object window
    cdef double[:] window_view
    cdef bint fillna

    def __init__(self, int window, bint fillna=True):
        self.window = np.zeros(window)
        self.window_view = self.window
        self.first_pass = True
        self.offset = 0
        self.window_size = window
        self.fillna = fillna

    @cython.boundscheck(False)
    @cython.wraparound(False)
    cpdef double update(self, double value):
        cdef double min
        cdef int i

        self.window_view[self.offset] = value

        if self.first_pass:
            if not self.fillna:
                self.offset += 1
                if self.offset >= self.window_size:
                    self.first_pass = False
                    self.offset = 0
                return np.nan
            min = self.window_view[0]
            for i in range(1, self.offset+1):
                if self.window_view[i] < min:
                    min = self.window_view[i]
            self.offset += 1
            if self.offset >= self.window_size:
                self.first_pass = False
                self.offset = 0
            return min
        min = self.window_view[0]
        for i in range(1, self.window_size):
            if self.window_view[i] < min:
                min = self.window_view[i]
        self.offset += 1
        if self.offset >= self.window_size:
            self.offset = 0
        return min


cdef class StochRSI:
    """StochRSI: Measures the RSI relative to its high/low range.

    https://school.stockcharts.com/doku.php?id=technical_indicators:stochrsi

    Args:
      window: default 14
      fillna: default True
    """

    cdef RSI rsi
    cdef Low lowest_rsi
    cdef High highest_rsi
    cdef int window_size
    cdef bint fillna

    def __init__(self, int window=14, bint fillna=True):
        self.rsi = RSI(window, True)
        self.lowest_rsi = Low(window, fillna)
        self.highest_rsi = High(window, fillna)

    @cython.boundscheck(False)
    @cython.wraparound(False)
    cpdef double update(self, double value):
        cdef double rsi = self.rsi.update(value)
        cdef double low = self.lowest_rsi.update(rsi)
        cdef double high = self.highest_rsi.update(rsi)
        if high == low:
            return 0
        return (rsi / low ) / (high - low)

cdef class TSI:
    """TSI: True Strength Index

    https://school.stockcharts.com/doku.php?id=technical_indicators:true_strength_index
    """

    cdef Delay P
    cdef EMA A
    cdef EMA B
    cdef Delay D
    cdef EMA E
    cdef EMA F

    def __init__(self, int window_1 = 25, int window_2 = 13):

        self.P = Delay(1)
        self.A = EMA(window_1, True)
        self.B = EMA(window_2, True)
        self.D = Delay(1)
        self.E = EMA(window_1, True)
        self.F = EMA(window_2, True)

    def update(self, double x):
        return (100 * self.B.update(self.A.update(x-self.P.update(x)))) / (self.F.update(self.E.update(abs(x-self.D.update(x)))))


cdef class StdDev:
    """StdDev: Standard Deviation

    https://www.khanacademy.org/math/statistics-probability/summarizing-quantitative-data/variance-standard-deviation-population/a/calculating-standard-deviation-step-by-step
    """

    cdef object history
    cdef double[:] history_view
    cdef bint fillna
    cdef int offset
    cdef int window_size
    cdef bint first_pass
    cdef SMA mean

    def __init__(self, int window, bint fillna=True):
        self.history = np.zeros(window)
        self.history_view = self.history
        self.first_pass = True
        self.window_size = window
        self.offset = 0
        self.mean = SMA(window, True)
        self.fillna = fillna

    @cython.boundscheck(False) # turn off bounds-checking for entire function
    @cython.wraparound(False)
    cpdef double update(self, double value):
        cdef int i
        cdef double tally
        cdef double mean

        try:
            self.history_view[self.offset] = value
            self.offset += 1
            mean = self.mean.update(value)
            tally = 0
            if self.first_pass:
                if not self.fillna:
                    return np.nan
                for i in range(self.offset):
                    tally += (self.history_view[i] - mean) ** 2
                tally /= self.offset
            else:
                for i in range(self.window_size):
                    tally += (self.history_view[i] - mean) ** 2
                tally /= self.window_size
            return sqrt(tally)

        finally:
            if self.offset >= self.window_size:
                self.offset = 0
                self.first_pass = False



cdef struct BollingerBandResponse:
    double middle
    double lower
    double upper


cdef class BollingerBands:
    """BollingerBands: Bollinger Bands

    https://school.stockcharts.com/doku.php?id=technical_indicators:bollinger_bands
    """

    cdef SMA sma
    cdef StdDev stddev

    def __init__(self, int window=20, bint fillna=True):
        self.sma = SMA(window, fillna=fillna)
        self.stddev=StdDev(window, fillna=fillna)

    @cython.boundscheck(False) # turn off bounds-checking for entire function
    @cython.wraparound(False)
    cpdef BollingerBandResponse update(self, double value):

        cdef BollingerBandResponse retval

        cdef double stddev = self.stddev.update(value)
        retval.middle = self.sma.update(value)

        retval.upper = retval.middle + stddev * 2
        retval.lower = retval.middle - stddev * 2

        return retval
