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