bullishpy 0.5.0__tar.gz → 0.6.0__tar.gz
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.
- {bullishpy-0.5.0 → bullishpy-0.6.0}/PKG-INFO +3 -4
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/analysis/analysis.py +112 -251
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/analysis/filter.py +83 -41
- bullishpy-0.6.0/bullish/analysis/functions.py +344 -0
- bullishpy-0.6.0/bullish/analysis/indicators.py +450 -0
- bullishpy-0.6.0/bullish/analysis/predefined_filters.py +87 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/app/app.py +149 -67
- bullishpy-0.6.0/bullish/database/alembic/versions/08ac1116e055_.py +592 -0
- bullishpy-0.6.0/bullish/database/alembic/versions/49c83f9eb5ac_.py +103 -0
- bullishpy-0.6.0/bullish/database/alembic/versions/ee5baabb35f8_.py +51 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/database/crud.py +5 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/database/schemas.py +13 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/figures/figures.py +52 -12
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/interface/interface.py +3 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/jobs/tasks.py +10 -0
- bullishpy-0.6.0/bullish/utils/__init__.py +0 -0
- bullishpy-0.6.0/bullish/utils/checks.py +64 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/pyproject.toml +7 -5
- {bullishpy-0.5.0 → bullishpy-0.6.0}/README.md +0 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/__init__.py +0 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/analysis/__init__.py +0 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/app/__init__.py +0 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/cli.py +0 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/database/__init__.py +0 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/database/alembic/README +0 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/database/alembic/alembic.ini +0 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/database/alembic/env.py +0 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/database/alembic/script.py.mako +0 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/database/alembic/versions/037dbd721317_.py +0 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/database/alembic/versions/11d35a452b40_.py +0 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/database/alembic/versions/4b0a2f40b7d3_.py +0 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/database/alembic/versions/73564b60fe24_.py +0 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/database/scripts/create_revision.py +0 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/database/scripts/stamp.py +0 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/database/scripts/upgrade.py +0 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/database/settings.py +0 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/exceptions.py +0 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/figures/__init__.py +0 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/interface/__init__.py +0 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/jobs/__init__.py +0 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/jobs/app.py +0 -0
- {bullishpy-0.5.0 → bullishpy-0.6.0}/bullish/jobs/models.py +0 -0
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: bullishpy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary:
|
|
5
5
|
Author: aan
|
|
6
6
|
Author-email: andoludovic.andriamamonjy@gmail.com
|
|
7
|
-
Requires-Python: >=3.
|
|
7
|
+
Requires-Python: >=3.12,<3.13
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
11
9
|
Classifier: Programming Language :: Python :: 3.12
|
|
12
10
|
Requires-Dist: bearishpy (>=0.20.0,<0.21.0)
|
|
13
11
|
Requires-Dist: huey (>=2.5.3,<3.0.0)
|
|
@@ -16,6 +14,7 @@ Requires-Dist: plotly (>=6.1.2,<7.0.0)
|
|
|
16
14
|
Requires-Dist: streamlit (>=1.45.1,<2.0.0)
|
|
17
15
|
Requires-Dist: streamlit-file-browser (>=3.2.22,<4.0.0)
|
|
18
16
|
Requires-Dist: streamlit-pydantic (>=v0.6.1-rc.3,<0.7.0)
|
|
17
|
+
Requires-Dist: ta-lib (>=0.6.4,<0.7.0)
|
|
19
18
|
Requires-Dist: tickermood (>=0.4.0,<0.5.0)
|
|
20
19
|
Description-Content-Type: text/markdown
|
|
21
20
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from datetime import date
|
|
3
2
|
from typing import (
|
|
4
3
|
Annotated,
|
|
5
4
|
Any,
|
|
@@ -7,13 +6,12 @@ from typing import (
|
|
|
7
6
|
Optional,
|
|
8
7
|
Sequence,
|
|
9
8
|
Type,
|
|
10
|
-
cast,
|
|
11
9
|
get_args,
|
|
12
10
|
TYPE_CHECKING,
|
|
11
|
+
ClassVar,
|
|
13
12
|
)
|
|
14
13
|
|
|
15
14
|
import pandas as pd
|
|
16
|
-
import pandas_ta as ta # type: ignore
|
|
17
15
|
from bearish.interface.interface import BearishDbBase # type: ignore
|
|
18
16
|
from bearish.models.assets.equity import BaseEquity # type: ignore
|
|
19
17
|
from bearish.models.base import ( # type: ignore
|
|
@@ -41,6 +39,8 @@ from bearish.models.query.query import AssetQuery, Symbols # type: ignore
|
|
|
41
39
|
from bearish.types import TickerOnlySources # type: ignore
|
|
42
40
|
from pydantic import BaseModel, BeforeValidator, Field, create_model
|
|
43
41
|
|
|
42
|
+
from bullish.analysis.indicators import Indicators, IndicatorModels
|
|
43
|
+
|
|
44
44
|
if TYPE_CHECKING:
|
|
45
45
|
from bullish.database.crud import BullishDb
|
|
46
46
|
|
|
@@ -61,59 +61,6 @@ def to_float(value: Any) -> Optional[float]:
|
|
|
61
61
|
return float(value)
|
|
62
62
|
|
|
63
63
|
|
|
64
|
-
def price_growth(prices: pd.DataFrame, days: int, max: bool = False) -> Optional[float]:
|
|
65
|
-
prices_ = prices.copy()
|
|
66
|
-
last_index = prices_.last_valid_index()
|
|
67
|
-
delta = pd.Timedelta(days=days)
|
|
68
|
-
start_index = last_index - delta # type: ignore
|
|
69
|
-
|
|
70
|
-
try:
|
|
71
|
-
closest_index = prices_.index.unique().asof(start_index) # type: ignore
|
|
72
|
-
price = (
|
|
73
|
-
prices_.loc[closest_index].close
|
|
74
|
-
if not max
|
|
75
|
-
else prices_[closest_index:].close.max()
|
|
76
|
-
)
|
|
77
|
-
except Exception as e:
|
|
78
|
-
logger.warning(
|
|
79
|
-
f"""Failing to calculate price growth: {e}.""",
|
|
80
|
-
exc_info=True,
|
|
81
|
-
)
|
|
82
|
-
return None
|
|
83
|
-
return ( # type: ignore
|
|
84
|
-
(prices_.loc[last_index].close - price) * 100 / prices_.loc[last_index].close
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def buy_opportunity(
|
|
89
|
-
series_a: pd.Series, series_b: pd.Series # type: ignore
|
|
90
|
-
) -> Optional[date]:
|
|
91
|
-
sell = ta.cross(series_a=series_a, series_b=series_b)
|
|
92
|
-
buy = ta.cross(series_a=series_b, series_b=series_a)
|
|
93
|
-
if not buy[buy == 1].index.empty and not sell[sell == 1].index.empty:
|
|
94
|
-
last_buy_signal = pd.Timestamp(buy[buy == 1].index[-1])
|
|
95
|
-
last_sell_signal = pd.Timestamp(sell[sell == 1].index[-1])
|
|
96
|
-
if last_buy_signal > last_sell_signal:
|
|
97
|
-
return last_buy_signal
|
|
98
|
-
return None
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def perc(data: pd.Series) -> float: # type: ignore
|
|
102
|
-
return cast(float, ((data.iloc[-1] - data.iloc[0]) / data.iloc[0]) * 100)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def yoy(prices: pd.DataFrame) -> pd.Series: # type: ignore
|
|
106
|
-
return prices.close.resample("YE").apply(perc) # type: ignore
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def mom(prices: pd.DataFrame) -> pd.Series: # type: ignore
|
|
110
|
-
return prices.close.resample("ME").apply(perc) # type: ignore
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def wow(prices: pd.DataFrame) -> pd.Series: # type: ignore
|
|
114
|
-
return prices.close.resample("W").apply(perc) # type: ignore
|
|
115
|
-
|
|
116
|
-
|
|
117
64
|
def _load_data(
|
|
118
65
|
data: Sequence[DataSourceBase], symbol: str, class_: Type[DataSourceBase]
|
|
119
66
|
) -> pd.DataFrame:
|
|
@@ -128,23 +75,23 @@ def _load_data(
|
|
|
128
75
|
return pd.DataFrame(columns=columns).sort_index()
|
|
129
76
|
|
|
130
77
|
|
|
131
|
-
def _compute_growth(series: pd.Series) -> bool:
|
|
78
|
+
def _compute_growth(series: pd.Series) -> bool:
|
|
132
79
|
if series.empty:
|
|
133
80
|
return False
|
|
134
81
|
return all(series.pct_change(fill_method=None).dropna() > 0)
|
|
135
82
|
|
|
136
83
|
|
|
137
|
-
def _all_positive(series: pd.Series) -> bool:
|
|
84
|
+
def _all_positive(series: pd.Series) -> bool:
|
|
138
85
|
if series.empty:
|
|
139
86
|
return False
|
|
140
87
|
return all(series.dropna() > 0)
|
|
141
88
|
|
|
142
89
|
|
|
143
|
-
def _get_last(data: pd.Series) -> Optional[float]:
|
|
90
|
+
def _get_last(data: pd.Series) -> Optional[float]:
|
|
144
91
|
return data.iloc[-1] if not data.empty else None
|
|
145
92
|
|
|
146
93
|
|
|
147
|
-
def _abs(data: pd.Series) -> pd.Series:
|
|
94
|
+
def _abs(data: pd.Series) -> pd.Series:
|
|
148
95
|
try:
|
|
149
96
|
return abs(data)
|
|
150
97
|
except Exception as e:
|
|
@@ -152,19 +99,8 @@ def _abs(data: pd.Series) -> pd.Series: # type: ignore
|
|
|
152
99
|
return data
|
|
153
100
|
|
|
154
101
|
|
|
155
|
-
class
|
|
156
|
-
|
|
157
|
-
None, alias="RSI Last value", description="RSI last value", ge=0, le=100
|
|
158
|
-
)
|
|
159
|
-
macd_12_26_9_buy_date: Optional[date] = None
|
|
160
|
-
ma_50_200_buy_date: Optional[date] = None
|
|
161
|
-
slope_7: Optional[float] = None
|
|
162
|
-
slope_14: Optional[float] = None
|
|
163
|
-
slope_30: Optional[float] = None
|
|
164
|
-
slope_60: Optional[float] = None
|
|
165
|
-
last_adx: Optional[float] = None
|
|
166
|
-
last_dmp: Optional[float] = None
|
|
167
|
-
last_dmn: Optional[float] = None
|
|
102
|
+
class TechnicalAnalysisBase(BaseModel):
|
|
103
|
+
_description: ClassVar[str] = "General technical indicators"
|
|
168
104
|
last_price: Annotated[
|
|
169
105
|
Optional[float],
|
|
170
106
|
BeforeValidator(to_float),
|
|
@@ -172,191 +108,113 @@ class TechnicalAnalysis(BaseModel):
|
|
|
172
108
|
default=None,
|
|
173
109
|
),
|
|
174
110
|
]
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
year_to_date_growth: Annotated[
|
|
182
|
-
Optional[float],
|
|
183
|
-
Field(
|
|
184
|
-
default=None,
|
|
185
|
-
),
|
|
186
|
-
]
|
|
187
|
-
last_52_weeks_growth: Annotated[
|
|
188
|
-
Optional[float],
|
|
189
|
-
Field(
|
|
190
|
-
default=None,
|
|
191
|
-
),
|
|
192
|
-
]
|
|
193
|
-
last_week_growth: Annotated[
|
|
194
|
-
Optional[float],
|
|
195
|
-
Field(
|
|
196
|
-
default=None,
|
|
197
|
-
),
|
|
198
|
-
]
|
|
199
|
-
last_month_growth: Annotated[
|
|
200
|
-
Optional[float],
|
|
201
|
-
Field(
|
|
202
|
-
default=None,
|
|
203
|
-
),
|
|
204
|
-
]
|
|
205
|
-
last_year_growth: Annotated[
|
|
206
|
-
Optional[float],
|
|
207
|
-
Field(
|
|
208
|
-
default=None,
|
|
209
|
-
),
|
|
210
|
-
]
|
|
211
|
-
year_to_date_max_growth: Annotated[
|
|
212
|
-
Optional[float],
|
|
213
|
-
Field(
|
|
214
|
-
default=None,
|
|
215
|
-
),
|
|
216
|
-
]
|
|
217
|
-
last_week_max_growth: Annotated[
|
|
218
|
-
Optional[float],
|
|
219
|
-
Field(
|
|
220
|
-
default=None,
|
|
221
|
-
),
|
|
222
|
-
]
|
|
223
|
-
last_month_max_growth: Annotated[
|
|
224
|
-
Optional[float],
|
|
225
|
-
Field(
|
|
226
|
-
default=None,
|
|
227
|
-
),
|
|
228
|
-
]
|
|
229
|
-
last_year_max_growth: Annotated[
|
|
230
|
-
Optional[float],
|
|
231
|
-
Field(
|
|
232
|
-
default=None,
|
|
233
|
-
),
|
|
234
|
-
]
|
|
235
|
-
macd_12_26_9_buy: Annotated[
|
|
236
|
-
Optional[float],
|
|
237
|
-
Field(
|
|
238
|
-
default=None,
|
|
239
|
-
),
|
|
240
|
-
]
|
|
241
|
-
star_yoy: Annotated[
|
|
242
|
-
Optional[float],
|
|
243
|
-
Field(
|
|
244
|
-
default=None,
|
|
245
|
-
),
|
|
246
|
-
]
|
|
247
|
-
star_wow: Annotated[
|
|
248
|
-
Optional[float],
|
|
249
|
-
Field(
|
|
250
|
-
default=None,
|
|
251
|
-
),
|
|
252
|
-
]
|
|
253
|
-
star_mom: Annotated[
|
|
254
|
-
Optional[float],
|
|
255
|
-
Field(
|
|
256
|
-
default=None,
|
|
257
|
-
),
|
|
258
|
-
]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
TechnicalAnalysisModels = [*IndicatorModels, TechnicalAnalysisBase]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class TechnicalAnalysis(*TechnicalAnalysisModels): # type: ignore
|
|
259
117
|
|
|
260
118
|
@classmethod
|
|
261
119
|
def from_data(cls, prices: pd.DataFrame) -> "TechnicalAnalysis":
|
|
262
120
|
try:
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
last_index
|
|
266
|
-
- pd.Timestamp(year=last_index.year, month=1, day=1, tz="UTC") # type: ignore
|
|
267
|
-
).days
|
|
268
|
-
year_to_date_growth = price_growth(prices, year_to_date_days)
|
|
269
|
-
last_52_weeks_growth = price_growth(prices=prices, days=399)
|
|
270
|
-
last_week_growth = price_growth(prices=prices, days=7)
|
|
271
|
-
last_month_growth = price_growth(prices=prices, days=31)
|
|
272
|
-
last_year_growth = price_growth(prices=prices, days=365)
|
|
273
|
-
year_to_date_max_growth = price_growth(prices, year_to_date_days, max=True)
|
|
274
|
-
last_week_max_growth = price_growth(prices=prices, days=7, max=True)
|
|
275
|
-
last_month_max_growth = price_growth(prices=prices, days=31, max=True)
|
|
276
|
-
last_year_max_growth = price_growth(prices=prices, days=365, max=True)
|
|
277
|
-
prices.ta.sma(50, append=True)
|
|
278
|
-
prices.ta.sma(200, append=True)
|
|
279
|
-
prices.ta.adx(append=True)
|
|
280
|
-
prices["SLOPE_14"] = ta.linreg(prices.close, slope=True, length=14)
|
|
281
|
-
prices["SLOPE_7"] = ta.linreg(prices.close, slope=True, length=7)
|
|
282
|
-
prices["SLOPE_30"] = ta.linreg(prices.close, slope=True, length=30)
|
|
283
|
-
prices["SLOPE_60"] = ta.linreg(prices.close, slope=True, length=60)
|
|
284
|
-
prices.ta.macd(append=True)
|
|
285
|
-
prices.ta.rsi(append=True)
|
|
286
|
-
|
|
287
|
-
rsi_last_value = prices.RSI_14.iloc[-1]
|
|
288
|
-
macd_12_26_9_buy_date = buy_opportunity(
|
|
289
|
-
prices.MACDs_12_26_9, prices.MACD_12_26_9
|
|
290
|
-
)
|
|
291
|
-
star_yoy = yoy(prices).median()
|
|
292
|
-
star_mom = mom(prices).median()
|
|
293
|
-
star_wow = wow(prices).median()
|
|
294
|
-
try:
|
|
295
|
-
macd_12_26_9_buy = (
|
|
296
|
-
prices.MACD_12_26_9.iloc[-1] > prices.MACDs_12_26_9.iloc[-1]
|
|
297
|
-
)
|
|
298
|
-
except Exception as e:
|
|
299
|
-
logger.warning(
|
|
300
|
-
f"Failing to calculate MACD buy date: {e}", exc_info=True
|
|
301
|
-
)
|
|
302
|
-
macd_12_26_9_buy = None
|
|
303
|
-
ma_50_200_buy_date = buy_opportunity(prices.SMA_200, prices.SMA_50)
|
|
304
|
-
return cls(
|
|
305
|
-
rsi_last_value=rsi_last_value,
|
|
306
|
-
macd_12_26_9_buy_date=macd_12_26_9_buy_date,
|
|
307
|
-
macd_12_26_9_buy=macd_12_26_9_buy,
|
|
308
|
-
ma_50_200_buy_date=ma_50_200_buy_date,
|
|
309
|
-
last_price=prices.close.iloc[-1],
|
|
310
|
-
last_price_date=prices.index[-1],
|
|
311
|
-
last_adx=prices.ADX_14.iloc[-1],
|
|
312
|
-
last_dmp=prices.DMP_14.iloc[-1],
|
|
313
|
-
last_dmn=prices.DMN_14.iloc[-1],
|
|
314
|
-
slope_7=prices.SLOPE_7.iloc[-1],
|
|
315
|
-
slope_14=prices.SLOPE_14.iloc[-1],
|
|
316
|
-
slope_30=prices.SLOPE_30.iloc[-1],
|
|
317
|
-
slope_60=prices.SLOPE_60.iloc[-1],
|
|
318
|
-
year_to_date_growth=year_to_date_growth,
|
|
319
|
-
last_52_weeks_growth=last_52_weeks_growth,
|
|
320
|
-
last_week_growth=last_week_growth,
|
|
321
|
-
last_month_growth=last_month_growth,
|
|
322
|
-
last_year_growth=last_year_growth,
|
|
323
|
-
year_to_date_max_growth=year_to_date_max_growth,
|
|
324
|
-
last_week_max_growth=last_week_max_growth,
|
|
325
|
-
last_month_max_growth=last_month_max_growth,
|
|
326
|
-
last_year_max_growth=last_year_max_growth,
|
|
327
|
-
star_yoy=star_yoy,
|
|
328
|
-
star_mom=star_mom,
|
|
329
|
-
star_wow=star_wow,
|
|
330
|
-
)
|
|
121
|
+
res = Indicators().to_dict(prices)
|
|
122
|
+
return cls(last_price=prices.close.iloc[-1], **res)
|
|
331
123
|
except Exception as e:
|
|
332
124
|
logger.error(f"Failing to calculate technical analysis: {e}", exc_info=True)
|
|
333
|
-
return cls()
|
|
125
|
+
return cls()
|
|
334
126
|
|
|
335
127
|
|
|
336
128
|
class BaseFundamentalAnalysis(BaseModel):
|
|
337
|
-
positive_debt_to_equity: Optional[bool] =
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
129
|
+
positive_debt_to_equity: Optional[bool] = Field(
|
|
130
|
+
None,
|
|
131
|
+
description="True if the company's debt-to-equity ratio is favorable (typically low or improving).",
|
|
132
|
+
)
|
|
133
|
+
positive_return_on_assets: Optional[bool] = Field(
|
|
134
|
+
None,
|
|
135
|
+
description="True if the company reports a positive return on assets (ROA), "
|
|
136
|
+
"indicating efficient use of its assets.",
|
|
137
|
+
)
|
|
138
|
+
positive_return_on_equity: Optional[bool] = Field(
|
|
139
|
+
None,
|
|
140
|
+
description="True if the return on equity (ROE) is positive, "
|
|
141
|
+
"showing profitability relative to shareholder equity.",
|
|
142
|
+
)
|
|
143
|
+
positive_diluted_eps: Optional[bool] = Field(
|
|
144
|
+
None,
|
|
145
|
+
description="True if the diluted earnings per share (EPS), "
|
|
146
|
+
"which includes the effect of convertible securities, is positive.",
|
|
147
|
+
)
|
|
148
|
+
positive_basic_eps: Optional[bool] = Field(
|
|
149
|
+
None,
|
|
150
|
+
description="True if the basic earnings per share (EPS) is positive, reflecting profitable operations.",
|
|
151
|
+
)
|
|
152
|
+
growing_basic_eps: Optional[bool] = Field(
|
|
153
|
+
None,
|
|
154
|
+
description="True if the basic EPS has shown consistent growth over a defined time period.",
|
|
155
|
+
)
|
|
156
|
+
growing_diluted_eps: Optional[bool] = Field(
|
|
157
|
+
None,
|
|
158
|
+
description="True if the diluted EPS has consistently increased over time.",
|
|
159
|
+
)
|
|
160
|
+
positive_net_income: Optional[bool] = Field(
|
|
161
|
+
None,
|
|
162
|
+
description="True if the net income is positive, indicating overall profitability.",
|
|
163
|
+
)
|
|
164
|
+
positive_operating_income: Optional[bool] = Field(
|
|
165
|
+
None,
|
|
166
|
+
description="True if the company has positive operating income from its core business operations.",
|
|
167
|
+
)
|
|
168
|
+
growing_net_income: Optional[bool] = Field(
|
|
169
|
+
None, description="True if net income has shown consistent growth over time."
|
|
170
|
+
)
|
|
171
|
+
growing_operating_income: Optional[bool] = Field(
|
|
172
|
+
None,
|
|
173
|
+
description="True if the operating income has consistently increased over a period.",
|
|
174
|
+
)
|
|
175
|
+
positive_free_cash_flow: Optional[bool] = Field(
|
|
176
|
+
None,
|
|
177
|
+
description="True if the company has positive free cash flow, indicating financial flexibility and health.",
|
|
178
|
+
)
|
|
179
|
+
growing_operating_cash_flow: Optional[bool] = Field(
|
|
180
|
+
None,
|
|
181
|
+
description="True if the company's operating cash flow is growing steadily.",
|
|
182
|
+
)
|
|
183
|
+
operating_cash_flow_is_higher_than_net_income: Optional[bool] = Field(
|
|
184
|
+
None,
|
|
185
|
+
description="True if the operating cash flow exceeds net income, often a sign of high-quality earnings.",
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Capital Expenditure Ratios
|
|
189
|
+
mean_capex_ratio: Optional[float] = Field(
|
|
190
|
+
None,
|
|
191
|
+
description="Average capital expenditure (CapEx) ratio, usually "
|
|
192
|
+
"calculated as CapEx divided by revenue or operating cash flow.",
|
|
193
|
+
)
|
|
194
|
+
max_capex_ratio: Optional[float] = Field(
|
|
195
|
+
None, description="Maximum observed CapEx ratio over the evaluation period."
|
|
196
|
+
)
|
|
197
|
+
min_capex_ratio: Optional[float] = Field(
|
|
198
|
+
None, description="Minimum observed CapEx ratio over the evaluation period."
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Dividend Payout Ratios
|
|
202
|
+
mean_dividend_payout_ratio: Optional[float] = Field(
|
|
203
|
+
None,
|
|
204
|
+
description="Average dividend payout ratio, representing the proportion of earnings paid out as dividends.",
|
|
205
|
+
)
|
|
206
|
+
max_dividend_payout_ratio: Optional[float] = Field(
|
|
207
|
+
None, description="Maximum dividend payout ratio observed over the period."
|
|
208
|
+
)
|
|
209
|
+
min_dividend_payout_ratio: Optional[float] = Field(
|
|
210
|
+
None, description="Minimum dividend payout ratio observed over the period."
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# EPS Value
|
|
214
|
+
earning_per_share: Optional[float] = Field(
|
|
215
|
+
None,
|
|
216
|
+
description="The latest or most relevant value of earnings per share (EPS), indicating net income per share.",
|
|
217
|
+
)
|
|
360
218
|
|
|
361
219
|
def is_empty(self) -> bool:
|
|
362
220
|
return all(getattr(self, field) is None for field in self.model_fields)
|
|
@@ -489,7 +347,10 @@ class YearlyFundamentalAnalysis(BaseFundamentalAnalysis): ...
|
|
|
489
347
|
|
|
490
348
|
|
|
491
349
|
fields_with_prefix = {
|
|
492
|
-
f"{QUARTERLY}_{name}": (
|
|
350
|
+
f"{QUARTERLY}_{name}": (
|
|
351
|
+
field_info.annotation,
|
|
352
|
+
Field(default=None, description=field_info.description),
|
|
353
|
+
)
|
|
493
354
|
for name, field_info in BaseFundamentalAnalysis.model_fields.items()
|
|
494
355
|
}
|
|
495
356
|
|
|
@@ -510,7 +371,7 @@ class QuarterlyFundamentalAnalysis(BaseQuarterlyFundamentalAnalysis): # type: i
|
|
|
510
371
|
cash_flows=financials.quarterly_cash_flows,
|
|
511
372
|
ticker=ticker,
|
|
512
373
|
)
|
|
513
|
-
return cls.model_validate({f"{QUARTERLY}_{k}": v for k, v in base_financial_analisys.model_dump().items()}) # type: ignore
|
|
374
|
+
return cls.model_validate({f"{QUARTERLY}_{k}": v for k, v in base_financial_analisys.model_dump().items()}) # type: ignore
|
|
514
375
|
|
|
515
376
|
|
|
516
377
|
class FundamentalAnalysis(YearlyFundamentalAnalysis, QuarterlyFundamentalAnalysis):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
from datetime import date
|
|
3
|
-
from typing import Literal, get_args, Any, Optional, List, Tuple, Type
|
|
3
|
+
from typing import Literal, get_args, Any, Optional, List, Tuple, Type, Dict
|
|
4
4
|
|
|
5
5
|
from bearish.types import SeriesLength # type: ignore
|
|
6
6
|
from pydantic import BaseModel, Field, ConfigDict
|
|
@@ -8,9 +8,9 @@ from pydantic import create_model
|
|
|
8
8
|
from pydantic.fields import FieldInfo
|
|
9
9
|
|
|
10
10
|
from bullish.analysis.analysis import (
|
|
11
|
-
TechnicalAnalysis,
|
|
12
11
|
YearlyFundamentalAnalysis,
|
|
13
12
|
QuarterlyFundamentalAnalysis,
|
|
13
|
+
TechnicalAnalysisModels,
|
|
14
14
|
)
|
|
15
15
|
|
|
16
16
|
Industry = Literal[
|
|
@@ -425,11 +425,18 @@ def _get_type(name: str, info: FieldInfo) -> Tuple[Any, Any]:
|
|
|
425
425
|
if info.annotation == Optional[float]: # type: ignore
|
|
426
426
|
ge = next((item.ge for item in info.metadata if hasattr(item, "ge")), 0)
|
|
427
427
|
le = next((item.le for item in info.metadata if hasattr(item, "le")), 100)
|
|
428
|
-
|
|
428
|
+
default = [ge, le]
|
|
429
|
+
return (
|
|
430
|
+
Optional[List[float]],
|
|
431
|
+
Field(default=default, alias=alias, description=info.description),
|
|
432
|
+
)
|
|
429
433
|
elif info.annotation == Optional[date]: # type: ignore
|
|
430
434
|
le = date.today()
|
|
431
|
-
ge = le - datetime.timedelta(days=30 *
|
|
432
|
-
return (
|
|
435
|
+
ge = le - datetime.timedelta(days=30 * 2) # 30 days * 12 months
|
|
436
|
+
return (
|
|
437
|
+
List[date],
|
|
438
|
+
Field(default=[ge, le], alias=alias, description=info.description),
|
|
439
|
+
)
|
|
433
440
|
else:
|
|
434
441
|
raise NotImplementedError
|
|
435
442
|
|
|
@@ -467,49 +474,80 @@ PROPERTIES_GROUP = list(
|
|
|
467
474
|
)
|
|
468
475
|
)
|
|
469
476
|
|
|
470
|
-
GROUP_MAPPING = {
|
|
477
|
+
GROUP_MAPPING: Dict[str, List[str]] = {
|
|
471
478
|
"income": INCOME_GROUP,
|
|
472
479
|
"cash_flow": CASH_FLOW_GROUP,
|
|
473
480
|
"eps": EPS_GROUP,
|
|
474
481
|
"properties": PROPERTIES_GROUP,
|
|
475
|
-
"country": get_args(Country),
|
|
476
|
-
"industry": get_args(Industry),
|
|
477
|
-
"industry_group": get_args(IndustryGroup),
|
|
478
|
-
"sector": get_args(Sector),
|
|
482
|
+
"country": list(get_args(Country)),
|
|
483
|
+
"industry": list(get_args(Industry)),
|
|
484
|
+
"industry_group": list(get_args(IndustryGroup)),
|
|
485
|
+
"sector": list(get_args(Sector)),
|
|
486
|
+
"symbol": [],
|
|
479
487
|
}
|
|
480
488
|
|
|
481
489
|
|
|
482
|
-
def
|
|
490
|
+
def _create_fundamental_analysis_models() -> List[Type[BaseModel]]:
|
|
491
|
+
models = []
|
|
483
492
|
boolean_fields = {
|
|
484
|
-
"income": (Optional[List[str]], Field(default=None)),
|
|
485
|
-
"cash_flow": (
|
|
486
|
-
|
|
487
|
-
|
|
493
|
+
"income": (Optional[List[str]], Field(default=None, description="Income")),
|
|
494
|
+
"cash_flow": (
|
|
495
|
+
Optional[List[str]],
|
|
496
|
+
Field(default=None, description="Cash flow"),
|
|
497
|
+
),
|
|
498
|
+
"eps": (
|
|
499
|
+
Optional[List[str]],
|
|
500
|
+
Field(default=None, description="Earnings per share"),
|
|
501
|
+
),
|
|
502
|
+
"properties": (
|
|
503
|
+
Optional[List[str]],
|
|
504
|
+
Field(default=None, description="General properties"),
|
|
505
|
+
),
|
|
488
506
|
}
|
|
489
|
-
|
|
507
|
+
yearly_fields = {
|
|
490
508
|
name: _get_type(name, info)
|
|
491
|
-
for name, info in
|
|
492
|
-
|
|
493
|
-
**QuarterlyFundamentalAnalysis.model_fields,
|
|
494
|
-
}.items()
|
|
495
|
-
if info.annotation != Optional[bool]
|
|
509
|
+
for name, info in YearlyFundamentalAnalysis.model_fields.items()
|
|
510
|
+
if info.annotation != Optional[bool] # type: ignore
|
|
496
511
|
}
|
|
497
|
-
|
|
498
|
-
"FundamentalAnalysisFilter",
|
|
499
|
-
__config__=ConfigDict(populate_by_name=True),
|
|
500
|
-
**(boolean_fields | remaining_fields),
|
|
501
|
-
)
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
TechnicalAnalysisFilter = create_model( # type: ignore
|
|
505
|
-
"TechnicalAnalysisFilter",
|
|
506
|
-
__config__=ConfigDict(populate_by_name=True),
|
|
507
|
-
**{
|
|
512
|
+
quarterly_fields = {
|
|
508
513
|
name: _get_type(name, info)
|
|
509
|
-
for name, info in
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
514
|
+
for name, info in QuarterlyFundamentalAnalysis.model_fields.items()
|
|
515
|
+
if info.annotation != Optional[bool]
|
|
516
|
+
}
|
|
517
|
+
for property in [
|
|
518
|
+
(boolean_fields, "Selection filter", "SelectionFilter"),
|
|
519
|
+
(yearly_fields, "Yearly properties", "YearlyFilter"),
|
|
520
|
+
(quarterly_fields, "Quarterly properties", "QuarterlyFilter"),
|
|
521
|
+
]:
|
|
522
|
+
model_ = create_model( # type: ignore
|
|
523
|
+
property[-1],
|
|
524
|
+
__config__=ConfigDict(populate_by_name=True),
|
|
525
|
+
**property[0],
|
|
526
|
+
)
|
|
527
|
+
model_._description = property[1]
|
|
528
|
+
models.append(model_)
|
|
529
|
+
|
|
530
|
+
return models
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def create_technical_analysis_models() -> List[Type[BaseModel]]:
|
|
534
|
+
models = []
|
|
535
|
+
for model in TechnicalAnalysisModels:
|
|
536
|
+
model_ = create_model( # type: ignore
|
|
537
|
+
f"{model.__name__}Filter", # type: ignore
|
|
538
|
+
__config__=ConfigDict(populate_by_name=True),
|
|
539
|
+
**{
|
|
540
|
+
name: _get_type(name, info) for name, info in model.model_fields.items() # type: ignore
|
|
541
|
+
},
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
model_._description = model._description # type: ignore
|
|
545
|
+
models.append(model_)
|
|
546
|
+
return models
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
TechnicalAnalysisFilters = create_technical_analysis_models()
|
|
550
|
+
FundamentalAnalysisFilters = _create_fundamental_analysis_models()
|
|
513
551
|
|
|
514
552
|
|
|
515
553
|
class GeneralFilter(BaseModel):
|
|
@@ -517,15 +555,19 @@ class GeneralFilter(BaseModel):
|
|
|
517
555
|
industry: Optional[List[str]] = None
|
|
518
556
|
industry_group: Optional[List[str]] = None
|
|
519
557
|
sector: Optional[List[str]] = None
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
)
|
|
558
|
+
symbol: Optional[List[str]] = None
|
|
559
|
+
market_capitalization: Optional[List[float]] = Field(default=[5e8, 1e12])
|
|
523
560
|
|
|
524
561
|
|
|
525
|
-
class FilterQuery(GeneralFilter,
|
|
562
|
+
class FilterQuery(GeneralFilter, *TechnicalAnalysisFilters, *FundamentalAnalysisFilters): # type: ignore
|
|
526
563
|
|
|
527
564
|
def valid(self) -> bool:
|
|
528
|
-
return
|
|
565
|
+
return any(
|
|
566
|
+
bool(v)
|
|
567
|
+
for _, v in self.model_dump(
|
|
568
|
+
exclude_defaults=True, exclude_unset=True
|
|
569
|
+
).items()
|
|
570
|
+
)
|
|
529
571
|
|
|
530
572
|
def to_query(self) -> str:
|
|
531
573
|
parameters = self.model_dump(exclude_defaults=True, exclude_unset=True)
|