bullishpy 0.14.0__py3-none-any.whl → 0.16.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of bullishpy might be problematic. Click here for more details.
- bullish/analysis/analysis.py +62 -4
- bullish/analysis/backtest.py +422 -0
- bullish/analysis/filter.py +5 -2
- bullish/analysis/functions.py +14 -13
- bullish/analysis/indicators.py +181 -85
- bullish/analysis/predefined_filters.py +143 -9
- bullish/app/app.py +15 -2
- bullish/database/alembic/versions/12889a2cbd7d_.py +52 -0
- bullish/database/alembic/versions/6d252e23f543_.py +48 -0
- bullish/database/crud.py +68 -2
- bullish/database/schemas.py +23 -0
- bullish/interface/interface.py +24 -0
- bullish/jobs/models.py +1 -1
- bullish/jobs/tasks.py +19 -3
- {bullishpy-0.14.0.dist-info → bullishpy-0.16.0.dist-info}/METADATA +4 -3
- {bullishpy-0.14.0.dist-info → bullishpy-0.16.0.dist-info}/RECORD +18 -15
- {bullishpy-0.14.0.dist-info → bullishpy-0.16.0.dist-info}/WHEEL +0 -0
- {bullishpy-0.14.0.dist-info → bullishpy-0.16.0.dist-info}/entry_points.txt +0 -0
bullish/analysis/indicators.py
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from datetime import date
|
|
3
|
-
from typing import Optional, List, Callable, Any, Literal, Dict
|
|
3
|
+
from typing import Optional, List, Callable, Any, Literal, Dict
|
|
4
4
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
import pandas as pd
|
|
7
7
|
from pydantic import BaseModel, Field, PrivateAttr, create_model
|
|
8
8
|
|
|
9
9
|
from bullish.analysis.functions import (
|
|
10
|
-
cross,
|
|
11
|
-
cross_value,
|
|
12
10
|
ADX,
|
|
13
11
|
MACD,
|
|
14
12
|
RSI,
|
|
@@ -19,24 +17,48 @@ from bullish.analysis.functions import (
|
|
|
19
17
|
SMA,
|
|
20
18
|
ADOSC,
|
|
21
19
|
PRICE,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
cross_simple,
|
|
21
|
+
cross_value_series,
|
|
22
|
+
find_last_true_run_start,
|
|
25
23
|
)
|
|
26
24
|
|
|
27
25
|
logger = logging.getLogger(__name__)
|
|
28
26
|
SignalType = Literal["Short", "Long", "Oversold", "Overbought", "Value"]
|
|
29
27
|
|
|
30
28
|
|
|
29
|
+
def _last_date(d: pd.Series) -> Optional[date]:
|
|
30
|
+
d_valid = d[d == 1]
|
|
31
|
+
if d_valid.empty:
|
|
32
|
+
return None
|
|
33
|
+
last_index = d_valid.last_valid_index()
|
|
34
|
+
return last_index.date() if last_index is not None else None # type: ignore
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ProcessingFunction(BaseModel):
|
|
38
|
+
date: Callable[[pd.Series], Optional[date]] = Field(default=_last_date)
|
|
39
|
+
number: Callable[[pd.Series], Optional[float]] = Field(
|
|
40
|
+
default=lambda d: d.iloc[-1] if not d.dropna().empty else None
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SignalSeries(BaseModel):
|
|
45
|
+
name: str
|
|
46
|
+
date: date
|
|
47
|
+
value: float
|
|
48
|
+
symbol: str
|
|
49
|
+
|
|
50
|
+
|
|
31
51
|
class Signal(BaseModel):
|
|
32
52
|
name: str
|
|
33
53
|
type_info: SignalType
|
|
34
54
|
type: Any
|
|
35
55
|
range: Optional[List[float]] = None
|
|
36
|
-
function: Callable[[pd.DataFrame],
|
|
56
|
+
function: Callable[[pd.DataFrame], pd.Series]
|
|
57
|
+
processing: ProcessingFunction = Field(default_factory=ProcessingFunction)
|
|
37
58
|
description: str
|
|
38
59
|
date: Optional[date] = None
|
|
39
60
|
value: Optional[float] = None
|
|
61
|
+
in_use_backtest: bool = False
|
|
40
62
|
|
|
41
63
|
def is_date(self) -> bool:
|
|
42
64
|
if self.type == Optional[date]:
|
|
@@ -46,11 +68,22 @@ class Signal(BaseModel):
|
|
|
46
68
|
else:
|
|
47
69
|
raise NotImplementedError
|
|
48
70
|
|
|
71
|
+
def apply_function(self, data: pd.DataFrame) -> pd.Series:
|
|
72
|
+
result = self.function(data)
|
|
73
|
+
if not isinstance(result, pd.Series):
|
|
74
|
+
raise ValueError(
|
|
75
|
+
f"Function for signal {self.name} must return a pandas Series"
|
|
76
|
+
)
|
|
77
|
+
return result
|
|
78
|
+
|
|
49
79
|
def compute(self, data: pd.DataFrame) -> None:
|
|
50
80
|
if self.is_date():
|
|
51
|
-
self.date = self.
|
|
81
|
+
self.date = self.processing.date(self.apply_function(data))
|
|
52
82
|
else:
|
|
53
|
-
self.value = self.
|
|
83
|
+
self.value = self.processing.number(self.apply_function(data))
|
|
84
|
+
|
|
85
|
+
def compute_series(self, data: pd.DataFrame) -> pd.Series:
|
|
86
|
+
return self.apply_function(data)
|
|
54
87
|
|
|
55
88
|
|
|
56
89
|
class Indicator(BaseModel):
|
|
@@ -68,9 +101,9 @@ class Indicator(BaseModel):
|
|
|
68
101
|
f"Expected columns {self.expected_columns}, but got {results.columns.tolist()}"
|
|
69
102
|
)
|
|
70
103
|
self._data = results
|
|
71
|
-
self.
|
|
104
|
+
self.compute_signals()
|
|
72
105
|
|
|
73
|
-
def
|
|
106
|
+
def compute_signals(self) -> None:
|
|
74
107
|
for signal in self.signals:
|
|
75
108
|
try:
|
|
76
109
|
signal.compute(self._data)
|
|
@@ -79,6 +112,45 @@ class Indicator(BaseModel):
|
|
|
79
112
|
f"Fail to compute signal {signal.name} for indicator {self.name}: {e}"
|
|
80
113
|
)
|
|
81
114
|
|
|
115
|
+
def compute_series(self, data: pd.DataFrame, symbol: str) -> pd.DataFrame:
|
|
116
|
+
series = []
|
|
117
|
+
try:
|
|
118
|
+
results = self.function(data)
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.error(
|
|
121
|
+
f"Failed to compute indicator {self.name} for symbol {symbol}: {e}"
|
|
122
|
+
)
|
|
123
|
+
return pd.DataFrame()
|
|
124
|
+
if not set(self.expected_columns).issubset(results.columns):
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f"Expected columns {self.expected_columns}, but got {results.columns.tolist()}"
|
|
127
|
+
)
|
|
128
|
+
for signal in self.signals:
|
|
129
|
+
if not signal.in_use_backtest:
|
|
130
|
+
continue
|
|
131
|
+
try:
|
|
132
|
+
series_ = signal.compute_series(results)
|
|
133
|
+
if signal.type == Optional[date]:
|
|
134
|
+
series__ = pd.DataFrame(series_[series_ == 1].rename("value"))
|
|
135
|
+
else:
|
|
136
|
+
series__ = pd.DataFrame(
|
|
137
|
+
series_[series_ != None].rename("value") # noqa: E711
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
series__["name"] = signal.name
|
|
141
|
+
series__["date"] = series__.index.date # type: ignore
|
|
142
|
+
series__["symbol"] = symbol
|
|
143
|
+
series__ = series__.reset_index(drop=True)
|
|
144
|
+
series.append(series__)
|
|
145
|
+
except Exception as e:
|
|
146
|
+
logger.error(
|
|
147
|
+
f"Fail to compute signal {signal.name} for indicator {self.name}: {e}"
|
|
148
|
+
)
|
|
149
|
+
if not series:
|
|
150
|
+
return pd.DataFrame()
|
|
151
|
+
data = pd.concat(series).reset_index(drop=True)
|
|
152
|
+
return data
|
|
153
|
+
|
|
82
154
|
|
|
83
155
|
def indicators_factory() -> List[Indicator]:
|
|
84
156
|
return [
|
|
@@ -93,18 +165,14 @@ def indicators_factory() -> List[Indicator]:
|
|
|
93
165
|
description="ADX 14 Long Signal",
|
|
94
166
|
type_info="Long",
|
|
95
167
|
type=Optional[date],
|
|
96
|
-
function=lambda d: d
|
|
97
|
-
(d.ADX_14 > 20) & (d.PLUS_DI > d.MINUS_DI)
|
|
98
|
-
].last_valid_index(),
|
|
168
|
+
function=lambda d: (d.ADX_14 > 20) & (d.PLUS_DI > d.MINUS_DI),
|
|
99
169
|
),
|
|
100
170
|
Signal(
|
|
101
171
|
name="ADX_14_SHORT",
|
|
102
172
|
description="ADX 14 Short Signal",
|
|
103
173
|
type_info="Short",
|
|
104
174
|
type=Optional[date],
|
|
105
|
-
function=lambda d: d
|
|
106
|
-
(d.ADX_14 > 20) & (d.MINUS_DI > d.PLUS_DI)
|
|
107
|
-
].last_valid_index(),
|
|
175
|
+
function=lambda d: (d.ADX_14 > 20) & (d.MINUS_DI > d.PLUS_DI),
|
|
108
176
|
),
|
|
109
177
|
],
|
|
110
178
|
),
|
|
@@ -123,28 +191,35 @@ def indicators_factory() -> List[Indicator]:
|
|
|
123
191
|
description="MACD 12-26-9 Bullish Crossover",
|
|
124
192
|
type_info="Long",
|
|
125
193
|
type=Optional[date],
|
|
126
|
-
function=lambda d:
|
|
194
|
+
function=lambda d: cross_simple(
|
|
195
|
+
d.MACD_12_26_9, d.MACD_12_26_9_SIGNAL
|
|
196
|
+
),
|
|
197
|
+
in_use_backtest=True,
|
|
127
198
|
),
|
|
128
199
|
Signal(
|
|
129
200
|
name="MACD_12_26_9_BEARISH_CROSSOVER",
|
|
130
201
|
description="MACD 12-26-9 Bearish Crossover",
|
|
131
202
|
type_info="Short",
|
|
132
203
|
type=Optional[date],
|
|
133
|
-
function=lambda d:
|
|
204
|
+
function=lambda d: cross_simple(
|
|
205
|
+
d.MACD_12_26_9_SIGNAL, d.MACD_12_26_9
|
|
206
|
+
),
|
|
134
207
|
),
|
|
135
208
|
Signal(
|
|
136
209
|
name="MACD_12_26_9_ZERO_LINE_CROSS_UP",
|
|
137
210
|
description="MACD 12-26-9 Zero Line Cross Up",
|
|
138
211
|
type_info="Long",
|
|
139
212
|
type=Optional[date],
|
|
140
|
-
function=lambda d:
|
|
213
|
+
function=lambda d: cross_value_series(d.MACD_12_26_9, 0),
|
|
141
214
|
),
|
|
142
215
|
Signal(
|
|
143
216
|
name="MACD_12_26_9_ZERO_LINE_CROSS_DOWN",
|
|
144
217
|
description="MACD 12-26-9 Zero Line Cross Down",
|
|
145
218
|
type_info="Long",
|
|
146
219
|
type=Optional[date],
|
|
147
|
-
function=lambda d:
|
|
220
|
+
function=lambda d: cross_value_series(
|
|
221
|
+
d.MACD_12_26_9, 0, above=False
|
|
222
|
+
),
|
|
148
223
|
),
|
|
149
224
|
],
|
|
150
225
|
),
|
|
@@ -159,53 +234,53 @@ def indicators_factory() -> List[Indicator]:
|
|
|
159
234
|
description="RSI Bullish Crossover",
|
|
160
235
|
type_info="Long",
|
|
161
236
|
type=Optional[date],
|
|
162
|
-
function=lambda d:
|
|
237
|
+
function=lambda d: cross_value_series(d.RSI, 30),
|
|
238
|
+
in_use_backtest=True,
|
|
163
239
|
),
|
|
164
240
|
Signal(
|
|
165
241
|
name="RSI_BULLISH_CROSSOVER_40",
|
|
166
242
|
description="RSI Bullish Crossover 40",
|
|
167
243
|
type_info="Long",
|
|
168
244
|
type=Optional[date],
|
|
169
|
-
function=lambda d:
|
|
245
|
+
function=lambda d: cross_value_series(d.RSI, 40),
|
|
246
|
+
in_use_backtest=True,
|
|
170
247
|
),
|
|
171
248
|
Signal(
|
|
172
249
|
name="RSI_BULLISH_CROSSOVER_45",
|
|
173
250
|
description="RSI Bullish Crossover 45",
|
|
174
251
|
type_info="Long",
|
|
175
252
|
type=Optional[date],
|
|
176
|
-
function=lambda d:
|
|
253
|
+
function=lambda d: cross_value_series(d.RSI, 45),
|
|
254
|
+
in_use_backtest=True,
|
|
177
255
|
),
|
|
178
256
|
Signal(
|
|
179
257
|
name="RSI_BEARISH_CROSSOVER",
|
|
180
258
|
description="RSI Bearish Crossover",
|
|
181
259
|
type_info="Short",
|
|
182
260
|
type=Optional[date],
|
|
183
|
-
function=lambda d:
|
|
261
|
+
function=lambda d: cross_value_series(d.RSI, 70, above=False),
|
|
184
262
|
),
|
|
185
263
|
Signal(
|
|
186
264
|
name="RSI_OVERSOLD",
|
|
187
265
|
description="RSI Oversold Signal",
|
|
188
266
|
type_info="Oversold",
|
|
189
267
|
type=Optional[date],
|
|
190
|
-
function=lambda d:
|
|
268
|
+
function=lambda d: (d.RSI < 30) & (d.RSI > 0),
|
|
269
|
+
in_use_backtest=True,
|
|
191
270
|
),
|
|
192
271
|
Signal(
|
|
193
272
|
name="RSI_OVERBOUGHT",
|
|
194
273
|
description="RSI Overbought Signal",
|
|
195
274
|
type_info="Overbought",
|
|
196
275
|
type=Optional[date],
|
|
197
|
-
function=lambda d: d
|
|
198
|
-
(d.RSI < 100) & (d.RSI > 70)
|
|
199
|
-
].last_valid_index(),
|
|
276
|
+
function=lambda d: (d.RSI < 100) & (d.RSI > 70),
|
|
200
277
|
),
|
|
201
278
|
Signal(
|
|
202
279
|
name="RSI_NEUTRAL",
|
|
203
280
|
description="RSI Neutral Signal",
|
|
204
281
|
type_info="Overbought",
|
|
205
282
|
type=Optional[date],
|
|
206
|
-
function=lambda d: d
|
|
207
|
-
(d.RSI < 60) & (d.RSI > 40)
|
|
208
|
-
].last_valid_index(),
|
|
283
|
+
function=lambda d: (d.RSI < 60) & (d.RSI > 40),
|
|
209
284
|
),
|
|
210
285
|
],
|
|
211
286
|
),
|
|
@@ -220,18 +295,14 @@ def indicators_factory() -> List[Indicator]:
|
|
|
220
295
|
description="Stoch Oversold Signal",
|
|
221
296
|
type_info="Oversold",
|
|
222
297
|
type=Optional[date],
|
|
223
|
-
function=lambda d: d
|
|
224
|
-
(d.SLOW_K < 20) & (d.SLOW_K > 0)
|
|
225
|
-
].last_valid_index(),
|
|
298
|
+
function=lambda d: (d.SLOW_K < 20) & (d.SLOW_K > 0),
|
|
226
299
|
),
|
|
227
300
|
Signal(
|
|
228
301
|
name="STOCH_OVERBOUGHT",
|
|
229
302
|
description="Stoch Overbought Signal",
|
|
230
303
|
type_info="Overbought",
|
|
231
304
|
type=Optional[date],
|
|
232
|
-
function=lambda d: d
|
|
233
|
-
(d.SLOW_K < 100) & (d.SLOW_K > 80)
|
|
234
|
-
].last_valid_index(),
|
|
305
|
+
function=lambda d: (d.SLOW_K < 100) & (d.SLOW_K > 80),
|
|
235
306
|
),
|
|
236
307
|
],
|
|
237
308
|
),
|
|
@@ -246,14 +317,14 @@ def indicators_factory() -> List[Indicator]:
|
|
|
246
317
|
description="MFI Oversold Signal",
|
|
247
318
|
type_info="Oversold",
|
|
248
319
|
type=Optional[date],
|
|
249
|
-
function=lambda d:
|
|
320
|
+
function=lambda d: (d.MFI < 20),
|
|
250
321
|
),
|
|
251
322
|
Signal(
|
|
252
323
|
name="MFI_OVERBOUGHT",
|
|
253
324
|
description="MFI Overbought Signal",
|
|
254
325
|
type_info="Overbought",
|
|
255
326
|
type=Optional[date],
|
|
256
|
-
function=lambda d:
|
|
327
|
+
function=lambda d: (d.MFI > 80),
|
|
257
328
|
),
|
|
258
329
|
],
|
|
259
330
|
),
|
|
@@ -268,35 +339,33 @@ def indicators_factory() -> List[Indicator]:
|
|
|
268
339
|
description="Golden cross: SMA 50 crosses above SMA 200",
|
|
269
340
|
type_info="Oversold",
|
|
270
341
|
type=Optional[date],
|
|
271
|
-
function=lambda d:
|
|
342
|
+
function=lambda d: cross_simple(d.SMA_50, d.SMA_200),
|
|
343
|
+
in_use_backtest=True,
|
|
272
344
|
),
|
|
273
345
|
Signal(
|
|
274
346
|
name="DEATH_CROSS",
|
|
275
347
|
description="Death cross: SMA 50 crosses below SMA 200",
|
|
276
348
|
type_info="Overbought",
|
|
277
349
|
type=Optional[date],
|
|
278
|
-
function=lambda d:
|
|
279
|
-
),
|
|
280
|
-
Signal(
|
|
281
|
-
name="MOMENTUM_TIME_SPAN",
|
|
282
|
-
description="Momentum time span",
|
|
283
|
-
type_info="Overbought",
|
|
284
|
-
type=Optional[date],
|
|
285
|
-
function=lambda d: momentum(d),
|
|
350
|
+
function=lambda d: cross_simple(d.SMA_50, d.SMA_200, above=False),
|
|
286
351
|
),
|
|
287
352
|
Signal(
|
|
288
353
|
name="SMA_50_ABOVE_SMA_200",
|
|
289
354
|
description="SMA 50 is above SMA 200",
|
|
290
355
|
type_info="Overbought",
|
|
291
356
|
type=Optional[date],
|
|
292
|
-
function=lambda d:
|
|
357
|
+
function=lambda d: d.SMA_50 > d.SMA_200,
|
|
358
|
+
in_use_backtest=True,
|
|
359
|
+
processing=ProcessingFunction(date=find_last_true_run_start),
|
|
293
360
|
),
|
|
294
361
|
Signal(
|
|
295
362
|
name="PRICE_ABOVE_SMA_50",
|
|
296
363
|
description="Price is above SMA 50",
|
|
297
364
|
type_info="Overbought",
|
|
298
365
|
type=Optional[date],
|
|
299
|
-
function=lambda d:
|
|
366
|
+
function=lambda d: d.SMA_50 < d.CLOSE,
|
|
367
|
+
in_use_backtest=True,
|
|
368
|
+
processing=ProcessingFunction(date=find_last_true_run_start),
|
|
300
369
|
),
|
|
301
370
|
],
|
|
302
371
|
),
|
|
@@ -311,39 +380,44 @@ def indicators_factory() -> List[Indicator]:
|
|
|
311
380
|
description="Current price is lower than the 200-day high",
|
|
312
381
|
type_info="Oversold",
|
|
313
382
|
type=Optional[date],
|
|
314
|
-
function=lambda d: d[
|
|
315
|
-
0.6 * d["200_DAY_HIGH"] > d.LAST_PRICE
|
|
316
|
-
].last_valid_index(),
|
|
383
|
+
function=lambda d: 0.6 * d["200_DAY_HIGH"] > d.LAST_PRICE,
|
|
317
384
|
),
|
|
318
385
|
Signal(
|
|
319
386
|
name="LOWER_THAN_20_DAY_HIGH",
|
|
320
387
|
description="Current price is lower than the 20-day high",
|
|
321
388
|
type_info="Oversold",
|
|
322
389
|
type=Optional[date],
|
|
323
|
-
function=lambda d: d[
|
|
324
|
-
0.6 * d["20_DAY_HIGH"] > d.LAST_PRICE
|
|
325
|
-
].last_valid_index(),
|
|
390
|
+
function=lambda d: 0.6 * d["20_DAY_HIGH"] > d.LAST_PRICE,
|
|
326
391
|
),
|
|
327
392
|
Signal(
|
|
328
393
|
name="MEDIAN_WEEKLY_GROWTH",
|
|
329
394
|
description="Median weekly growth",
|
|
330
395
|
type_info="Oversold",
|
|
331
396
|
type=Optional[float],
|
|
332
|
-
function=lambda d:
|
|
397
|
+
function=lambda d: d.WEEKLY_GROWTH,
|
|
398
|
+
processing=ProcessingFunction(
|
|
399
|
+
number=lambda v: np.median(v.unique())
|
|
400
|
+
),
|
|
333
401
|
),
|
|
334
402
|
Signal(
|
|
335
403
|
name="MEDIAN_MONTHLY_GROWTH",
|
|
336
404
|
description="Median monthly growth",
|
|
337
405
|
type_info="Oversold",
|
|
338
406
|
type=Optional[float],
|
|
339
|
-
function=lambda d:
|
|
407
|
+
function=lambda d: d.MONTHLY_GROWTH,
|
|
408
|
+
processing=ProcessingFunction(
|
|
409
|
+
number=lambda v: np.median(v.unique())
|
|
410
|
+
),
|
|
340
411
|
),
|
|
341
412
|
Signal(
|
|
342
413
|
name="MEDIAN_YEARLY_GROWTH",
|
|
343
414
|
description="Median yearly growth",
|
|
344
415
|
type_info="Oversold",
|
|
345
416
|
type=Optional[float],
|
|
346
|
-
function=lambda d:
|
|
417
|
+
function=lambda d: d.YEARLY_GROWTH,
|
|
418
|
+
processing=ProcessingFunction(
|
|
419
|
+
number=lambda v: np.median(v.unique())
|
|
420
|
+
),
|
|
347
421
|
),
|
|
348
422
|
],
|
|
349
423
|
),
|
|
@@ -358,49 +432,61 @@ def indicators_factory() -> List[Indicator]:
|
|
|
358
432
|
type_info="Value",
|
|
359
433
|
description="Median daily Rate of Change of the last 30 days",
|
|
360
434
|
type=Optional[float],
|
|
361
|
-
function=lambda d:
|
|
435
|
+
function=lambda d: d.ROC_1,
|
|
436
|
+
processing=ProcessingFunction(
|
|
437
|
+
number=lambda v: np.median(v.tolist()[-30:])
|
|
438
|
+
),
|
|
362
439
|
),
|
|
363
440
|
Signal(
|
|
364
441
|
name="MEDIAN_RATE_OF_CHANGE_7_4",
|
|
365
442
|
type_info="Value",
|
|
366
443
|
description="Median weekly Rate of Change of the last 4 weeks",
|
|
367
444
|
type=Optional[float],
|
|
368
|
-
function=lambda d:
|
|
445
|
+
function=lambda d: d.ROC_7,
|
|
446
|
+
processing=ProcessingFunction(
|
|
447
|
+
number=lambda v: np.median(v.tolist()[-4:])
|
|
448
|
+
),
|
|
369
449
|
),
|
|
370
450
|
Signal(
|
|
371
451
|
name="MEDIAN_RATE_OF_CHANGE_7_12",
|
|
372
452
|
type_info="Value",
|
|
373
453
|
description="Median weekly Rate of Change of the last 12 weeks",
|
|
374
454
|
type=Optional[float],
|
|
375
|
-
function=lambda d:
|
|
455
|
+
function=lambda d: d.ROC_7,
|
|
456
|
+
processing=ProcessingFunction(
|
|
457
|
+
number=lambda v: np.median(v.tolist()[-12:])
|
|
458
|
+
),
|
|
376
459
|
),
|
|
377
460
|
Signal(
|
|
378
461
|
name="MEDIAN_RATE_OF_CHANGE_30",
|
|
379
462
|
type_info="Value",
|
|
380
463
|
description="Median monthly Rate of Change of the last 12 Months",
|
|
381
464
|
type=Optional[float],
|
|
382
|
-
function=lambda d:
|
|
465
|
+
function=lambda d: d.ROC_30,
|
|
466
|
+
processing=ProcessingFunction(
|
|
467
|
+
number=lambda v: np.median(v.tolist()[-12:])
|
|
468
|
+
),
|
|
383
469
|
),
|
|
384
470
|
Signal(
|
|
385
471
|
name="RATE_OF_CHANGE_30",
|
|
386
472
|
type_info="Value",
|
|
387
473
|
description="30-day Rate of Change",
|
|
388
474
|
type=Optional[float],
|
|
389
|
-
function=lambda d: d.ROC_30
|
|
475
|
+
function=lambda d: d.ROC_30,
|
|
390
476
|
),
|
|
391
477
|
Signal(
|
|
392
478
|
name="RATE_OF_CHANGE_7",
|
|
393
479
|
type_info="Value",
|
|
394
480
|
description="7-day Rate of Change",
|
|
395
481
|
type=Optional[float],
|
|
396
|
-
function=lambda d: d.ROC_7
|
|
482
|
+
function=lambda d: d.ROC_7,
|
|
397
483
|
),
|
|
398
484
|
Signal(
|
|
399
485
|
name="MOMENTUM",
|
|
400
486
|
type_info="Value",
|
|
401
487
|
description="7-day Rate of Change",
|
|
402
488
|
type=Optional[float],
|
|
403
|
-
function=lambda d: d.MOM
|
|
489
|
+
function=lambda d: d.MOM,
|
|
404
490
|
),
|
|
405
491
|
],
|
|
406
492
|
),
|
|
@@ -415,16 +501,14 @@ def indicators_factory() -> List[Indicator]:
|
|
|
415
501
|
type_info="Oversold",
|
|
416
502
|
description="Bullish momentum in money flow",
|
|
417
503
|
type=Optional[date],
|
|
418
|
-
function=lambda d:
|
|
504
|
+
function=lambda d: cross_value_series(d.ADOSC, 0, above=True),
|
|
419
505
|
),
|
|
420
506
|
Signal(
|
|
421
507
|
name="POSITIVE_ADOSC_20_DAY_BREAKOUT",
|
|
422
508
|
type_info="Oversold",
|
|
423
509
|
description="20-day breakout confirmed by positive ADOSC",
|
|
424
510
|
type=Optional[date],
|
|
425
|
-
function=lambda d: d
|
|
426
|
-
(d.ADOSC_SIGNAL == True) # noqa: E712
|
|
427
|
-
].last_valid_index(),
|
|
511
|
+
function=lambda d: (d.ADOSC_SIGNAL == True), # noqa: E712
|
|
428
512
|
),
|
|
429
513
|
],
|
|
430
514
|
),
|
|
@@ -447,53 +531,49 @@ def indicators_factory() -> List[Indicator]:
|
|
|
447
531
|
type_info="Long",
|
|
448
532
|
description="Morning Star Candlestick Pattern",
|
|
449
533
|
type=Optional[date],
|
|
450
|
-
function=lambda d: d
|
|
534
|
+
function=lambda d: d.CDLMORNINGSTAR == 100,
|
|
451
535
|
),
|
|
452
536
|
Signal(
|
|
453
537
|
name="CDL3LINESTRIKE",
|
|
454
538
|
description="3 Line Strike Candlestick Pattern",
|
|
455
539
|
type_info="Long",
|
|
456
540
|
type=Optional[date],
|
|
457
|
-
function=lambda d: d
|
|
541
|
+
function=lambda d: d.CDL3LINESTRIKE == 100,
|
|
458
542
|
),
|
|
459
543
|
Signal(
|
|
460
544
|
name="CDL3WHITESOLDIERS",
|
|
461
545
|
description="3 White Soldiers Candlestick Pattern",
|
|
462
546
|
type_info="Long",
|
|
463
547
|
type=Optional[date],
|
|
464
|
-
function=lambda d: d
|
|
465
|
-
(d.CDL3WHITESOLDIERS == 100)
|
|
466
|
-
].last_valid_index(),
|
|
548
|
+
function=lambda d: d.CDL3WHITESOLDIERS == 100,
|
|
467
549
|
),
|
|
468
550
|
Signal(
|
|
469
551
|
name="CDLABANDONEDBABY",
|
|
470
552
|
description="Abandoned Baby Candlestick Pattern",
|
|
471
553
|
type_info="Long",
|
|
472
554
|
type=Optional[date],
|
|
473
|
-
function=lambda d: d
|
|
474
|
-
(d.CDLABANDONEDBABY == 100)
|
|
475
|
-
].last_valid_index(),
|
|
555
|
+
function=lambda d: d.CDLABANDONEDBABY == 100,
|
|
476
556
|
),
|
|
477
557
|
Signal(
|
|
478
558
|
name="CDLTASUKIGAP",
|
|
479
559
|
description="Tasukigap Candlestick Pattern",
|
|
480
560
|
type_info="Long",
|
|
481
561
|
type=Optional[date],
|
|
482
|
-
function=lambda d: d
|
|
562
|
+
function=lambda d: d.CDLTASUKIGAP == 100,
|
|
483
563
|
),
|
|
484
564
|
Signal(
|
|
485
565
|
name="CDLPIERCING",
|
|
486
566
|
description="Piercing Candlestick Pattern",
|
|
487
567
|
type_info="Long",
|
|
488
568
|
type=Optional[date],
|
|
489
|
-
function=lambda d: d
|
|
569
|
+
function=lambda d: d.CDLPIERCING == 100,
|
|
490
570
|
),
|
|
491
571
|
Signal(
|
|
492
572
|
name="CDLENGULFING",
|
|
493
573
|
description="Engulfing Candlestick Pattern",
|
|
494
574
|
type_info="Long",
|
|
495
575
|
type=Optional[date],
|
|
496
|
-
function=lambda d: d
|
|
576
|
+
function=lambda d: d.CDLENGULFING == 100,
|
|
497
577
|
),
|
|
498
578
|
],
|
|
499
579
|
),
|
|
@@ -503,7 +583,15 @@ def indicators_factory() -> List[Indicator]:
|
|
|
503
583
|
class Indicators(BaseModel):
|
|
504
584
|
indicators: List[Indicator] = Field(default_factory=indicators_factory)
|
|
505
585
|
|
|
506
|
-
def
|
|
586
|
+
def in_use_backtest(self) -> List[str]:
|
|
587
|
+
return [
|
|
588
|
+
signal.name.lower()
|
|
589
|
+
for indicator in self.indicators
|
|
590
|
+
for signal in indicator.signals
|
|
591
|
+
if signal.in_use_backtest
|
|
592
|
+
]
|
|
593
|
+
|
|
594
|
+
def _compute(self, data: pd.DataFrame) -> None:
|
|
507
595
|
for indicator in self.indicators:
|
|
508
596
|
try:
|
|
509
597
|
indicator.compute(data)
|
|
@@ -514,8 +602,16 @@ class Indicators(BaseModel):
|
|
|
514
602
|
f"Computed {indicator.name} with {len(indicator.signals)} signals"
|
|
515
603
|
)
|
|
516
604
|
|
|
517
|
-
def
|
|
518
|
-
|
|
605
|
+
def compute_series(self, data: pd.DataFrame, symbol: str) -> List[SignalSeries]:
|
|
606
|
+
data__ = pd.concat(
|
|
607
|
+
[indicator.compute_series(data, symbol) for indicator in self.indicators]
|
|
608
|
+
)
|
|
609
|
+
return [
|
|
610
|
+
SignalSeries.model_validate(s) for s in data__.to_dict(orient="records")
|
|
611
|
+
]
|
|
612
|
+
|
|
613
|
+
def compute(self, data: pd.DataFrame) -> Dict[str, Any]:
|
|
614
|
+
self._compute(data)
|
|
519
615
|
res = {}
|
|
520
616
|
for indicator in self.indicators:
|
|
521
617
|
for signal in indicator.signals:
|