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
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.
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.
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
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
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.
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
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
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.
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
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.
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.
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.
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
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.
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
199@define 200class MACDResult: 201 """MACD result""" 202 203 macd: float 204 signal: float 205 hist: float
MACD result
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
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.
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
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