bullishpy 0.28.0__tar.gz → 0.30.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.
Files changed (59) hide show
  1. {bullishpy-0.28.0 → bullishpy-0.30.0}/PKG-INFO +1 -1
  2. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/analysis/analysis.py +16 -10
  3. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/analysis/constants.py +4 -7
  4. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/analysis/filter.py +25 -5
  5. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/analysis/functions.py +15 -3
  6. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/analysis/indicators.py +58 -0
  7. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/analysis/industry_views.py +2 -2
  8. bullishpy-0.30.0/bullish/analysis/predefined_filters.py +196 -0
  9. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/app/app.py +9 -3
  10. bullishpy-0.30.0/bullish/database/alembic/versions/d0e58e050845_.py +39 -0
  11. bullishpy-0.30.0/bullish/database/alembic/versions/ff0cc4ba40ec_.py +69 -0
  12. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/crud.py +10 -0
  13. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/interface/interface.py +4 -1
  14. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/jobs/tasks.py +1 -1
  15. {bullishpy-0.28.0 → bullishpy-0.30.0}/pyproject.toml +1 -1
  16. bullishpy-0.28.0/bullish/analysis/predefined_filters.py +0 -391
  17. {bullishpy-0.28.0 → bullishpy-0.30.0}/README.md +0 -0
  18. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/__init__.py +0 -0
  19. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/analysis/__init__.py +0 -0
  20. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/analysis/backtest.py +0 -0
  21. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/app/__init__.py +0 -0
  22. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/cli.py +0 -0
  23. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/__init__.py +0 -0
  24. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/alembic/README +0 -0
  25. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/alembic/alembic.ini +0 -0
  26. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/alembic/env.py +0 -0
  27. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/alembic/script.py.mako +0 -0
  28. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/alembic/versions/037dbd721317_.py +0 -0
  29. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/alembic/versions/040b15fba458_.py +0 -0
  30. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/alembic/versions/08ac1116e055_.py +0 -0
  31. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/alembic/versions/11d35a452b40_.py +0 -0
  32. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/alembic/versions/12889a2cbd7d_.py +0 -0
  33. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/alembic/versions/17e51420e7ad_.py +0 -0
  34. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/alembic/versions/49c83f9eb5ac_.py +0 -0
  35. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/alembic/versions/4b0a2f40b7d3_.py +0 -0
  36. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/alembic/versions/5b10ee7604c1_.py +0 -0
  37. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/alembic/versions/6d252e23f543_.py +0 -0
  38. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/alembic/versions/73564b60fe24_.py +0 -0
  39. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/alembic/versions/79bc71ec6f9e_.py +0 -0
  40. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/alembic/versions/b76079e9845f_.py +0 -0
  41. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/alembic/versions/bf6b86dd5463_.py +0 -0
  42. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/alembic/versions/d663166c531d_.py +0 -0
  43. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/alembic/versions/ec25c8fa449f_.py +0 -0
  44. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/alembic/versions/ee5baabb35f8_.py +0 -0
  45. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/alembic/versions/fc191121f522_.py +0 -0
  46. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/schemas.py +0 -0
  47. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/scripts/create_revision.py +0 -0
  48. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/scripts/stamp.py +0 -0
  49. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/scripts/upgrade.py +0 -0
  50. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/database/settings.py +0 -0
  51. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/exceptions.py +0 -0
  52. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/figures/__init__.py +0 -0
  53. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/figures/figures.py +0 -0
  54. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/interface/__init__.py +0 -0
  55. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/jobs/__init__.py +0 -0
  56. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/jobs/app.py +0 -0
  57. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/jobs/models.py +0 -0
  58. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/utils/__init__.py +0 -0
  59. {bullishpy-0.28.0 → bullishpy-0.30.0}/bullish/utils/checks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: bullishpy
3
- Version: 0.28.0
3
+ Version: 0.30.0
4
4
  Summary:
5
5
  Author: aan
6
6
  Author-email: andoludovic.andriamamonjy@gmail.com
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  import time
3
+ from datetime import date
3
4
  from itertools import batched, chain
4
5
  from pathlib import Path
5
6
  from typing import (
@@ -15,7 +16,6 @@ from typing import (
15
16
  )
16
17
 
17
18
  import pandas as pd
18
- from bearish.interface.interface import BearishDbBase # type: ignore
19
19
  from bearish.models.assets.equity import BaseEquity # type: ignore
20
20
  from bearish.models.base import ( # type: ignore
21
21
  DataSourceBase,
@@ -87,10 +87,10 @@ def _compute_growth(series: pd.Series) -> bool:
87
87
  return all(series.pct_change(fill_method=None).dropna() > 0)
88
88
 
89
89
 
90
- def _all_positive(series: pd.Series) -> bool:
90
+ def _all_positive(series: pd.Series, threshold: int = 0) -> bool:
91
91
  if series.empty:
92
92
  return False
93
- return all(series.dropna() > 0)
93
+ return all(series.dropna() > threshold)
94
94
 
95
95
 
96
96
  def _get_last(data: pd.Series) -> Optional[float]:
@@ -274,7 +274,7 @@ class BaseFundamentalAnalysis(BaseModel):
274
274
  debt_to_equity = (
275
275
  balance_sheet.total_liabilities / balance_sheet.total_shareholder_equity
276
276
  ).dropna()
277
- positive_debt_to_equity = _all_positive(debt_to_equity)
277
+ positive_debt_to_equity = _all_positive(debt_to_equity, threshold=1)
278
278
 
279
279
  # Add relevant balance sheet data to financials
280
280
  financial["total_shareholder_equity"] = balance_sheet[
@@ -433,6 +433,10 @@ class FundamentalAnalysis(YearlyFundamentalAnalysis, QuarterlyFundamentalAnalysi
433
433
  ]
434
434
 
435
435
 
436
+ class AnalysisEarningsDate(BaseModel):
437
+ next_earnings_date: Optional[date] = None
438
+
439
+
436
440
  class AnalysisView(BaseModel):
437
441
  sector: Annotated[
438
442
  Optional[str],
@@ -484,15 +488,15 @@ class AnalysisView(BaseModel):
484
488
  default=None,
485
489
  ),
486
490
  ]
487
- median_yearly_growth: Optional[float] = None
488
- median_weekly_growth: Optional[float] = None
489
- median_monthly_growth: Optional[float] = None
491
+ yearly_growth: Optional[float] = None
492
+ weekly_growth: Optional[float] = None
493
+ monthly_growth: Optional[float] = None
490
494
 
491
495
 
492
- class Analysis(AnalysisView, BaseEquity, TechnicalAnalysis, FundamentalAnalysis): # type: ignore
496
+ class Analysis(AnalysisEarningsDate, AnalysisView, BaseEquity, TechnicalAnalysis, FundamentalAnalysis): # type: ignore
493
497
 
494
498
  @classmethod
495
- def from_ticker(cls, bearish_db: BearishDbBase, ticker: Ticker) -> "Analysis":
499
+ def from_ticker(cls, bearish_db: "BullishDb", ticker: Ticker) -> "Analysis":
496
500
  asset = bearish_db.read_assets(
497
501
  AssetQuery(
498
502
  symbols=Symbols(equities=[ticker]),
@@ -504,11 +508,13 @@ class Analysis(AnalysisView, BaseEquity, TechnicalAnalysis, FundamentalAnalysis)
504
508
  fundamental_analysis = FundamentalAnalysis.from_financials(financials, ticker)
505
509
  prices = Prices.from_ticker(bearish_db, ticker)
506
510
  technical_analysis = TechnicalAnalysis.from_data(prices.to_dataframe(), ticker)
511
+ next_earnings_date = bearish_db.read_next_earnings_date(ticker.symbol)
507
512
  return cls.model_validate(
508
513
  equity.model_dump()
509
514
  | fundamental_analysis.model_dump()
510
515
  | technical_analysis.model_dump()
511
516
  | {
517
+ "next_earnings_date": next_earnings_date,
512
518
  "price_per_earning_ratio": (
513
519
  (
514
520
  technical_analysis.last_price
@@ -518,7 +524,7 @@ class Analysis(AnalysisView, BaseEquity, TechnicalAnalysis, FundamentalAnalysis)
518
524
  and fundamental_analysis.earning_per_share != 0
519
525
  and fundamental_analysis.earning_per_share is not None
520
526
  else None
521
- )
527
+ ),
522
528
  }
523
529
  )
524
530
 
@@ -308,23 +308,20 @@ Sector = Literal[
308
308
  "Financial Services",
309
309
  "Conglomerates",
310
310
  ]
311
-
312
- SubCountry = Literal["United kingdom", "United states", "Germany", "Belgium", "France"]
311
+ Europe = Literal["Germany", "Belgium", "France"]
312
+ Us = Literal["United states"]
313
+ WesternCountries = Literal["United kingdom", Europe, Us]
313
314
  Country = Literal[
315
+ WesternCountries,
314
316
  "Australia",
315
317
  "China",
316
318
  "Japan",
317
- "United kingdom",
318
- "United states",
319
319
  "Poland",
320
320
  "Switzerland",
321
321
  "Canada",
322
322
  "Greece",
323
323
  "Spain",
324
- "Germany",
325
324
  "Indonesia",
326
- "Belgium",
327
- "France",
328
325
  "Netherlands",
329
326
  "British virgin islands",
330
327
  "Italy",
@@ -172,6 +172,10 @@ class GeneralFilter(BaseModel):
172
172
  industry_group: Optional[List[str]] = None
173
173
  sector: Optional[List[str]] = None
174
174
  symbol: Optional[List[str]] = None
175
+ limit: Optional[str] = None
176
+ next_earnings_date: List[date] = Field(
177
+ default=[date.today(), date.today() + datetime.timedelta(days=30 * 12)],
178
+ )
175
179
  market_capitalization: Optional[List[float]] = Field(default=[5e8, 1e12])
176
180
  price_per_earning_ratio: Optional[List[float]] = Field(default=[0.0, 1000.0])
177
181
 
@@ -186,11 +190,12 @@ class FilterQuery(GeneralFilter, *TechnicalAnalysisFilters, *FundamentalAnalysis
186
190
  ).items()
187
191
  )
188
192
 
189
- def to_query(self) -> str:
193
+ def to_query(self) -> str: # noqa: C901
190
194
  parameters = self.model_dump(exclude_defaults=True, exclude_unset=True)
191
195
  query = []
192
196
  order_by_desc = ""
193
197
  order_by_asc = ""
198
+ limit = None
194
199
  for parameter, value in parameters.items():
195
200
  if not value:
196
201
  continue
@@ -207,10 +212,20 @@ class FilterQuery(GeneralFilter, *TechnicalAnalysisFilters, *FundamentalAnalysis
207
212
  order_by_desc = f"ORDER BY {value} DESC"
208
213
  elif isinstance(value, str) and bool(value) and parameter == "order_by_asc":
209
214
  order_by_asc = f"ORDER BY {value} ASC"
215
+ elif isinstance(value, str) and bool(value) and parameter == "limit":
216
+ limit = f" LIMIT {int(value)}"
210
217
  elif (
211
- isinstance(value, list)
212
- and len(value) == SIZE_RANGE
213
- and all(isinstance(item, (int, float)) for item in value)
218
+ (
219
+ isinstance(value, list)
220
+ and len(value) == SIZE_RANGE
221
+ and all(isinstance(item, date) for item in value)
222
+ )
223
+ and parameter == "next_earnings_date"
224
+ or (
225
+ isinstance(value, list)
226
+ and len(value) == SIZE_RANGE
227
+ and all(isinstance(item, (int, float)) for item in value)
228
+ )
214
229
  ):
215
230
  query.append(f"{parameter} BETWEEN {value[0]} AND {value[1]}")
216
231
  elif (
@@ -229,7 +244,12 @@ class FilterQuery(GeneralFilter, *TechnicalAnalysisFilters, *FundamentalAnalysis
229
244
  else:
230
245
  raise NotImplementedError
231
246
  query_ = " AND ".join(query)
232
- return f"{query_} {order_by_desc.strip()} {order_by_asc.strip()}".strip()
247
+ query__ = f"{query_} {order_by_desc.strip()} {order_by_asc.strip()}".strip()
248
+ if limit is not None:
249
+ query__ += limit
250
+ else:
251
+ query__ += " LIMIT 1000"
252
+ return query__
233
253
 
234
254
 
235
255
  class FilterQueryStored(FilterQuery): ...
@@ -282,9 +282,17 @@ def compute_price(data: pd.DataFrame) -> pd.DataFrame:
282
282
  results["20_DAY_HIGH"] = data.close.rolling(window=20).max()
283
283
  results["20_DAY_LOW"] = data.close.rolling(window=20).min()
284
284
  results["LAST_PRICE"] = data.close
285
- results["WEEKLY_GROWTH"] = data.close.resample("W").transform(perc) # type: ignore
286
- results["MONTHLY_GROWTH"] = data.close.resample("ME").transform(perc) # type: ignore
287
- results["YEARLY_GROWTH"] = data.close.resample("YE").transform(perc) # type: ignore
285
+ results["WEEKLY_GROWTH"] = data.close.resample("W").transform(perc).ffill() # type: ignore
286
+ results["MONTHLY_GROWTH"] = data.close.resample("ME").transform(perc).ffill() # type: ignore
287
+ results["YEARLY_GROWTH"] = data.close.resample("YE").transform(perc).ffill() # type: ignore
288
+ return results
289
+
290
+
291
+ def compute_volume(data: pd.DataFrame) -> pd.DataFrame:
292
+ results = pd.DataFrame(index=data.index)
293
+ results["AVERAGE_VOLUME_10"] = data.volume.rolling(window=10).mean()
294
+ results["AVERAGE_VOLUME_30"] = data.volume.rolling(window=30).mean()
295
+ results["VOLUME"] = data.volume
288
296
  return results
289
297
 
290
298
 
@@ -397,6 +405,10 @@ TRANGE = IndicatorFunction(
397
405
  expected_columns=["TRANGE"],
398
406
  functions=[compute_trange, compute_pandas_ta_trange],
399
407
  )
408
+ VOLUME = IndicatorFunction(
409
+ expected_columns=["AVERAGE_VOLUME_10", "AVERAGE_VOLUME_30", "VOLUME"],
410
+ functions=[compute_volume],
411
+ )
400
412
  PRICE = IndicatorFunction(
401
413
  expected_columns=[
402
414
  "200_DAY_HIGH",
@@ -20,6 +20,7 @@ from bullish.analysis.functions import (
20
20
  cross_simple,
21
21
  cross_value_series,
22
22
  find_last_true_run_start,
23
+ VOLUME,
23
24
  )
24
25
 
25
26
  logger = logging.getLogger(__name__)
@@ -389,6 +390,27 @@ def indicators_factory() -> List[Indicator]:
389
390
  type=Optional[date],
390
391
  function=lambda d: 0.6 * d["20_DAY_HIGH"] > d.LAST_PRICE,
391
392
  ),
393
+ Signal(
394
+ name="WEEKLY_GROWTH",
395
+ description="weekly growth",
396
+ type_info="Oversold",
397
+ type=Optional[float],
398
+ function=lambda d: d.WEEKLY_GROWTH,
399
+ ),
400
+ Signal(
401
+ name="MONTHLY_GROWTH",
402
+ description="Median monthly growth",
403
+ type_info="Oversold",
404
+ type=Optional[float],
405
+ function=lambda d: d.MONTHLY_GROWTH,
406
+ ),
407
+ Signal(
408
+ name="YEARLY_GROWTH",
409
+ description="Median yearly growth",
410
+ type_info="Oversold",
411
+ type=Optional[float],
412
+ function=lambda d: d.YEARLY_GROWTH,
413
+ ),
392
414
  Signal(
393
415
  name="MEDIAN_WEEKLY_GROWTH",
394
416
  description="Median weekly growth",
@@ -419,6 +441,42 @@ def indicators_factory() -> List[Indicator]:
419
441
  number=lambda v: np.median(v.unique())
420
442
  ),
421
443
  ),
444
+ Signal(
445
+ name="LOWER_THAN_20_DAY_HIGH",
446
+ description="Current price is lower than the 20-day high",
447
+ type_info="Oversold",
448
+ type=Optional[date],
449
+ function=lambda d: 0.6 * d["20_DAY_HIGH"] > d.LAST_PRICE,
450
+ ),
451
+ ],
452
+ ),
453
+ Indicator(
454
+ name="VOLUME",
455
+ description="Volume based indicators",
456
+ expected_columns=VOLUME.expected_columns,
457
+ function=VOLUME.call,
458
+ signals=[
459
+ Signal(
460
+ name="AVERAGE_VOLUME_10",
461
+ type_info="Value",
462
+ description="Average volume over the last 10 days",
463
+ type=Optional[float],
464
+ function=lambda d: d.AVERAGE_VOLUME_10,
465
+ ),
466
+ Signal(
467
+ name="AVERAGE_VOLUME_30",
468
+ type_info="Value",
469
+ description="Average volume over the last 30 days",
470
+ type=Optional[float],
471
+ function=lambda d: d.AVERAGE_VOLUME_30,
472
+ ),
473
+ Signal(
474
+ name="VOLUME_ABOVE_AVERAGE",
475
+ type_info="Value",
476
+ description="Volume above average volume over the last 30 days",
477
+ type=Optional[date],
478
+ function=lambda d: d.AVERAGE_VOLUME_30 < d.VOLUME,
479
+ ),
422
480
  ],
423
481
  ),
424
482
  Indicator(
@@ -23,7 +23,7 @@ from bullish.analysis.constants import (
23
23
  IndustryGroup,
24
24
  Sector,
25
25
  Country,
26
- SubCountry,
26
+ WesternCountries,
27
27
  )
28
28
 
29
29
  if TYPE_CHECKING:
@@ -61,7 +61,7 @@ def get_industry_comparison_data(
61
61
  normalized_symbol = compute_normalized_close(symbol_data.close).rename("symbol")
62
62
  normalized_industry = industry_data.normalized_close.rename(industry)
63
63
  data = [normalized_symbol, normalized_industry]
64
- for country in get_args(SubCountry):
64
+ for country in get_args(WesternCountries):
65
65
  views = bullish_db.read_returns(type, industry, country)
66
66
  if views:
67
67
  industry_data = IndustryViews.from_views(views).to_dataframe()
@@ -0,0 +1,196 @@
1
+ import datetime
2
+ from datetime import timedelta
3
+ from typing import Dict, Any, Optional, List, Union, get_args
4
+
5
+ from bullish.analysis.analysis import AnalysisView
6
+ from bullish.analysis.backtest import (
7
+ BacktestQueryDate,
8
+ BacktestQueries,
9
+ BacktestQueryRange,
10
+ BacktestQuerySelection,
11
+ )
12
+ from bullish.analysis.constants import Europe, Us
13
+ from bullish.analysis.filter import FilterQuery, BOOLEAN_GROUP_MAPPING
14
+ from pydantic import BaseModel, Field
15
+
16
+ from bullish.analysis.indicators import Indicators
17
+ from bullish.database.crud import BullishDb
18
+
19
+ DATE_THRESHOLD = [
20
+ datetime.date.today() - datetime.timedelta(days=7),
21
+ datetime.date.today(),
22
+ ]
23
+
24
+
25
+ class NamedFilterQuery(FilterQuery):
26
+ name: str
27
+ description: Optional[str] = None
28
+
29
+ def to_dict(self) -> Dict[str, Any]:
30
+ return self.model_dump(
31
+ exclude_unset=True,
32
+ exclude_none=True,
33
+ exclude_defaults=True,
34
+ exclude={"name"},
35
+ )
36
+
37
+ def to_backtesting_query(
38
+ self, backtest_start_date: datetime.date
39
+ ) -> BacktestQueries:
40
+ queries: List[
41
+ Union[BacktestQueryRange, BacktestQueryDate, BacktestQuerySelection]
42
+ ] = []
43
+ in_use_backtests = Indicators().in_use_backtest()
44
+ for in_use in in_use_backtests:
45
+ value = self.to_dict().get(in_use)
46
+ if value and self.model_fields[in_use].annotation == List[datetime.date]:
47
+ delta = value[1] - value[0]
48
+ queries.append(
49
+ BacktestQueryDate(
50
+ name=in_use.upper(),
51
+ start=backtest_start_date - delta,
52
+ end=backtest_start_date,
53
+ table="signalseries",
54
+ )
55
+ )
56
+ for field in self.to_dict():
57
+ if field in BOOLEAN_GROUP_MAPPING:
58
+ value = self.to_dict().get(field)
59
+ if value and self.model_fields[field].annotation == Optional[List[str]]: # type: ignore
60
+ queries.extend(
61
+ [
62
+ BacktestQueryDate(
63
+ name=v.upper(),
64
+ start=backtest_start_date - timedelta(days=252),
65
+ end=backtest_start_date,
66
+ table="signalseries",
67
+ )
68
+ for v in value
69
+ ]
70
+ )
71
+
72
+ if field in AnalysisView.model_fields:
73
+ value = self.to_dict().get(field)
74
+ if (
75
+ value
76
+ and self.model_fields[field].annotation == Optional[List[float]] # type: ignore
77
+ and len(value) == 2
78
+ ):
79
+ queries.append(
80
+ BacktestQueryRange(
81
+ name=field.lower(),
82
+ min=value[0],
83
+ max=value[1],
84
+ table="analysis",
85
+ )
86
+ )
87
+ if value and self.model_fields[field].annotation == Optional[List[str]]: # type: ignore
88
+ queries.append(
89
+ BacktestQuerySelection(
90
+ name=field.lower(),
91
+ selections=value,
92
+ table="analysis",
93
+ )
94
+ )
95
+
96
+ return BacktestQueries(queries=queries)
97
+
98
+ def get_backtesting_symbols(
99
+ self, bullish_db: BullishDb, backtest_start_date: datetime.date
100
+ ) -> List[str]:
101
+ queries = self.to_backtesting_query(backtest_start_date)
102
+
103
+ return bullish_db.read_query(queries.to_query())["symbol"].tolist() # type: ignore
104
+
105
+ def country_variant(self, suffix: str, countries: List[str]) -> "NamedFilterQuery":
106
+ return NamedFilterQuery.model_validate(
107
+ self.model_dump()
108
+ | {"name": f"{self.name} ({suffix})", "country": countries}
109
+ )
110
+
111
+ def variants(self) -> List["NamedFilterQuery"]:
112
+ return [
113
+ self.country_variant("Europe", list(get_args(Europe))),
114
+ self.country_variant("Us", list(get_args(Us))),
115
+ ]
116
+
117
+
118
+ SMALL_CAP = NamedFilterQuery(
119
+ name="Small Cap",
120
+ last_price=[1, 20],
121
+ market_capitalization=[5e7, 5e8],
122
+ properties=["positive_debt_to_equity"],
123
+ average_volume_30=[50000, 5e9],
124
+ order_by_desc="market_capitalization",
125
+ ).variants()
126
+
127
+ TOP_PERFORMERS = NamedFilterQuery(
128
+ name="Top Performers",
129
+ sma_50_above_sma_200=[
130
+ datetime.date.today() - datetime.timedelta(days=5000),
131
+ datetime.date.today() - datetime.timedelta(days=10),
132
+ ],
133
+ price_above_sma_50=[
134
+ datetime.date.today() - datetime.timedelta(days=5000),
135
+ datetime.date.today() - datetime.timedelta(days=10),
136
+ ],
137
+ volume_above_average=DATE_THRESHOLD,
138
+ weekly_growth=[1, 100],
139
+ monthly_growth=[8, 100],
140
+ order_by_desc="market_capitalization",
141
+ ).variants()
142
+
143
+ LARGE_CAPS = NamedFilterQuery(
144
+ name="Large caps",
145
+ order_by_desc="market_capitalization",
146
+ limit="50",
147
+ ).variants()
148
+
149
+ NEXT_EARNINGS_DATE = NamedFilterQuery(
150
+ name="Next Earnings date",
151
+ order_by_desc="market_capitalization",
152
+ next_earnings_date=[
153
+ datetime.date.today(),
154
+ datetime.date.today() + timedelta(days=10),
155
+ ],
156
+ ).variants()
157
+
158
+ RSI_CROSSOVER_40 = NamedFilterQuery(
159
+ name="RSI cross-over 40",
160
+ rsi_bullish_crossover_40=DATE_THRESHOLD,
161
+ market_capitalization=[5e8, 1e13],
162
+ order_by_desc="market_capitalization",
163
+ country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
164
+ ).variants()
165
+
166
+ RSI_CROSSOVER_30 = NamedFilterQuery(
167
+ name="RSI cross-over 30",
168
+ price_per_earning_ratio=[10, 500],
169
+ rsi_bullish_crossover_30=DATE_THRESHOLD,
170
+ market_capitalization=[5e8, 1e13],
171
+ order_by_desc="market_capitalization",
172
+ ).variants()
173
+
174
+
175
+ def predefined_filters() -> list[NamedFilterQuery]:
176
+ return [
177
+ *SMALL_CAP,
178
+ *TOP_PERFORMERS,
179
+ *LARGE_CAPS,
180
+ *NEXT_EARNINGS_DATE,
181
+ *RSI_CROSSOVER_40,
182
+ *RSI_CROSSOVER_30,
183
+ ]
184
+
185
+
186
+ class PredefinedFilters(BaseModel):
187
+ filters: list[NamedFilterQuery] = Field(default_factory=predefined_filters)
188
+
189
+ def get_predefined_filter_names(self) -> list[str]:
190
+ return [filter.name for filter in self.filters]
191
+
192
+ def get_predefined_filter(self, name: str) -> Dict[str, Any]:
193
+ for filter in self.filters:
194
+ if filter.name == name:
195
+ return filter.to_dict()
196
+ raise ValueError(f"Filter with name '{name}' not found.")
@@ -146,14 +146,20 @@ def build_filter(model: Type[BaseModel], data: Dict[str, Any]) -> Dict[str, Any]
146
146
  if data.get(field) and data[field] != info.default:
147
147
  default = data[field]
148
148
  if info.annotation == Optional[List[str]]: # type: ignore
149
+ mapping = groups_mapping().get(field)
150
+ if not mapping:
151
+ continue
149
152
  data[field] = st.multiselect(
150
153
  name,
151
- groups_mapping()[field],
154
+ mapping,
152
155
  default=default,
153
156
  key=hash((model.__name__, field)),
154
157
  )
155
158
  elif info.annotation == Optional[str]: # type: ignore
156
- options = ["", *groups_mapping()[field]]
159
+ mapping = groups_mapping().get(field)
160
+ if not mapping:
161
+ continue
162
+ options = ["", *mapping]
157
163
  data[field] = st.selectbox(
158
164
  name,
159
165
  options,
@@ -183,7 +189,7 @@ def build_filter(model: Type[BaseModel], data: Dict[str, Any]) -> Dict[str, Any]
183
189
  except Exception as e:
184
190
  logger.error(
185
191
  f"Error building filter for {model.__name__}.{field} "
186
- f"with the parameters {(info.annotation, name, ge, le, tuple(default))}: {e}"
192
+ f"with the parameters {(info.annotation, name, ge, le)}: {e}"
187
193
  )
188
194
  raise e
189
195
  return data
@@ -0,0 +1,39 @@
1
+ """
2
+
3
+ Revision ID: d0e58e050845
4
+ Revises: ff0cc4ba40ec
5
+ Create Date: 2025-08-05 14:02:54.407561
6
+
7
+ """
8
+
9
+ from typing import Sequence, Union
10
+
11
+ from alembic import op
12
+ import sqlalchemy as sa
13
+ from sqlalchemy.dialects import sqlite
14
+
15
+ # revision identifiers, used by Alembic.
16
+ revision: str = "d0e58e050845"
17
+ down_revision: Union[str, None] = "ff0cc4ba40ec"
18
+ branch_labels: Union[str, Sequence[str], None] = None
19
+ depends_on: Union[str, Sequence[str], None] = None
20
+
21
+
22
+ def upgrade() -> None:
23
+ # ### commands auto generated by Alembic - please adjust! ###
24
+ with op.batch_alter_table("analysis", schema=None) as batch_op:
25
+ batch_op.add_column(sa.Column("next_earnings_date", sa.Date(), nullable=True))
26
+ batch_op.create_index(
27
+ "ix_analysis_next_earnings_date", ["next_earnings_date"], unique=False
28
+ )
29
+
30
+ # ### end Alembic commands ###
31
+
32
+
33
+ def downgrade() -> None:
34
+ # ### commands auto generated by Alembic - please adjust! ###
35
+ with op.batch_alter_table("analysis", schema=None) as batch_op:
36
+ batch_op.drop_index("ix_analysis_next_earnings_date")
37
+ batch_op.drop_column("next_earnings_date")
38
+
39
+ # ### end Alembic commands ###
@@ -0,0 +1,69 @@
1
+ """
2
+
3
+ Revision ID: ff0cc4ba40ec
4
+ Revises: 79bc71ec6f9e
5
+ Create Date: 2025-08-05 12:09:12.108606
6
+
7
+ """
8
+
9
+ from typing import Sequence, Union
10
+
11
+ from alembic import op
12
+ import sqlalchemy as sa
13
+ from sqlalchemy.dialects import sqlite
14
+
15
+ # revision identifiers, used by Alembic.
16
+ revision: str = "ff0cc4ba40ec"
17
+ down_revision: Union[str, None] = "79bc71ec6f9e"
18
+ branch_labels: Union[str, Sequence[str], None] = None
19
+ depends_on: Union[str, Sequence[str], None] = None
20
+
21
+
22
+ def upgrade() -> None:
23
+ # ### commands auto generated by Alembic - please adjust! ###
24
+ with op.batch_alter_table("analysis", schema=None) as batch_op:
25
+ batch_op.add_column(sa.Column("average_volume_10", sa.Float(), nullable=True))
26
+ batch_op.add_column(sa.Column("average_volume_30", sa.Float(), nullable=True))
27
+ batch_op.add_column(sa.Column("volume_above_average", sa.Date(), nullable=True))
28
+ batch_op.add_column(sa.Column("weekly_growth", sa.Float(), nullable=True))
29
+ batch_op.add_column(sa.Column("monthly_growth", sa.Float(), nullable=True))
30
+ batch_op.add_column(sa.Column("yearly_growth", sa.Float(), nullable=True))
31
+ batch_op.create_index(
32
+ "ix_analysis_average_volume_10", ["average_volume_10"], unique=False
33
+ )
34
+ batch_op.create_index(
35
+ "ix_analysis_average_volume_30", ["average_volume_30"], unique=False
36
+ )
37
+ batch_op.create_index(
38
+ "ix_analysis_monthly_growth", ["monthly_growth"], unique=False
39
+ )
40
+ batch_op.create_index(
41
+ "ix_analysis_volume_above_average", ["volume_above_average"], unique=False
42
+ )
43
+ batch_op.create_index(
44
+ "ix_analysis_weekly_growth", ["weekly_growth"], unique=False
45
+ )
46
+ batch_op.create_index(
47
+ "ix_analysis_yearly_growth", ["yearly_growth"], unique=False
48
+ )
49
+
50
+ # ### end Alembic commands ###
51
+
52
+
53
+ def downgrade() -> None:
54
+ # ### commands auto generated by Alembic - please adjust! ###
55
+ with op.batch_alter_table("analysis", schema=None) as batch_op:
56
+ batch_op.drop_index("ix_analysis_yearly_growth")
57
+ batch_op.drop_index("ix_analysis_weekly_growth")
58
+ batch_op.drop_index("ix_analysis_volume_above_average")
59
+ batch_op.drop_index("ix_analysis_monthly_growth")
60
+ batch_op.drop_index("ix_analysis_average_volume_30")
61
+ batch_op.drop_index("ix_analysis_average_volume_10")
62
+ batch_op.drop_column("yearly_growth")
63
+ batch_op.drop_column("monthly_growth")
64
+ batch_op.drop_column("weekly_growth")
65
+ batch_op.drop_column("volume_above_average")
66
+ batch_op.drop_column("average_volume_30")
67
+ batch_op.drop_column("average_volume_10")
68
+
69
+ # ### end Alembic commands ###
@@ -331,3 +331,13 @@ class BullishDb(BearishDb, BullishDbBase): # type: ignore
331
331
  return [BacktestResult.model_validate(r) for r in results]
332
332
  else:
333
333
  return []
334
+
335
+ def read_next_earnings_date(self, symbol: str) -> Optional[date]:
336
+ with Session(self._engine) as session:
337
+ stmt = select(EarningsDateORM.date).where(
338
+ EarningsDateORM.symbol == symbol, EarningsDateORM.date > date.today()
339
+ )
340
+ result = session.exec(stmt).first()
341
+ if result:
342
+ return result.date() # type: ignore
343
+ return None
@@ -35,7 +35,7 @@ class BullishDbBase(BearishDbBase): # type: ignore
35
35
  query_ = query.to_query()
36
36
  fields = ",".join(list(AnalysisView.model_fields))
37
37
  query_str: str = f"""
38
- SELECT {fields} FROM analysis WHERE {query_} LIMIT 1000
38
+ SELECT {fields} FROM analysis WHERE {query_}
39
39
  """ # noqa: S608
40
40
  return self._read_filter_query(query_str)
41
41
 
@@ -149,3 +149,6 @@ class BullishDbBase(BearishDbBase): # type: ignore
149
149
  def read_many_backtest_results(
150
150
  self, query: Optional[BacktestResultQuery] = None
151
151
  ) -> List[BacktestResult]: ...
152
+
153
+ @abc.abstractmethod
154
+ def read_next_earnings_date(self, symbol: str) -> Optional[date]: ...
@@ -111,4 +111,4 @@ def news(
111
111
  task: Optional[Task] = None,
112
112
  ) -> None:
113
113
  database_config = DatabaseConfig(database_path=database_path, no_migration=True)
114
- get_news(symbols, database_config, headless=headless, model_name = "gpt-4o-mini")
114
+ get_news(symbols, database_config, headless=headless, model_name="gpt-4o-mini")
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "bullishpy"
3
- version = "0.28.0"
3
+ version = "0.30.0"
4
4
  description = ""
5
5
  authors = ["aan <andoludovic.andriamamonjy@gmail.com>"]
6
6
  readme = "README.md"
@@ -1,391 +0,0 @@
1
- import datetime
2
- from datetime import timedelta
3
- from typing import Dict, Any, Optional, List, Union
4
-
5
- from bullish.analysis.analysis import AnalysisView
6
- from bullish.analysis.backtest import (
7
- BacktestQueryDate,
8
- BacktestQueries,
9
- BacktestQueryRange,
10
- BacktestQuerySelection,
11
- )
12
- from bullish.analysis.filter import FilterQuery, BOOLEAN_GROUP_MAPPING
13
- from pydantic import BaseModel, Field
14
-
15
- from bullish.analysis.indicators import Indicators
16
- from bullish.database.crud import BullishDb
17
-
18
- DATE_THRESHOLD = [
19
- datetime.date.today() - datetime.timedelta(days=7),
20
- datetime.date.today(),
21
- ]
22
-
23
-
24
- class NamedFilterQuery(FilterQuery):
25
- name: str
26
- description: Optional[str] = None
27
-
28
- def to_dict(self) -> Dict[str, Any]:
29
- return self.model_dump(
30
- exclude_unset=True,
31
- exclude_none=True,
32
- exclude_defaults=True,
33
- exclude={"name"},
34
- )
35
-
36
- def to_backtesting_query(
37
- self, backtest_start_date: datetime.date
38
- ) -> BacktestQueries:
39
- queries: List[
40
- Union[BacktestQueryRange, BacktestQueryDate, BacktestQuerySelection]
41
- ] = []
42
- in_use_backtests = Indicators().in_use_backtest()
43
- for in_use in in_use_backtests:
44
- value = self.to_dict().get(in_use)
45
- if value and self.model_fields[in_use].annotation == List[datetime.date]:
46
- delta = value[1] - value[0]
47
- queries.append(
48
- BacktestQueryDate(
49
- name=in_use.upper(),
50
- start=backtest_start_date - delta,
51
- end=backtest_start_date,
52
- table="signalseries",
53
- )
54
- )
55
- for field in self.to_dict():
56
- if field in BOOLEAN_GROUP_MAPPING:
57
- value = self.to_dict().get(field)
58
- if value and self.model_fields[field].annotation == Optional[List[str]]: # type: ignore
59
- queries.extend(
60
- [
61
- BacktestQueryDate(
62
- name=v.upper(),
63
- start=backtest_start_date - timedelta(days=252),
64
- end=backtest_start_date,
65
- table="signalseries",
66
- )
67
- for v in value
68
- ]
69
- )
70
-
71
- if field in AnalysisView.model_fields:
72
- value = self.to_dict().get(field)
73
- if (
74
- value
75
- and self.model_fields[field].annotation == Optional[List[float]] # type: ignore
76
- and len(value) == 2
77
- ):
78
- queries.append(
79
- BacktestQueryRange(
80
- name=field.lower(),
81
- min=value[0],
82
- max=value[1],
83
- table="analysis",
84
- )
85
- )
86
- if value and self.model_fields[field].annotation == Optional[List[str]]: # type: ignore
87
- queries.append(
88
- BacktestQuerySelection(
89
- name=field.lower(),
90
- selections=value,
91
- table="analysis",
92
- )
93
- )
94
-
95
- return BacktestQueries(queries=queries)
96
-
97
- def get_backtesting_symbols(
98
- self, bullish_db: BullishDb, backtest_start_date: datetime.date
99
- ) -> List[str]:
100
- queries = self.to_backtesting_query(backtest_start_date)
101
-
102
- return bullish_db.read_query(queries.to_query())["symbol"].tolist() # type: ignore
103
-
104
-
105
- STRONG_FUNDAMENTALS = NamedFilterQuery(
106
- name="Strong Fundamentals",
107
- income=[
108
- "positive_operating_income",
109
- "growing_operating_income",
110
- "positive_net_income",
111
- "growing_net_income",
112
- ],
113
- cash_flow=["positive_free_cash_flow", "growing_operating_cash_flow"],
114
- eps=["positive_diluted_eps", "growing_diluted_eps"],
115
- properties=[
116
- "operating_cash_flow_is_higher_than_net_income",
117
- "positive_return_on_equity",
118
- "positive_return_on_assets",
119
- "positive_debt_to_equity",
120
- ],
121
- market_capitalization=[1e10, 1e12], # 1 billion to 1 trillion
122
- rsi_bullish_crossover_30=DATE_THRESHOLD,
123
- )
124
-
125
- GOOD_FUNDAMENTALS = NamedFilterQuery(
126
- name="Good Fundamentals",
127
- income=[
128
- "positive_operating_income",
129
- "positive_net_income",
130
- ],
131
- cash_flow=["positive_free_cash_flow"],
132
- eps=["positive_diluted_eps"],
133
- properties=[
134
- "positive_return_on_equity",
135
- "positive_return_on_assets",
136
- "positive_debt_to_equity",
137
- ],
138
- market_capitalization=[1e10, 1e12], # 1 billion to 1 trillion
139
- rsi_bullish_crossover_30=DATE_THRESHOLD,
140
- )
141
-
142
- RSI_CROSSOVER_30_GROWTH_STOCK_STRONG_FUNDAMENTAL = NamedFilterQuery(
143
- name="RSI cross-over 30 growth stock strong fundamental",
144
- income=[
145
- "positive_operating_income",
146
- "growing_operating_income",
147
- "positive_net_income",
148
- "growing_net_income",
149
- ],
150
- cash_flow=["positive_free_cash_flow"],
151
- properties=["operating_cash_flow_is_higher_than_net_income"],
152
- price_per_earning_ratio=[10, 100],
153
- rsi_bullish_crossover_30=DATE_THRESHOLD,
154
- market_capitalization=[5e8, 1e12],
155
- order_by_desc="market_capitalization",
156
- country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
157
- )
158
- RSI_CROSSOVER_40_GROWTH_STOCK_STRONG_FUNDAMENTAL = NamedFilterQuery(
159
- name="RSI cross-over 40 growth stock strong fundamental",
160
- income=[
161
- "positive_operating_income",
162
- "growing_operating_income",
163
- "positive_net_income",
164
- "growing_net_income",
165
- ],
166
- cash_flow=["positive_free_cash_flow"],
167
- properties=["operating_cash_flow_is_higher_than_net_income"],
168
- price_per_earning_ratio=[10, 500],
169
- rsi_bullish_crossover_40=DATE_THRESHOLD,
170
- market_capitalization=[5e8, 1e12],
171
- order_by_desc="market_capitalization",
172
- country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
173
- )
174
-
175
- RSI_CROSSOVER_30_GROWTH_STOCK = NamedFilterQuery(
176
- name="RSI cross-over 30 growth stock",
177
- price_per_earning_ratio=[10, 500],
178
- rsi_bullish_crossover_30=DATE_THRESHOLD,
179
- market_capitalization=[1e10, 1e13],
180
- order_by_desc="market_capitalization",
181
- country=[
182
- "Germany",
183
- "United states",
184
- "France",
185
- "United kingdom",
186
- "Canada",
187
- "Japan",
188
- "Belgium",
189
- ],
190
- )
191
-
192
- MEDIAN_YEARLY_GROWTH = NamedFilterQuery(
193
- name="Median yearly growth",
194
- market_capitalization=[1e6, 1e13],
195
- median_yearly_growth=[40, 1000],
196
- last_price=[1, 100],
197
- order_by_asc="last_price",
198
- country=[
199
- "Germany",
200
- "United states",
201
- "France",
202
- "Belgium",
203
- ],
204
- )
205
- RSI_CROSSOVER_40_GROWTH_STOCK = NamedFilterQuery(
206
- name="RSI cross-over 40 growth stock",
207
- price_per_earning_ratio=[10, 500],
208
- rsi_bullish_crossover_40=DATE_THRESHOLD,
209
- market_capitalization=[1e10, 1e13],
210
- order_by_desc="market_capitalization",
211
- country=[
212
- "Germany",
213
- "United states",
214
- "France",
215
- "United kingdom",
216
- "Canada",
217
- "Japan",
218
- "Belgium",
219
- ],
220
- )
221
-
222
-
223
- MOMENTUM_GROWTH_GOOD_FUNDAMENTALS = NamedFilterQuery(
224
- name="Momentum Growth Good Fundamentals (RSI 30)",
225
- cash_flow=["positive_free_cash_flow"],
226
- properties=["operating_cash_flow_is_higher_than_net_income"],
227
- price_per_earning_ratio=[10, 500],
228
- rsi_bullish_crossover_30=[
229
- datetime.date.today() - datetime.timedelta(days=7),
230
- datetime.date.today(),
231
- ],
232
- macd_12_26_9_bullish_crossover=[
233
- datetime.date.today() - datetime.timedelta(days=7),
234
- datetime.date.today(),
235
- ],
236
- sma_50_above_sma_200=[
237
- datetime.date.today() - datetime.timedelta(days=5000),
238
- datetime.date.today() - datetime.timedelta(days=10),
239
- ],
240
- market_capitalization=[5e8, 1e12],
241
- order_by_desc="momentum",
242
- country=[
243
- "Germany",
244
- "United states",
245
- "France",
246
- "United kingdom",
247
- "Canada",
248
- "Japan",
249
- "Belgium",
250
- ],
251
- )
252
-
253
- MOMENTUM_GROWTH_STRONG_FUNDAMENTALS = NamedFilterQuery(
254
- name="Momentum Growth Strong Fundamentals (RSI 30)",
255
- income=[
256
- "positive_operating_income",
257
- "growing_operating_income",
258
- "positive_net_income",
259
- "growing_net_income",
260
- ],
261
- cash_flow=["positive_free_cash_flow"],
262
- properties=["operating_cash_flow_is_higher_than_net_income"],
263
- price_per_earning_ratio=[10, 500],
264
- rsi_bullish_crossover_30=[
265
- datetime.date.today() - datetime.timedelta(days=7),
266
- datetime.date.today(),
267
- ],
268
- macd_12_26_9_bullish_crossover=[
269
- datetime.date.today() - datetime.timedelta(days=7),
270
- datetime.date.today(),
271
- ],
272
- sma_50_above_sma_200=[
273
- datetime.date.today() - datetime.timedelta(days=5000),
274
- datetime.date.today() - datetime.timedelta(days=10),
275
- ],
276
- market_capitalization=[5e8, 1e12],
277
- order_by_desc="momentum",
278
- country=[
279
- "Germany",
280
- "United states",
281
- "France",
282
- "United kingdom",
283
- "Canada",
284
- "Japan",
285
- "Belgium",
286
- ],
287
- )
288
- MOMENTUM_GROWTH_RSI_30 = NamedFilterQuery(
289
- name="Momentum Growth Screener (RSI 30)",
290
- price_per_earning_ratio=[10, 500],
291
- rsi_bullish_crossover_30=[
292
- datetime.date.today() - datetime.timedelta(days=7),
293
- datetime.date.today(),
294
- ],
295
- macd_12_26_9_bullish_crossover=[
296
- datetime.date.today() - datetime.timedelta(days=7),
297
- datetime.date.today(),
298
- ],
299
- sma_50_above_sma_200=[
300
- datetime.date.today() - datetime.timedelta(days=5000),
301
- datetime.date.today() - datetime.timedelta(days=10),
302
- ],
303
- market_capitalization=[5e8, 1e12],
304
- order_by_desc="momentum",
305
- country=[
306
- "Germany",
307
- "United states",
308
- "France",
309
- "United kingdom",
310
- "Canada",
311
- "Japan",
312
- "Belgium",
313
- ],
314
- )
315
- MOMENTUM_GROWTH_RSI_40 = NamedFilterQuery(
316
- name="Momentum Growth Screener (RSI 40)",
317
- price_per_earning_ratio=[10, 500],
318
- rsi_bullish_crossover_40=[
319
- datetime.date.today() - datetime.timedelta(days=7),
320
- datetime.date.today(),
321
- ],
322
- macd_12_26_9_bullish_crossover=[
323
- datetime.date.today() - datetime.timedelta(days=7),
324
- datetime.date.today(),
325
- ],
326
- sma_50_above_sma_200=[
327
- datetime.date.today() - datetime.timedelta(days=5000),
328
- datetime.date.today() - datetime.timedelta(days=10),
329
- ],
330
- market_capitalization=[5e8, 1e12],
331
- order_by_desc="momentum",
332
- country=[
333
- "Germany",
334
- "United states",
335
- "France",
336
- "United kingdom",
337
- "Canada",
338
- "Japan",
339
- "Belgium",
340
- ],
341
- )
342
-
343
- GOLDEN_CROSS_LAST_SEVEN_DAYS = NamedFilterQuery(
344
- name="Golden cross in the last five days",
345
- price_per_earning_ratio=[10, 500],
346
- last_price=[1, 10000],
347
- golden_cross=[
348
- datetime.date.today() - datetime.timedelta(days=7),
349
- datetime.date.today(),
350
- ],
351
- order_by_desc="market_capitalization",
352
- country=[
353
- "Germany",
354
- "United states",
355
- "France",
356
- "United kingdom",
357
- "Canada",
358
- "Japan",
359
- "Belgium",
360
- ],
361
- )
362
-
363
-
364
- def predefined_filters() -> list[NamedFilterQuery]:
365
- return [
366
- STRONG_FUNDAMENTALS,
367
- GOOD_FUNDAMENTALS,
368
- RSI_CROSSOVER_30_GROWTH_STOCK_STRONG_FUNDAMENTAL,
369
- RSI_CROSSOVER_40_GROWTH_STOCK_STRONG_FUNDAMENTAL,
370
- RSI_CROSSOVER_30_GROWTH_STOCK,
371
- RSI_CROSSOVER_40_GROWTH_STOCK,
372
- MOMENTUM_GROWTH_GOOD_FUNDAMENTALS,
373
- MOMENTUM_GROWTH_STRONG_FUNDAMENTALS,
374
- MOMENTUM_GROWTH_RSI_30,
375
- MOMENTUM_GROWTH_RSI_40,
376
- GOLDEN_CROSS_LAST_SEVEN_DAYS,
377
- MEDIAN_YEARLY_GROWTH,
378
- ]
379
-
380
-
381
- class PredefinedFilters(BaseModel):
382
- filters: list[NamedFilterQuery] = Field(default_factory=predefined_filters)
383
-
384
- def get_predefined_filter_names(self) -> list[str]:
385
- return [filter.name for filter in self.filters]
386
-
387
- def get_predefined_filter(self, name: str) -> Dict[str, Any]:
388
- for filter in self.filters:
389
- if filter.name == name:
390
- return filter.to_dict()
391
- raise ValueError(f"Filter with name '{name}' not found.")
File without changes
File without changes