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.

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

The same as __getitem__

class MA(Indicator):
 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)
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:
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

class EMA(Indicator):
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)
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:
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

class ROI(Indicator):
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)
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:
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.

class RSI(Indicator):
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)
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:
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

@define
class MACDResult:
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)
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):
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)
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:
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

def roi(initial_value, final_value):
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