bullishpy 0.5.0__tar.gz → 0.7.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.

Files changed (42) hide show
  1. {bullishpy-0.5.0 → bullishpy-0.7.0}/PKG-INFO +3 -4
  2. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/analysis/analysis.py +112 -251
  3. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/analysis/filter.py +83 -41
  4. bullishpy-0.7.0/bullish/analysis/functions.py +344 -0
  5. bullishpy-0.7.0/bullish/analysis/indicators.py +450 -0
  6. bullishpy-0.7.0/bullish/analysis/predefined_filters.py +87 -0
  7. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/app/app.py +149 -67
  8. bullishpy-0.7.0/bullish/database/alembic/versions/08ac1116e055_.py +592 -0
  9. bullishpy-0.7.0/bullish/database/alembic/versions/49c83f9eb5ac_.py +103 -0
  10. bullishpy-0.7.0/bullish/database/alembic/versions/ee5baabb35f8_.py +51 -0
  11. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/database/crud.py +5 -0
  12. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/database/schemas.py +13 -0
  13. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/figures/figures.py +52 -12
  14. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/interface/interface.py +3 -0
  15. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/jobs/tasks.py +10 -0
  16. bullishpy-0.7.0/bullish/utils/__init__.py +0 -0
  17. bullishpy-0.7.0/bullish/utils/checks.py +64 -0
  18. {bullishpy-0.5.0 → bullishpy-0.7.0}/pyproject.toml +7 -5
  19. {bullishpy-0.5.0 → bullishpy-0.7.0}/README.md +0 -0
  20. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/__init__.py +0 -0
  21. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/analysis/__init__.py +0 -0
  22. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/app/__init__.py +0 -0
  23. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/cli.py +0 -0
  24. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/database/__init__.py +0 -0
  25. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/database/alembic/README +0 -0
  26. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/database/alembic/alembic.ini +0 -0
  27. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/database/alembic/env.py +0 -0
  28. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/database/alembic/script.py.mako +0 -0
  29. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/database/alembic/versions/037dbd721317_.py +0 -0
  30. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/database/alembic/versions/11d35a452b40_.py +0 -0
  31. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/database/alembic/versions/4b0a2f40b7d3_.py +0 -0
  32. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/database/alembic/versions/73564b60fe24_.py +0 -0
  33. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/database/scripts/create_revision.py +0 -0
  34. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/database/scripts/stamp.py +0 -0
  35. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/database/scripts/upgrade.py +0 -0
  36. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/database/settings.py +0 -0
  37. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/exceptions.py +0 -0
  38. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/figures/__init__.py +0 -0
  39. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/interface/__init__.py +0 -0
  40. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/jobs/__init__.py +0 -0
  41. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/jobs/app.py +0 -0
  42. {bullishpy-0.5.0 → bullishpy-0.7.0}/bullish/jobs/models.py +0 -0
@@ -1,13 +1,11 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: bullishpy
3
- Version: 0.5.0
3
+ Version: 0.7.0
4
4
  Summary:
5
5
  Author: aan
6
6
  Author-email: andoludovic.andriamamonjy@gmail.com
7
- Requires-Python: >=3.10,<3.13
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: # type: ignore
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: # type: ignore
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]: # type: ignore
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: # type: ignore
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 TechnicalAnalysis(BaseModel):
156
- rsi_last_value: Optional[float] = Field(
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
- last_price_date: Annotated[
176
- Optional[date],
177
- Field(
178
- default=None,
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
- last_index = prices.last_valid_index()
264
- year_to_date_days = (
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() # type: ignore
125
+ return cls()
334
126
 
335
127
 
336
128
  class BaseFundamentalAnalysis(BaseModel):
337
- positive_debt_to_equity: Optional[bool] = None
338
- positive_return_on_assets: Optional[bool] = None
339
- positive_return_on_equity: Optional[bool] = None
340
- positive_diluted_eps: Optional[bool] = None
341
- positive_basic_eps: Optional[bool] = None
342
- growing_basic_eps: Optional[bool] = None
343
- growing_diluted_eps: Optional[bool] = None
344
- positive_net_income: Optional[bool] = None
345
- positive_operating_income: Optional[bool] = None
346
- growing_net_income: Optional[bool] = None
347
- growing_operating_income: Optional[bool] = None
348
- positive_free_cash_flow: Optional[bool] = None
349
- growing_operating_cash_flow: Optional[bool] = None
350
- operating_cash_flow_is_higher_than_net_income: Optional[bool] = None
351
-
352
- mean_capex_ratio: Optional[float] = None
353
- max_capex_ratio: Optional[float] = None
354
- min_capex_ratio: Optional[float] = None
355
- mean_dividend_payout_ratio: Optional[float] = None
356
- max_dividend_payout_ratio: Optional[float] = None
357
- min_dividend_payout_ratio: Optional[float] = None
358
-
359
- earning_per_share: Optional[float] = None
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}": (field_info.annotation, Field(default=None))
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 # noqa: E501
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
- return (Optional[List[float]], Field(default=[ge, le], alias=alias))
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 * 12) # 30 days * 12 months
432
- return (List[date], Field(default=[ge, le], alias=alias))
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 _create_fundamental_analysis_model() -> Type[BaseModel]:
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": (Optional[List[str]], Field(default=None)),
486
- "eps": (Optional[List[str]], Field(default=None)),
487
- "properties": (Optional[List[str]], Field(default=None)),
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
- remaining_fields = {
507
+ yearly_fields = {
490
508
  name: _get_type(name, info)
491
- for name, info in {
492
- **YearlyFundamentalAnalysis.model_fields,
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
- return create_model(
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 TechnicalAnalysis.model_fields.items()
510
- },
511
- )
512
- FundamentalAnalysisFilter = _create_fundamental_analysis_model()
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
- market_capitalization: Optional[List[float]] = Field(
521
- default_factory=lambda: [5e8, 1e12]
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, TechnicalAnalysisFilter, FundamentalAnalysisFilter): # type: ignore
562
+ class FilterQuery(GeneralFilter, *TechnicalAnalysisFilters, *FundamentalAnalysisFilters): # type: ignore
526
563
 
527
564
  def valid(self) -> bool:
528
- return bool(self.model_dump(exclude_defaults=True, exclude_unset=True))
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)