bullishpy 0.5.0__py3-none-any.whl → 0.7.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 +112 -251
- bullish/analysis/filter.py +83 -41
- bullish/analysis/functions.py +344 -0
- bullish/analysis/indicators.py +450 -0
- bullish/analysis/predefined_filters.py +87 -0
- bullish/app/app.py +149 -67
- bullish/database/alembic/versions/08ac1116e055_.py +592 -0
- bullish/database/alembic/versions/49c83f9eb5ac_.py +103 -0
- bullish/database/alembic/versions/ee5baabb35f8_.py +51 -0
- bullish/database/crud.py +5 -0
- bullish/database/schemas.py +13 -0
- bullish/figures/figures.py +52 -12
- bullish/interface/interface.py +3 -0
- bullish/jobs/tasks.py +10 -0
- bullish/utils/__init__.py +0 -0
- bullish/utils/checks.py +64 -0
- {bullishpy-0.5.0.dist-info → bullishpy-0.7.0.dist-info}/METADATA +3 -4
- {bullishpy-0.5.0.dist-info → bullishpy-0.7.0.dist-info}/RECORD +20 -12
- {bullishpy-0.5.0.dist-info → bullishpy-0.7.0.dist-info}/WHEEL +0 -0
- {bullishpy-0.5.0.dist-info → bullishpy-0.7.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import date
|
|
3
|
+
from typing import Optional, Callable
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import pandas_ta as ta # type: ignore
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
try:
|
|
12
|
+
import talib
|
|
13
|
+
except Exception:
|
|
14
|
+
logger.warning("Talib is not installed, skipping analysis")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def cross(
|
|
18
|
+
series_a: pd.Series, series_b: pd.Series, above: bool = True
|
|
19
|
+
) -> Optional[date]:
|
|
20
|
+
crossing = ta.cross(series_a=series_a, series_b=series_b, above=above)
|
|
21
|
+
if not crossing[crossing == 1].index.empty:
|
|
22
|
+
return pd.Timestamp(crossing[crossing == 1].index[-1]).date()
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def cross_value(series: pd.Series, number: int, above: bool = True) -> Optional[date]:
|
|
27
|
+
return cross(series, pd.Series(number, index=series.index), above=above)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def compute_adx(data: pd.DataFrame) -> pd.DataFrame:
|
|
31
|
+
results = pd.DataFrame(index=data.index)
|
|
32
|
+
results["ADX_14"] = talib.ADX(data.high, data.low, close=data.close) # type: ignore
|
|
33
|
+
results["MINUS_DI"] = talib.MINUS_DI(data.high, data.low, data.close) # type: ignore
|
|
34
|
+
results["PLUS_DI"] = talib.PLUS_DI(data.high, data.low, data.close) # type: ignore
|
|
35
|
+
return results
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def compute_pandas_ta_adx(data: pd.DataFrame) -> pd.DataFrame:
|
|
39
|
+
results = pd.DataFrame(index=data.index)
|
|
40
|
+
adx = ta.adx(data.high, data.low, data.close, length=14)
|
|
41
|
+
results["ADX_14"] = adx["ADX_14"]
|
|
42
|
+
results["MINUS_DI"] = adx["DMN_14"]
|
|
43
|
+
results["PLUS_DI"] = adx["DMP_14"]
|
|
44
|
+
return results
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def compute_macd(data: pd.DataFrame) -> pd.DataFrame:
|
|
48
|
+
results = pd.DataFrame(index=data.index)
|
|
49
|
+
(
|
|
50
|
+
results["MACD_12_26_9"],
|
|
51
|
+
results["MACD_12_26_9_SIGNAL"],
|
|
52
|
+
results["MACD_12_26_9_HIST"],
|
|
53
|
+
) = talib.MACD(
|
|
54
|
+
data.close # type: ignore
|
|
55
|
+
)
|
|
56
|
+
return results
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def compute_pandas_ta_macd(data: pd.DataFrame) -> pd.DataFrame:
|
|
60
|
+
|
|
61
|
+
macd = ta.macd(data.close, fast=12, slow=26, signal=9)
|
|
62
|
+
results = pd.DataFrame(index=macd.index)
|
|
63
|
+
results["MACD_12_26_9"] = macd["MACD_12_26_9"]
|
|
64
|
+
results["MACD_12_26_9_SIGNAL"] = macd["MACDs_12_26_9"]
|
|
65
|
+
results["MACD_12_26_9_HIST"] = macd["MACDh_12_26_9"]
|
|
66
|
+
return results
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def compute_rsi(data: pd.DataFrame) -> pd.DataFrame:
|
|
70
|
+
results = pd.DataFrame(index=data.index)
|
|
71
|
+
results["RSI"] = talib.RSI(data.close) # type: ignore
|
|
72
|
+
return results
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def compute_pandas_ta_rsi(data: pd.DataFrame) -> pd.DataFrame:
|
|
76
|
+
results = pd.DataFrame(index=data.index)
|
|
77
|
+
results["RSI"] = ta.rsi(data.close, length=14)
|
|
78
|
+
return results
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def compute_stoch(data: pd.DataFrame) -> pd.DataFrame:
|
|
82
|
+
results = pd.DataFrame(index=data.index)
|
|
83
|
+
results["SLOW_K"], results["SLOW_D"] = talib.STOCH(data.high, data.low, data.close) # type: ignore
|
|
84
|
+
return results
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def compute_pandas_ta_stoch(data: pd.DataFrame) -> pd.DataFrame:
|
|
88
|
+
results = pd.DataFrame(index=data.index)
|
|
89
|
+
stoch = ta.stoch(data.high, data.low, data.close, k=5, d=3, smooth_k=3)
|
|
90
|
+
results["SLOW_K"] = stoch["STOCHk_5_3_3"]
|
|
91
|
+
results["SLOW_D"] = stoch["STOCHd_5_3_3"]
|
|
92
|
+
return results
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def compute_mfi(data: pd.DataFrame) -> pd.DataFrame:
|
|
96
|
+
results = pd.DataFrame(index=data.index)
|
|
97
|
+
results["MFI"] = talib.MFI(data.high, data.low, data.close, data.volume) # type: ignore
|
|
98
|
+
return results
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def compute_pandas_ta_mfi(data: pd.DataFrame) -> pd.DataFrame:
|
|
102
|
+
results = pd.DataFrame(index=data.index)
|
|
103
|
+
results["MFI"] = ta.mfi(data.high, data.low, data.close, data.volume, length=14)
|
|
104
|
+
return results
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def compute_roc(data: pd.DataFrame) -> pd.DataFrame:
|
|
108
|
+
results = pd.DataFrame(index=data.index)
|
|
109
|
+
results["ROC_7"] = talib.ROC(data.close, timeperiod=7) # type: ignore
|
|
110
|
+
results["ROC_1"] = talib.ROC(data.close, timeperiod=1) # type: ignore
|
|
111
|
+
results["ROC_30"] = talib.ROC(data.close, timeperiod=30) # type: ignore
|
|
112
|
+
return results
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def compute_pandas_ta_roc(data: pd.DataFrame) -> pd.DataFrame:
|
|
116
|
+
results = pd.DataFrame(index=data.index)
|
|
117
|
+
results["ROC_7"] = ta.roc(data.close, length=7)
|
|
118
|
+
results["ROC_1"] = ta.roc(data.close, length=1)
|
|
119
|
+
results["ROC_30"] = ta.roc(data.close, length=30)
|
|
120
|
+
return results
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def compute_sma(data: pd.DataFrame) -> pd.DataFrame:
|
|
124
|
+
results = pd.DataFrame(index=data.index)
|
|
125
|
+
results["SMA_50"] = talib.SMA(data.close, timeperiod=50) # type: ignore
|
|
126
|
+
results["SMA_200"] = talib.SMA(data.close, timeperiod=200) # type: ignore
|
|
127
|
+
return results
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def compute_pandas_ta_sma(data: pd.DataFrame) -> pd.DataFrame:
|
|
131
|
+
results = pd.DataFrame(index=data.index)
|
|
132
|
+
results["SMA_50"] = ta.sma(data.close, length=50)
|
|
133
|
+
results["SMA_200"] = ta.sma(data.close, length=200)
|
|
134
|
+
return results
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def compute_adosc(data: pd.DataFrame) -> pd.DataFrame:
|
|
138
|
+
data_ = data.copy()
|
|
139
|
+
results = pd.DataFrame(index=data.index)
|
|
140
|
+
results["ADOSC"] = talib.ADOSC(data.high, data.low, data.close, data.volume) # type: ignore
|
|
141
|
+
data_["ADOSC"] = results["ADOSC"]
|
|
142
|
+
data_["HIGHEST_20"] = data_.close.rolling(window=20).max()
|
|
143
|
+
results["ADOSC_SIGNAL"] = (data_.close > data_["HIGHEST_20"].shift(1)) & (
|
|
144
|
+
data_["ADOSC"] > 0
|
|
145
|
+
)
|
|
146
|
+
return results
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def compute_pandas_ta_adosc(data: pd.DataFrame) -> pd.DataFrame:
|
|
150
|
+
data_ = data.copy()
|
|
151
|
+
results = pd.DataFrame(index=data.index)
|
|
152
|
+
results["ADOSC"] = ta.adosc(data.high, data.low, data.close, data.volume)
|
|
153
|
+
data_["ADOSC"] = results["ADOSC"]
|
|
154
|
+
data_["HIGHEST_20"] = data_.close.rolling(window=20).max()
|
|
155
|
+
results["ADOSC_SIGNAL"] = (data_.close > data_["HIGHEST_20"].shift(1)) & (
|
|
156
|
+
data_["ADOSC"] > 0
|
|
157
|
+
)
|
|
158
|
+
return results
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def compute_ad(data: pd.DataFrame) -> pd.DataFrame:
|
|
162
|
+
results = pd.DataFrame(index=data.index)
|
|
163
|
+
results["AD"] = talib.AD(data.high, data.low, data.close, data.volume) # type: ignore
|
|
164
|
+
return results
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def compute_pandas_ta_ad(data: pd.DataFrame) -> pd.DataFrame:
|
|
168
|
+
results = pd.DataFrame(index=data.index)
|
|
169
|
+
results["AD"] = ta.ad(data.high, data.low, data.close, data.volume)
|
|
170
|
+
return results
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def compute_obv(data: pd.DataFrame) -> pd.DataFrame:
|
|
174
|
+
results = pd.DataFrame(index=data.index)
|
|
175
|
+
results["OBV"] = talib.OBV(data.close, data.volume) # type: ignore
|
|
176
|
+
return results
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def compute_pandas_ta_obv(data: pd.DataFrame) -> pd.DataFrame:
|
|
180
|
+
results = pd.DataFrame(index=data.index)
|
|
181
|
+
results["OBV"] = ta.obv(data.close, data.volume)
|
|
182
|
+
return results
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def compute_atr(data: pd.DataFrame) -> pd.DataFrame:
|
|
186
|
+
results = pd.DataFrame(index=data.index)
|
|
187
|
+
results["ATR"] = talib.ATR(data.high, data.low, data.close) # type: ignore
|
|
188
|
+
return results
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def compute_pandas_ta_atr(data: pd.DataFrame) -> pd.DataFrame:
|
|
192
|
+
results = pd.DataFrame(index=data.index)
|
|
193
|
+
results["ATR"] = ta.atr(data.high, data.low, data.close, length=14)
|
|
194
|
+
return results
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def compute_natr(data: pd.DataFrame) -> pd.DataFrame:
|
|
198
|
+
results = pd.DataFrame(index=data.index)
|
|
199
|
+
results["NATR"] = talib.NATR(data.high, data.low, data.close) # type: ignore
|
|
200
|
+
return results
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def compute_pandas_ta_natr(data: pd.DataFrame) -> pd.DataFrame:
|
|
204
|
+
results = pd.DataFrame(index=data.index)
|
|
205
|
+
results["NATR"] = ta.natr(data.high, data.low, data.close, length=14)
|
|
206
|
+
return results
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def compute_trange(data: pd.DataFrame) -> pd.DataFrame:
|
|
210
|
+
results = pd.DataFrame(index=data.index)
|
|
211
|
+
results["TRANGE"] = talib.TRANGE(data.high, data.low, data.close) # type: ignore
|
|
212
|
+
return results
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def compute_pandas_ta_trange(data: pd.DataFrame) -> pd.DataFrame:
|
|
216
|
+
results = pd.DataFrame(index=data.index)
|
|
217
|
+
results["TRANGE"] = ta.true_range(data.high, data.low, data.close)
|
|
218
|
+
return results
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def compute_patterns(data: pd.DataFrame) -> pd.DataFrame:
|
|
222
|
+
results = pd.DataFrame(index=data.index)
|
|
223
|
+
results["CDLMORNINGSTAR"] = talib.CDLMORNINGSTAR(
|
|
224
|
+
data.open, data.high, data.low, data.close # type: ignore
|
|
225
|
+
)
|
|
226
|
+
results["CDL3LINESTRIKE"] = talib.CDL3LINESTRIKE(
|
|
227
|
+
data.open, data.high, data.low, data.close # type: ignore
|
|
228
|
+
)
|
|
229
|
+
results["CDL3WHITESOLDIERS"] = talib.CDL3WHITESOLDIERS(
|
|
230
|
+
data.open, data.high, data.low, data.close # type: ignore
|
|
231
|
+
)
|
|
232
|
+
results["CDLABANDONEDBABY"] = talib.CDLABANDONEDBABY(
|
|
233
|
+
data.open, data.high, data.low, data.close # type: ignore
|
|
234
|
+
)
|
|
235
|
+
results["CDLTASUKIGAP"] = talib.CDLTASUKIGAP(
|
|
236
|
+
data.open, data.high, data.low, data.close # type: ignore
|
|
237
|
+
)
|
|
238
|
+
results["CDLPIERCING"] = talib.CDLPIERCING(
|
|
239
|
+
data.open, data.high, data.low, data.close # type: ignore
|
|
240
|
+
)
|
|
241
|
+
results["CDLENGULFING"] = talib.CDLENGULFING(
|
|
242
|
+
data.open, data.high, data.low, data.close # type: ignore
|
|
243
|
+
)
|
|
244
|
+
return results
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class IndicatorFunction(BaseModel):
|
|
248
|
+
expected_columns: list[str]
|
|
249
|
+
functions: list[Callable[[pd.DataFrame], pd.DataFrame]]
|
|
250
|
+
|
|
251
|
+
def call(self, data: pd.DataFrame) -> pd.DataFrame:
|
|
252
|
+
data_ = None
|
|
253
|
+
for function in self.functions:
|
|
254
|
+
try:
|
|
255
|
+
data_ = function(data)
|
|
256
|
+
break
|
|
257
|
+
except Exception as e:
|
|
258
|
+
logger.warning(f"Fail to compute function {function.__name__}: {e}")
|
|
259
|
+
if data_ is None:
|
|
260
|
+
raise ValueError("No data returned from indicator functions.")
|
|
261
|
+
if not set(self.expected_columns).issubset(set(data_.columns)):
|
|
262
|
+
raise ValueError(
|
|
263
|
+
f"Expected columns {self.expected_columns} not found in data columns {data_.columns.tolist()}"
|
|
264
|
+
)
|
|
265
|
+
return data_
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
ADX = IndicatorFunction(
|
|
269
|
+
expected_columns=["ADX_14", "MINUS_DI", "PLUS_DI"],
|
|
270
|
+
functions=[compute_adx, compute_pandas_ta_adx],
|
|
271
|
+
)
|
|
272
|
+
MACD = IndicatorFunction(
|
|
273
|
+
expected_columns=["MACD_12_26_9", "MACD_12_26_9_SIGNAL", "MACD_12_26_9_HIST"],
|
|
274
|
+
functions=[compute_macd, compute_pandas_ta_macd],
|
|
275
|
+
)
|
|
276
|
+
RSI = IndicatorFunction(
|
|
277
|
+
expected_columns=["RSI"], functions=[compute_rsi, compute_pandas_ta_rsi]
|
|
278
|
+
)
|
|
279
|
+
STOCH = IndicatorFunction(
|
|
280
|
+
expected_columns=["SLOW_K", "SLOW_D"],
|
|
281
|
+
functions=[compute_stoch, compute_pandas_ta_stoch],
|
|
282
|
+
)
|
|
283
|
+
MFI = IndicatorFunction(
|
|
284
|
+
expected_columns=["MFI"], functions=[compute_mfi, compute_pandas_ta_mfi]
|
|
285
|
+
)
|
|
286
|
+
ROC = IndicatorFunction(
|
|
287
|
+
expected_columns=["ROC_7", "ROC_1", "ROC_30"],
|
|
288
|
+
functions=[compute_roc, compute_pandas_ta_roc],
|
|
289
|
+
)
|
|
290
|
+
CANDLESTOCK_PATTERNS = IndicatorFunction(
|
|
291
|
+
expected_columns=[
|
|
292
|
+
"CDLMORNINGSTAR",
|
|
293
|
+
"CDL3LINESTRIKE",
|
|
294
|
+
"CDL3WHITESOLDIERS",
|
|
295
|
+
"CDLABANDONEDBABY",
|
|
296
|
+
"CDLTASUKIGAP",
|
|
297
|
+
"CDLPIERCING",
|
|
298
|
+
"CDLENGULFING",
|
|
299
|
+
],
|
|
300
|
+
functions=[compute_patterns],
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
SMA = IndicatorFunction(
|
|
304
|
+
expected_columns=["SMA_50", "SMA_200"],
|
|
305
|
+
functions=[compute_sma, compute_pandas_ta_sma],
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
ADOSC = IndicatorFunction(
|
|
309
|
+
expected_columns=["ADOSC", "ADOSC_SIGNAL"],
|
|
310
|
+
functions=[compute_adosc, compute_pandas_ta_adosc],
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
AD = IndicatorFunction(
|
|
314
|
+
expected_columns=["AD"],
|
|
315
|
+
functions=[compute_ad, compute_pandas_ta_ad],
|
|
316
|
+
)
|
|
317
|
+
OBV = IndicatorFunction(
|
|
318
|
+
expected_columns=["OBV"],
|
|
319
|
+
functions=[compute_obv, compute_pandas_ta_obv],
|
|
320
|
+
)
|
|
321
|
+
ATR = IndicatorFunction(
|
|
322
|
+
expected_columns=["ATR"],
|
|
323
|
+
functions=[compute_atr, compute_pandas_ta_atr],
|
|
324
|
+
)
|
|
325
|
+
NATR = IndicatorFunction(
|
|
326
|
+
expected_columns=["NATR"],
|
|
327
|
+
functions=[compute_natr, compute_pandas_ta_natr],
|
|
328
|
+
)
|
|
329
|
+
TRANGE = IndicatorFunction(
|
|
330
|
+
expected_columns=["TRANGE"],
|
|
331
|
+
functions=[compute_trange, compute_pandas_ta_trange],
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def add_indicators(data: pd.DataFrame) -> pd.DataFrame:
|
|
336
|
+
indicators = [ADX, MACD, RSI, STOCH, SMA, ADOSC, AD, OBV, ATR, NATR, TRANGE]
|
|
337
|
+
expected_columns = [c for i in indicators for c in i.expected_columns]
|
|
338
|
+
for indicator in indicators:
|
|
339
|
+
data = pd.concat([data, indicator.call(data)], axis=1)
|
|
340
|
+
if not set(expected_columns).issubset(set(data.columns)):
|
|
341
|
+
raise ValueError(
|
|
342
|
+
f"Expected columns {expected_columns} not found in data columns {data.columns.tolist()}"
|
|
343
|
+
)
|
|
344
|
+
return data
|