pybottrader._indicators API documentation

# pybottrader._indicators

# Financial indicators for streaming data

They don´t make calculations from scratch but instead by keeping memory of previous results (intended to be use with real time data). An `update` method is used to push new data and update their results. They use a bracket notation to bring access to results, like `ind[0]` for the most recent result and `ind[-1]` for the previous one. Current implemented indicators are `MA` (simple moving average), `EMA` (exponential moving average), `RSI` (Relative Strength Index), `MACD` (Moving average convergence/divergence), and `ROI` (return of investment). Check some examples in this test file.

View Source

      1"""
      2# Financial indicators for streaming data
      3
      4They don´t make calculations from scratch but instead by keeping memory of
      5previous results (intended to be use with real time data). An `update` method is
      6used to push new data and update their results. They use a bracket notation to
      7bring access to results, like `ind[0]` for the most recent result and `ind[-1]`
      8for the previous one.  Current implemented indicators are `MA` (simple moving
      9average), `EMA` (exponential moving average), `RSI` (Relative Strength Index),
     10`MACD` (Moving average convergence/divergence), and `ROI` (return of
     11investment). Check some examples in [this test
     12file](https://github.com/jailop/pybottrader/blob/main/test/test_indicators.py).
     13
     14"""
     15
     16import numpy as np
     17from attrs import define
     18
     19
     20class Indicator:
     21    """
     22    Base class to build indicators. It provides a buffer to
     23    keep the last `mem_size` values and functions to push
     24    new values and retrieve using bracket notation or the
     25    function get.
     26
     27    All derived classes, call the __init__ method of this class
     28    and when an update ocurrs, they push the new value
     29    into the memory buffer.
     30
     31    Because this class is intended to be used for time series,
     32    indices go backwards, being `0` the last updated value, -1
     33    the previous one, -2 the previous of the previos one...
     34    If a value is requested with a positive index or a negative
     35    index which absolute values is greater than `mem_size`, then
     36    an NAN value is returned.
     37    """
     38
     39    mem_pos: int
     40    mem_data: list
     41    mem_size: int
     42
     43    def __init__(self, mem_size=1):
     44        """
     45        @param mem_size: The size of the memory buffer. The
     46            default value is 1.
     47        """
     48        self.mem_data = [np.nan] * mem_size
     49        self.mem_pos = 0
     50        self.mem_size = mem_size
     51
     52    def __getitem__(self, key):
     53        """
     54        @param key: 0 for the most recent update
     55                    a negative number for the n-previous updates
     56
     57        A negative number less than -mem_size returns NAN.
     58        A positive number returns NAN.
     59        """
     60        if key > 0 or -key >= self.mem_size:
     61            return np.nan
     62        real_pos = (self.mem_pos - key) % self.mem_size
     63        return self.mem_data[real_pos]
     64
     65    def push(self, value):
     66        """
     67        Stores in the buffer `value` as the more recent update.
     68        In the current implementation, a ring buffer is used
     69        to save these values.
     70
     71        @param value: The most recent update
     72        """
     73        self.mem_pos = (self.mem_pos - 1) % self.mem_size
     74        self.mem_data[self.mem_pos] = value
     75
     76    def get(self, key=0) -> float:
     77        """The same as `__getitem__`"""
     78        return self[key]
     79
     80
     81class MA(Indicator):
     82    """Moving Average"""
     83
     84    period: int
     85    prevs: np.ndarray
     86    length: int = 0
     87    pos: int = 0
     88    accum: float = 0.0
     89
     90    def __init__(self, period: int, *args, **kwargs):
     91        """
     92        The number of period or window size is required to initialize a MA
     93        object.
     94        """
     95        super().__init__(*args, **kwargs)
     96        self.period = period
     97        self.prevs = np.zeros(period, dtype=float)
     98
     99    def update(self, value: float) -> float:
    100        """Aggregate a new value into the moving average"""
    101        if self.length < self.period:
    102            self.length += 1
    103        else:
    104            self.accum -= self.prevs[self.pos]
    105        self.prevs[self.pos] = value
    106        self.accum += value
    107        self.pos = (self.pos + 1) % self.period
    108        if self.length < self.period:
    109            self.push(np.nan)
    110        else:
    111            self.push(self.accum / self.period)
    112        return self[0]
    113
    114
    115class EMA(Indicator):
    116    """Exponential Moving Average"""
    117
    118    period: float
    119    alpha: float
    120    smooth_factor: float
    121    length: int = 0
    122    prev: float = 0.0
    123
    124    def __init__(self, period: int, *args, **kwargs):
    125        super().__init__(*args, **kwargs)
    126        alpha = 2.0 if "alpha" not in kwargs else kwargs["alpha"]
    127        self.period = period
    128        self.alpha = alpha
    129        self.smooth_factor = alpha / (1.0 + period)
    130
    131    def update(self, value: float) -> float:
    132        """Aggregate a new value into the moving average"""
    133        self.length += 1
    134        if self.length < self.period:
    135            self.prev += value
    136        elif self.length == self.period:
    137            self.prev += value
    138            self.prev /= self.period
    139        else:
    140            self.prev = (value * self.smooth_factor) + self.prev * (
    141                1.0 - self.smooth_factor
    142            )
    143        if self.length < self.period:
    144            self.push(np.nan)
    145        else:
    146            self.push(self.prev)
    147        return self[0]
    148
    149
    150class ROI(Indicator):
    151    """
    152    Return of investment for streaming data.
    153    """
    154
    155    prev: float
    156
    157    def __init__(self, *args, **kwargs):
    158        super().__init__(*args, **kwargs)
    159        self.prev = np.nan
    160
    161    def update(self, value: float) -> float:
    162        """
    163        Updates the indicator. If at least
    164        a previous value has been updated before,
    165        it starts reporting the return of the
    166        investment.
    167        """
    168        curr = roi(self.prev, value)
    169        self.push(curr)
    170        self.prev = value
    171        return self[0]
    172
    173
    174class RSI(Indicator):
    175    """Relative Strength Index"""
    176
    177    gains: MA
    178    losses: MA
    179
    180    def __init__(self, period: int = 14, **kwargs):
    181        args = []
    182        super().__init__(*args, **kwargs)
    183        self.gains = MA(period=period)
    184        self.losses = MA(period=period)
    185
    186    def update(self, open_price: float, close_price: float) -> float:
    187        """RSI update"""
    188        diff = close_price - open_price
    189        self.gains.update(diff if diff > 0.0 else 0.0)
    190        self.losses.update(-diff if diff < 0.0 else 0.0)
    191        if np.isnan(self.losses[0]) or self.losses[0] < 1e-9:
    192            self.push(np.nan)
    193        else:
    194            self.push(100.0 - 100.0 / (1 + self.gains[0] / self.losses[0]))
    195        return self[0]
    196
    197
    198@define
    199class MACDResult:
    200    """MACD result"""
    201
    202    macd: float
    203    signal: float
    204    hist: float
    205
    206
    207class MACD(Indicator):
    208    """Moving Average Convergence Divergence"""
    209
    210    short: EMA
    211    long: EMA
    212    diff: EMA
    213    start: int
    214    counter = 0
    215
    216    def __init__(
    217        self,
    218        short_period: float,
    219        long_period: float,
    220        diff_period: float,
    221        *args,
    222        **kwargs
    223    ):
    224        args = []
    225        super().__init__(*args, **kwargs)
    226        self.short = EMA(period=short_period, *args, **kwargs)
    227        self.long = EMA(period=long_period, *args, **kwargs)
    228        self.diff = EMA(period=diff_period, *args, **kwargs)
    229        self.start = long_period if long_period > short_period else short_period
    230
    231    def update(self, value: float) -> float:
    232        """MACD update"""
    233        self.counter += 1
    234        self.short.update(value)
    235        self.long.update(value)
    236        if self.counter >= self.start:
    237            diff = self.short[0] - self.long[0]
    238            self.diff.update(diff)
    239            hist = diff - self.diff[0]
    240            self.push(MACDResult(macd=diff, signal=self.diff[0], hist=hist))
    241        else:
    242            self.push(MACDResult(macd=np.nan, signal=np.nan, hist=np.nan))
    243        return self[0]
    244
    245
    246def roi(initial_value, final_value):
    247    """Return on investment"""
    248    if initial_value == 0 or np.isnan(initial_value):
    249        return np.nan
    250    return final_value / initial_value - 1.0

class Indicator: View Source

    21class Indicator:
    22    """
    23    Base class to build indicators. It provides a buffer to
    24    keep the last `mem_size` values and functions to push
    25    new values and retrieve using bracket notation or the
    26    function get.
    27
    28    All derived classes, call the __init__ method of this class
    29    and when an update ocurrs, they push the new value
    30    into the memory buffer.
    31
    32    Because this class is intended to be used for time series,
    33    indices go backwards, being `0` the last updated value, -1
    34    the previous one, -2 the previous of the previos one...
    35    If a value is requested with a positive index or a negative
    36    index which absolute values is greater than `mem_size`, then
    37    an NAN value is returned.
    38    """
    39
    40    mem_pos: int
    41    mem_data: list
    42    mem_size: int
    43
    44    def __init__(self, mem_size=1):
    45        """
    46        @param mem_size: The size of the memory buffer. The
    47            default value is 1.
    48        """
    49        self.mem_data = [np.nan] * mem_size
    50        self.mem_pos = 0
    51        self.mem_size = mem_size
    52
    53    def __getitem__(self, key):
    54        """
    55        @param key: 0 for the most recent update
    56                    a negative number for the n-previous updates
    57
    58        A negative number less than -mem_size returns NAN.
    59        A positive number returns NAN.
    60        """
    61        if key > 0 or -key >= self.mem_size:
    62            return np.nan
    63        real_pos = (self.mem_pos - key) % self.mem_size
    64        return self.mem_data[real_pos]
    65
    66    def push(self, value):
    67        """
    68        Stores in the buffer `value` as the more recent update.
    69        In the current implementation, a ring buffer is used
    70        to save these values.
    71
    72        @param value: The most recent update
    73        """
    74        self.mem_pos = (self.mem_pos - 1) % self.mem_size
    75        self.mem_data[self.mem_pos] = value
    76
    77    def get(self, key=0) -> float:
    78        """The same as `__getitem__`"""
    79        return self[key]

Base class to build indicators. It provides a buffer to keep the last `mem_size` values and functions to push new values and retrieve using bracket notation or the function get.

All derived classes, call the __init__ method of this class and when an update ocurrs, they push the new value into the memory buffer.

Because this class is intended to be used for time series, indices go backwards, being `0` the last updated value, -1 the previous one, -2 the previous of the previos one... If a value is requested with a positive index or a negative index which absolute values is greater than `mem_size`, then an NAN value is returned.

Indicator(mem_size=1) View Source

    44    def __init__(self, mem_size=1):
    45        """
    46        @param mem_size: The size of the memory buffer. The
    47            default value is 1.
    48        """
    49        self.mem_data = [np.nan] * mem_size
    50        self.mem_pos = 0
    51        self.mem_size = mem_size

@param mem_size: The size of the memory buffer. The default value is 1.

mem_pos: int
mem_data: list
mem_size: int
def push(self, value): View Source

    66    def push(self, value):
    67        """
    68        Stores in the buffer `value` as the more recent update.
    69        In the current implementation, a ring buffer is used
    70        to save these values.
    71
    72        @param value: The most recent update
    73        """
    74        self.mem_pos = (self.mem_pos - 1) % self.mem_size
    75        self.mem_data[self.mem_pos] = value

Stores in the buffer `value` as the more recent update. In the current implementation, a ring buffer is used to save these values.

@param value: The most recent update

def get(self, key=0) -> float: View Source

    77    def get(self, key=0) -> float:
    78        """The same as `__getitem__`"""
    79        return self[key]

The same as `__getitem__`

class MA(Indicator): View Source

     82class MA(Indicator):
     83    """Moving Average"""
     84
     85    period: int
     86    prevs: np.ndarray
     87    length: int = 0
     88    pos: int = 0
     89    accum: float = 0.0
     90
     91    def __init__(self, period: int, *args, **kwargs):
     92        """
     93        The number of period or window size is required to initialize a MA
     94        object.
     95        """
     96        super().__init__(*args, **kwargs)
     97        self.period = period
     98        self.prevs = np.zeros(period, dtype=float)
     99
    100    def update(self, value: float) -> float:
    101        """Aggregate a new value into the moving average"""
    102        if self.length < self.period:
    103            self.length += 1
    104        else:
    105            self.accum -= self.prevs[self.pos]
    106        self.prevs[self.pos] = value
    107        self.accum += value
    108        self.pos = (self.pos + 1) % self.period
    109        if self.length < self.period:
    110            self.push(np.nan)
    111        else:
    112            self.push(self.accum / self.period)
    113        return self[0]

Moving Average

MA(period: int, *args, **kwargs) View Source

    91    def __init__(self, period: int, *args, **kwargs):
    92        """
    93        The number of period or window size is required to initialize a MA
    94        object.
    95        """
    96        super().__init__(*args, **kwargs)
    97        self.period = period
    98        self.prevs = np.zeros(period, dtype=float)

The number of period or window size is required to initialize a MA object.

period: int
prevs: numpy.ndarray
length: int = 0
pos: int = 0
accum: float = 0.0
def update(self, value: float) -> float: View Source

    100    def update(self, value: float) -> float:
    101        """Aggregate a new value into the moving average"""
    102        if self.length < self.period:
    103            self.length += 1
    104        else:
    105            self.accum -= self.prevs[self.pos]
    106        self.prevs[self.pos] = value
    107        self.accum += value
    108        self.pos = (self.pos + 1) % self.period
    109        if self.length < self.period:
    110            self.push(np.nan)
    111        else:
    112            self.push(self.accum / self.period)
    113        return self[0]

Aggregate a new value into the moving average

##### Inherited Members

Indicator
  mem_pos
  mem_data
  mem_size
  push
  get

class EMA(Indicator): View Source

    116class EMA(Indicator):
    117    """Exponential Moving Average"""
    118
    119    period: float
    120    alpha: float
    121    smooth_factor: float
    122    length: int = 0
    123    prev: float = 0.0
    124
    125    def __init__(self, period: int, *args, **kwargs):
    126        super().__init__(*args, **kwargs)
    127        alpha = 2.0 if "alpha" not in kwargs else kwargs["alpha"]
    128        self.period = period
    129        self.alpha = alpha
    130        self.smooth_factor = alpha / (1.0 + period)
    131
    132    def update(self, value: float) -> float:
    133        """Aggregate a new value into the moving average"""
    134        self.length += 1
    135        if self.length < self.period:
    136            self.prev += value
    137        elif self.length == self.period:
    138            self.prev += value
    139            self.prev /= self.period
    140        else:
    141            self.prev = (value * self.smooth_factor) + self.prev * (
    142                1.0 - self.smooth_factor
    143            )
    144        if self.length < self.period:
    145            self.push(np.nan)
    146        else:
    147            self.push(self.prev)
    148        return self[0]

Exponential Moving Average

EMA(period: int, *args, **kwargs) View Source

    125    def __init__(self, period: int, *args, **kwargs):
    126        super().__init__(*args, **kwargs)
    127        alpha = 2.0 if "alpha" not in kwargs else kwargs["alpha"]
    128        self.period = period
    129        self.alpha = alpha
    130        self.smooth_factor = alpha / (1.0 + period)

@param mem_size: The size of the memory buffer. The default value is 1.

period: float
alpha: float
smooth_factor: float
length: int = 0
prev: float = 0.0
def update(self, value: float) -> float: View Source

    132    def update(self, value: float) -> float:
    133        """Aggregate a new value into the moving average"""
    134        self.length += 1
    135        if self.length < self.period:
    136            self.prev += value
    137        elif self.length == self.period:
    138            self.prev += value
    139            self.prev /= self.period
    140        else:
    141            self.prev = (value * self.smooth_factor) + self.prev * (
    142                1.0 - self.smooth_factor
    143            )
    144        if self.length < self.period:
    145            self.push(np.nan)
    146        else:
    147            self.push(self.prev)
    148        return self[0]

Aggregate a new value into the moving average

##### Inherited Members

Indicator
  mem_pos
  mem_data
  mem_size
  push
  get

class ROI(Indicator): View Source

    151class ROI(Indicator):
    152    """
    153    Return of investment for streaming data.
    154    """
    155
    156    prev: float
    157
    158    def __init__(self, *args, **kwargs):
    159        super().__init__(*args, **kwargs)
    160        self.prev = np.nan
    161
    162    def update(self, value: float) -> float:
    163        """
    164        Updates the indicator. If at least
    165        a previous value has been updated before,
    166        it starts reporting the return of the
    167        investment.
    168        """
    169        curr = roi(self.prev, value)
    170        self.push(curr)
    171        self.prev = value
    172        return self[0]

Return of investment for streaming data.

ROI(*args, **kwargs) View Source

    158    def __init__(self, *args, **kwargs):
    159        super().__init__(*args, **kwargs)
    160        self.prev = np.nan

@param mem_size: The size of the memory buffer. The default value is 1.

prev: float
def update(self, value: float) -> float: View Source

    162    def update(self, value: float) -> float:
    163        """
    164        Updates the indicator. If at least
    165        a previous value has been updated before,
    166        it starts reporting the return of the
    167        investment.
    168        """
    169        curr = roi(self.prev, value)
    170        self.push(curr)
    171        self.prev = value
    172        return self[0]

Updates the indicator. If at least a previous value has been updated before, it starts reporting the return of the investment.

##### Inherited Members

Indicator
  mem_pos
  mem_data
  mem_size
  push
  get

class RSI(Indicator): View Source

    175class RSI(Indicator):
    176    """Relative Strength Index"""
    177
    178    gains: MA
    179    losses: MA
    180
    181    def __init__(self, period: int = 14, **kwargs):
    182        args = []
    183        super().__init__(*args, **kwargs)
    184        self.gains = MA(period=period)
    185        self.losses = MA(period=period)
    186
    187    def update(self, open_price: float, close_price: float) -> float:
    188        """RSI update"""
    189        diff = close_price - open_price
    190        self.gains.update(diff if diff > 0.0 else 0.0)
    191        self.losses.update(-diff if diff < 0.0 else 0.0)
    192        if np.isnan(self.losses[0]) or self.losses[0] < 1e-9:
    193            self.push(np.nan)
    194        else:
    195            self.push(100.0 - 100.0 / (1 + self.gains[0] / self.losses[0]))
    196        return self[0]

Relative Strength Index

RSI(period: int = 14, **kwargs) View Source

    181    def __init__(self, period: int = 14, **kwargs):
    182        args = []
    183        super().__init__(*args, **kwargs)
    184        self.gains = MA(period=period)
    185        self.losses = MA(period=period)

@param mem_size: The size of the memory buffer. The default value is 1.

gains: MA
losses: MA
def update(self, open_price: float, close_price: float) -> float: View Source

    187    def update(self, open_price: float, close_price: float) -> float:
    188        """RSI update"""
    189        diff = close_price - open_price
    190        self.gains.update(diff if diff > 0.0 else 0.0)
    191        self.losses.update(-diff if diff < 0.0 else 0.0)
    192        if np.isnan(self.losses[0]) or self.losses[0] < 1e-9:
    193            self.push(np.nan)
    194        else:
    195            self.push(100.0 - 100.0 / (1 + self.gains[0] / self.losses[0]))
    196        return self[0]

RSI update

##### Inherited Members

Indicator
  mem_pos
  mem_data
  mem_size
  push
  get

@define
class MACDResult: View Source

    199@define
    200class MACDResult:
    201    """MACD result"""
    202
    203    macd: float
    204    signal: float
    205    hist: float

MACD result

MACDResult(macd: float, signal: float, hist: float) View Source

    2def __init__(self, macd, signal, hist):
    3    self.macd = macd
    4    self.signal = signal
    5    self.hist = hist

Method generated by attrs for class MACDResult.

macd: float
signal: float
hist: float
class MACD(Indicator): View Source

    208class MACD(Indicator):
    209    """Moving Average Convergence Divergence"""
    210
    211    short: EMA
    212    long: EMA
    213    diff: EMA
    214    start: int
    215    counter = 0
    216
    217    def __init__(
    218        self,
    219        short_period: float,
    220        long_period: float,
    221        diff_period: float,
    222        *args,
    223        **kwargs
    224    ):
    225        args = []
    226        super().__init__(*args, **kwargs)
    227        self.short = EMA(period=short_period, *args, **kwargs)
    228        self.long = EMA(period=long_period, *args, **kwargs)
    229        self.diff = EMA(period=diff_period, *args, **kwargs)
    230        self.start = long_period if long_period > short_period else short_period
    231
    232    def update(self, value: float) -> float:
    233        """MACD update"""
    234        self.counter += 1
    235        self.short.update(value)
    236        self.long.update(value)
    237        if self.counter >= self.start:
    238            diff = self.short[0] - self.long[0]
    239            self.diff.update(diff)
    240            hist = diff - self.diff[0]
    241            self.push(MACDResult(macd=diff, signal=self.diff[0], hist=hist))
    242        else:
    243            self.push(MACDResult(macd=np.nan, signal=np.nan, hist=np.nan))
    244        return self[0]

Moving Average Convergence Divergence

MACD( short_period: float, long_period: float, diff_period: float, *args, **kwargs) View Source

    217    def __init__(
    218        self,
    219        short_period: float,
    220        long_period: float,
    221        diff_period: float,
    222        *args,
    223        **kwargs
    224    ):
    225        args = []
    226        super().__init__(*args, **kwargs)
    227        self.short = EMA(period=short_period, *args, **kwargs)
    228        self.long = EMA(period=long_period, *args, **kwargs)
    229        self.diff = EMA(period=diff_period, *args, **kwargs)
    230        self.start = long_period if long_period > short_period else short_period

@param mem_size: The size of the memory buffer. The default value is 1.

short: EMA
long: EMA
diff: EMA
start: int
counter = 0
def update(self, value: float) -> float: View Source

    232    def update(self, value: float) -> float:
    233        """MACD update"""
    234        self.counter += 1
    235        self.short.update(value)
    236        self.long.update(value)
    237        if self.counter >= self.start:
    238            diff = self.short[0] - self.long[0]
    239            self.diff.update(diff)
    240            hist = diff - self.diff[0]
    241            self.push(MACDResult(macd=diff, signal=self.diff[0], hist=hist))
    242        else:
    243            self.push(MACDResult(macd=np.nan, signal=np.nan, hist=np.nan))
    244        return self[0]

MACD update

##### Inherited Members

Indicator
  mem_pos
  mem_data
  mem_size
  push
  get

def roi(initial_value, final_value): View Source

    247def roi(initial_value, final_value):
    248    """Return on investment"""
    249    if initial_value == 0 or np.isnan(initial_value):
    250        return np.nan
    251    return final_value / initial_value - 1.0

Return on investment