bullishpy 0.4.0__tar.gz → 0.5.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 (35) hide show
  1. {bullishpy-0.4.0 → bullishpy-0.5.0}/PKG-INFO +2 -2
  2. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/analysis/analysis.py +22 -19
  3. bullishpy-0.5.0/bullish/analysis/filter.py +583 -0
  4. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/app/app.py +123 -53
  5. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/cli.py +3 -1
  6. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/database/alembic/versions/037dbd721317_.py +1 -0
  7. bullishpy-0.5.0/bullish/database/alembic/versions/11d35a452b40_.py +368 -0
  8. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/database/alembic/versions/4b0a2f40b7d3_.py +1 -0
  9. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/database/alembic/versions/73564b60fe24_.py +1 -0
  10. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/database/crud.py +9 -2
  11. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/interface/interface.py +13 -27
  12. {bullishpy-0.4.0 → bullishpy-0.5.0}/pyproject.toml +2 -2
  13. bullishpy-0.4.0/bullish/analysis/filter.py +0 -123
  14. {bullishpy-0.4.0 → bullishpy-0.5.0}/README.md +0 -0
  15. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/__init__.py +0 -0
  16. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/analysis/__init__.py +0 -0
  17. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/app/__init__.py +0 -0
  18. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/database/__init__.py +0 -0
  19. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/database/alembic/README +0 -0
  20. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/database/alembic/alembic.ini +0 -0
  21. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/database/alembic/env.py +0 -0
  22. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/database/alembic/script.py.mako +0 -0
  23. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/database/schemas.py +0 -0
  24. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/database/scripts/create_revision.py +0 -0
  25. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/database/scripts/stamp.py +0 -0
  26. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/database/scripts/upgrade.py +0 -0
  27. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/database/settings.py +0 -0
  28. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/exceptions.py +0 -0
  29. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/figures/__init__.py +0 -0
  30. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/figures/figures.py +0 -0
  31. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/interface/__init__.py +0 -0
  32. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/jobs/__init__.py +0 -0
  33. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/jobs/app.py +0 -0
  34. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/jobs/models.py +0 -0
  35. {bullishpy-0.4.0 → bullishpy-0.5.0}/bullish/jobs/tasks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: bullishpy
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary:
5
5
  Author: aan
6
6
  Author-email: andoludovic.andriamamonjy@gmail.com
@@ -9,7 +9,7 @@ Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3.10
10
10
  Classifier: Programming Language :: Python :: 3.11
11
11
  Classifier: Programming Language :: Python :: 3.12
12
- Requires-Dist: bearishpy (>=0.19.0,<0.20.0)
12
+ Requires-Dist: bearishpy (>=0.20.0,<0.21.0)
13
13
  Requires-Dist: huey (>=2.5.3,<3.0.0)
14
14
  Requires-Dist: pandas-ta (>=0.3.14b0,<0.4.0)
15
15
  Requires-Dist: plotly (>=6.1.2,<7.0.0)
@@ -153,7 +153,9 @@ def _abs(data: pd.Series) -> pd.Series: # type: ignore
153
153
 
154
154
 
155
155
  class TechnicalAnalysis(BaseModel):
156
- rsi_last_value: Optional[float] = None
156
+ rsi_last_value: Optional[float] = Field(
157
+ None, alias="RSI Last value", description="RSI last value", ge=0, le=100
158
+ )
157
159
  macd_12_26_9_buy_date: Optional[date] = None
158
160
  ma_50_200_buy_date: Optional[date] = None
159
161
  slope_7: Optional[float] = None
@@ -332,26 +334,28 @@ class TechnicalAnalysis(BaseModel):
332
334
 
333
335
 
334
336
  class BaseFundamentalAnalysis(BaseModel):
335
- positive_free_cash_flow: Optional[float] = None
336
- growing_operating_cash_flow: Optional[float] = None
337
- operating_cash_flow_is_higher_than_net_income: Optional[float] = None
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
+
338
352
  mean_capex_ratio: Optional[float] = None
339
353
  max_capex_ratio: Optional[float] = None
340
354
  min_capex_ratio: Optional[float] = None
341
355
  mean_dividend_payout_ratio: Optional[float] = None
342
356
  max_dividend_payout_ratio: Optional[float] = None
343
357
  min_dividend_payout_ratio: Optional[float] = None
344
- positive_net_income: Optional[float] = None
345
- positive_operating_income: Optional[float] = None
346
- growing_net_income: Optional[float] = None
347
- growing_operating_income: Optional[float] = None
348
- positive_diluted_eps: Optional[float] = None
349
- positive_basic_eps: Optional[float] = None
350
- growing_basic_eps: Optional[float] = None
351
- growing_diluted_eps: Optional[float] = None
352
- positive_debt_to_equity: Optional[float] = None
353
- positive_return_on_assets: Optional[float] = None
354
- positive_return_on_equity: Optional[float] = None
358
+
355
359
  earning_per_share: Optional[float] = None
356
360
 
357
361
  def is_empty(self) -> bool:
@@ -481,13 +485,12 @@ class BaseFundamentalAnalysis(BaseModel):
481
485
  return cls()
482
486
 
483
487
 
484
- class YearlyFundamentalAnalysis(BaseFundamentalAnalysis):
485
- ...
488
+ class YearlyFundamentalAnalysis(BaseFundamentalAnalysis): ...
486
489
 
487
490
 
488
491
  fields_with_prefix = {
489
- f"{QUARTERLY}_{name}": (Optional[float], Field(default=None))
490
- for name in BaseFundamentalAnalysis.model_fields
492
+ f"{QUARTERLY}_{name}": (field_info.annotation, Field(default=None))
493
+ for name, field_info in BaseFundamentalAnalysis.model_fields.items()
491
494
  }
492
495
 
493
496
  # Create the new model
@@ -0,0 +1,583 @@
1
+ import datetime
2
+ from datetime import date
3
+ from typing import Literal, get_args, Any, Optional, List, Tuple, Type
4
+
5
+ from bearish.types import SeriesLength # type: ignore
6
+ from pydantic import BaseModel, Field, ConfigDict
7
+ from pydantic import create_model
8
+ from pydantic.fields import FieldInfo
9
+
10
+ from bullish.analysis.analysis import (
11
+ TechnicalAnalysis,
12
+ YearlyFundamentalAnalysis,
13
+ QuarterlyFundamentalAnalysis,
14
+ )
15
+
16
+ Industry = Literal[
17
+ "Publishing",
18
+ "Internet Retail",
19
+ "Scientific & Technical Instruments",
20
+ "Engineering & Construction",
21
+ "Diagnostics & Research",
22
+ "Software - Infrastructure",
23
+ "Thermal Coal",
24
+ "Software - Application",
25
+ "Auto Manufacturers",
26
+ "Farm Products",
27
+ "Medical Devices",
28
+ "Education & Training Services",
29
+ "Auto Parts",
30
+ "Specialty Chemicals",
31
+ "Marine Shipping",
32
+ "Biotechnology",
33
+ "Real Estate Services",
34
+ "Gold",
35
+ "Entertainment",
36
+ "Specialty Retail",
37
+ "Utilities - Independent Power Producers",
38
+ "Steel",
39
+ "Mortgage Finance",
40
+ "Communication Equipment",
41
+ "Drug Manufacturers - Specialty & Generic",
42
+ "Electronic Gaming & Multimedia",
43
+ "Banks - Regional",
44
+ "Oil & Gas E&P",
45
+ "Travel Services",
46
+ "Real Estate - Diversified",
47
+ "Telecom Services",
48
+ "Uranium",
49
+ "Consulting Services",
50
+ "Waste Management",
51
+ "Agricultural Inputs",
52
+ "Utilities - Diversified",
53
+ "Auto & Truck Dealerships",
54
+ "Confectioners",
55
+ "Other Industrial Metals & Mining",
56
+ "Beverages - Wineries & Distilleries",
57
+ "Oil & Gas Midstream",
58
+ "Recreational Vehicles",
59
+ "Electrical Equipment & Parts",
60
+ "Household & Personal Products",
61
+ "Packaging & Containers",
62
+ "REIT - Specialty",
63
+ "Home Improvement Retail",
64
+ "Electronic Components",
65
+ "Asset Management",
66
+ "Consumer Electronics",
67
+ "Conglomerates",
68
+ "Health Information Services",
69
+ "Medical Instruments & Supplies",
70
+ "Building Products & Equipment",
71
+ "Information Technology Services",
72
+ "Specialty Industrial Machinery",
73
+ "Food Distribution",
74
+ "Packaged Foods",
75
+ "Rental & Leasing Services",
76
+ "Medical Distribution",
77
+ "Grocery Stores",
78
+ "Advertising Agencies",
79
+ "Beverages - Non - Alcoholic",
80
+ "Apparel Manufacturing",
81
+ "Oil & Gas Equipment & Services",
82
+ "Coking Coal",
83
+ "Industrial Distribution",
84
+ "Restaurants",
85
+ "Beverages - Brewers",
86
+ "Chemicals",
87
+ "Real Estate - Development",
88
+ "Credit Services",
89
+ "Tobacco",
90
+ "Metal Fabrication",
91
+ "Building Materials",
92
+ "Residential Construction",
93
+ "Specialty Business Services",
94
+ "REIT - Hotel & Motel",
95
+ "Internet Content & Information",
96
+ "Lodging",
97
+ "Furnishings, Fixtures & Appliances",
98
+ "Airlines",
99
+ "Computer Hardware",
100
+ "Integrated Freight & Logistics",
101
+ "Solar",
102
+ "Capital Markets",
103
+ "Leisure",
104
+ "Airports & Air Services",
105
+ "Aluminum",
106
+ "Insurance Brokers",
107
+ "Semiconductors",
108
+ "REIT - Retail",
109
+ "Luxury Goods",
110
+ "Lumber & Wood Production",
111
+ "REIT - Mortgage",
112
+ "Semiconductor Equipment & Materials",
113
+ "Aerospace & Defense",
114
+ "Security & Protection Services",
115
+ "Utilities - Renewable",
116
+ "Utilities - Regulated Gas",
117
+ "Apparel Retail",
118
+ "Pollution & Treatment Controls",
119
+ "Broadcasting",
120
+ "Resorts & Casinos",
121
+ "Other Precious Metals & Mining",
122
+ "Financial Data & Stock Exchanges",
123
+ "Footwear & Accessories",
124
+ "Medical Care Facilities",
125
+ "Electronics & Computer Distribution",
126
+ "Gambling",
127
+ "Tools & Accessories",
128
+ "Insurance - Property & Casualty",
129
+ "Utilities - Regulated Water",
130
+ "Insurance - Specialty",
131
+ "Personal Services",
132
+ "Pharmaceutical Retailers",
133
+ "Farm & Heavy Construction Machinery",
134
+ "Utilities - Regulated Electric",
135
+ "Department Stores",
136
+ "Staffing & Employment Services",
137
+ "Textile Manufacturing",
138
+ "Silver",
139
+ "REIT - Industrial",
140
+ "REIT - Diversified",
141
+ "Copper",
142
+ "Business Equipment & Supplies",
143
+ "Infrastructure Operations",
144
+ "Trucking",
145
+ "Insurance - Reinsurance",
146
+ "Insurance - Diversified",
147
+ "Drug Manufacturers - General",
148
+ "Oil & Gas Drilling",
149
+ "Banks - Diversified",
150
+ "REIT - Residential",
151
+ "Oil & Gas Refining & Marketing",
152
+ "Shell Companies",
153
+ "Financial Conglomerates",
154
+ "Paper & Paper Products",
155
+ "Insurance - Life",
156
+ "REIT - Office",
157
+ "Railroads",
158
+ "Oil & Gas Integrated",
159
+ "Healthcare Plans",
160
+ "REIT - Healthcare Facilities",
161
+ "Discount Stores",
162
+ ]
163
+
164
+ IndustryGroup = Literal[
165
+ "publishing",
166
+ "internet-retail",
167
+ "scientific-technical-instruments",
168
+ "engineering-construction",
169
+ "diagnostics-research",
170
+ "software-infrastructure",
171
+ "thermal-coal",
172
+ "software-application",
173
+ "auto-manufacturers",
174
+ "farm-products",
175
+ "medical-devices",
176
+ "education-training-services",
177
+ "auto-parts",
178
+ "specialty-chemicals",
179
+ "marine-shipping",
180
+ "biotechnology",
181
+ "real-estate-services",
182
+ "gold",
183
+ "entertainment",
184
+ "specialty-retail",
185
+ "utilities-independent-power-producers",
186
+ "steel",
187
+ "mortgage-finance",
188
+ "communication-equipment",
189
+ "drug-manufacturers-specialty-generic",
190
+ "electronic-gaming-multimedia",
191
+ "banks-regional",
192
+ "oil-gas-e-p",
193
+ "travel-services",
194
+ "real-estate-diversified",
195
+ "telecom-services",
196
+ "uranium",
197
+ "consulting-services",
198
+ "waste-management",
199
+ "agricultural-inputs",
200
+ "utilities-diversified",
201
+ "auto-truck-dealerships",
202
+ "confectioners",
203
+ "other-industrial-metals-mining",
204
+ "beverages-wineries-distilleries",
205
+ "oil-gas-midstream",
206
+ "recreational-vehicles",
207
+ "electrical-equipment-parts",
208
+ "household-personal-products",
209
+ "packaging-containers",
210
+ "reit-specialty",
211
+ "home-improvement-retail",
212
+ "electronic-components",
213
+ "asset-management",
214
+ "consumer-electronics",
215
+ "conglomerates",
216
+ "health-information-services",
217
+ "medical-instruments-supplies",
218
+ "building-products-equipment",
219
+ "information-technology-services",
220
+ "specialty-industrial-machinery",
221
+ "food-distribution",
222
+ "packaged-foods",
223
+ "rental-leasing-services",
224
+ "medical-distribution",
225
+ "grocery-stores",
226
+ "advertising-agencies",
227
+ "beverages-non-alcoholic",
228
+ "apparel-manufacturing",
229
+ "oil-gas-equipment-services",
230
+ "coking-coal",
231
+ "industrial-distribution",
232
+ "restaurants",
233
+ "beverages-brewers",
234
+ "chemicals",
235
+ "real-estate-development",
236
+ "credit-services",
237
+ "tobacco",
238
+ "metal-fabrication",
239
+ "building-materials",
240
+ "residential-construction",
241
+ "specialty-business-services",
242
+ "reit-hotel-motel",
243
+ "internet-content-information",
244
+ "lodging",
245
+ "furnishings-fixtures-appliances",
246
+ "airlines",
247
+ "computer-hardware",
248
+ "integrated-freight-logistics",
249
+ "solar",
250
+ "capital-markets",
251
+ "leisure",
252
+ "airports-air-services",
253
+ "aluminum",
254
+ "insurance-brokers",
255
+ "semiconductors",
256
+ "reit-retail",
257
+ "luxury-goods",
258
+ "lumber-wood-production",
259
+ "reit-mortgage",
260
+ "semiconductor-equipment-materials",
261
+ "aerospace-defense",
262
+ "security-protection-services",
263
+ "utilities-renewable",
264
+ "utilities-regulated-gas",
265
+ "apparel-retail",
266
+ "pollution-treatment-controls",
267
+ "broadcasting",
268
+ "resorts-casinos",
269
+ "other-precious-metals-mining",
270
+ "financial-data-stock-exchanges",
271
+ "footwear-accessories",
272
+ "medical-care-facilities",
273
+ "electronics-computer-distribution",
274
+ "gambling",
275
+ "tools-accessories",
276
+ "insurance-property-casualty",
277
+ "utilities-regulated-water",
278
+ "insurance-specialty",
279
+ "personal-services",
280
+ "pharmaceutical-retailers",
281
+ "farm-heavy-construction-machinery",
282
+ "utilities-regulated-electric",
283
+ "department-stores",
284
+ "staffing-employment-services",
285
+ "textile-manufacturing",
286
+ "silver",
287
+ "reit-industrial",
288
+ "reit-diversified",
289
+ "copper",
290
+ "business-equipment-supplies",
291
+ "infrastructure-operations",
292
+ "trucking",
293
+ "insurance-reinsurance",
294
+ "insurance-diversified",
295
+ "drug-manufacturers-general",
296
+ "oil-gas-drilling",
297
+ "banks-diversified",
298
+ "reit-residential",
299
+ "oil-gas-refining-marketing",
300
+ "shell-companies",
301
+ "financial-conglomerates",
302
+ "paper-paper-products",
303
+ "insurance-life",
304
+ "reit-office",
305
+ "railroads",
306
+ "oil-gas-integrated",
307
+ "healthcare-plans",
308
+ "reit-healthcare-facilities",
309
+ "discount-stores",
310
+ ]
311
+
312
+ Sector = Literal[
313
+ "Communication Services",
314
+ "Consumer Cyclical",
315
+ "Technology",
316
+ "Industrials",
317
+ "Healthcare",
318
+ "Energy",
319
+ "Consumer Defensive",
320
+ "Basic Materials",
321
+ "Real Estate",
322
+ "Utilities",
323
+ "Financial Services",
324
+ "Conglomerates",
325
+ ]
326
+
327
+ Country = Literal[
328
+ "Australia",
329
+ "China",
330
+ "Japan",
331
+ "United kingdom",
332
+ "United states",
333
+ "Poland",
334
+ "Switzerland",
335
+ "Canada",
336
+ "Greece",
337
+ "Spain",
338
+ "Germany",
339
+ "Indonesia",
340
+ "Belgium",
341
+ "France",
342
+ "Netherlands",
343
+ "British virgin islands",
344
+ "Italy",
345
+ "Hungary",
346
+ "Austria",
347
+ "Finland",
348
+ "Sweden",
349
+ "Bermuda",
350
+ "Taiwan",
351
+ "Israel",
352
+ "Ukraine",
353
+ "Singapore",
354
+ "Jersey",
355
+ "Ireland",
356
+ "Luxembourg",
357
+ "Cyprus",
358
+ "Cayman islands",
359
+ "Norway",
360
+ "Denmark",
361
+ "Hong kong",
362
+ "New zealand",
363
+ "Kazakhstan",
364
+ "Nigeria",
365
+ "Argentina",
366
+ "Brazil",
367
+ "Czech republic",
368
+ "Mauritius",
369
+ "South africa",
370
+ "India",
371
+ "Mexico",
372
+ "Mongolia",
373
+ "Slovenia",
374
+ "Thailand",
375
+ "Malaysia",
376
+ "Costa rica",
377
+ "Isle of man",
378
+ "Egypt",
379
+ "Turkey",
380
+ "United arab emirates",
381
+ "Colombia",
382
+ "Gibraltar",
383
+ "Malta",
384
+ "Liechtenstein",
385
+ "Guernsey",
386
+ "Peru",
387
+ "Estonia",
388
+ "French guiana",
389
+ "Portugal",
390
+ "Uruguay",
391
+ "Chile",
392
+ "Martinique",
393
+ "Monaco",
394
+ "Panama",
395
+ "Papua new guinea",
396
+ "South korea",
397
+ "Macau",
398
+ "Gabon",
399
+ "Romania",
400
+ "Senegal",
401
+ "Morocco",
402
+ "Jordan",
403
+ "Lithuania",
404
+ "Dominican republic",
405
+ "Reunion",
406
+ "Zambia",
407
+ "Cambodia",
408
+ "Myanmar",
409
+ "Bahamas",
410
+ "Philippines",
411
+ "Bangladesh",
412
+ "Latvia",
413
+ "Vietnam",
414
+ "Iceland",
415
+ "Azerbaijan",
416
+ "Georgia",
417
+ "Liberia",
418
+ "Kenya",
419
+ ]
420
+ SIZE_RANGE = 2
421
+
422
+
423
+ def _get_type(name: str, info: FieldInfo) -> Tuple[Any, Any]:
424
+ alias = info.alias or " ".join(name.capitalize().split("_")).strip()
425
+ if info.annotation == Optional[float]: # type: ignore
426
+ ge = next((item.ge for item in info.metadata if hasattr(item, "ge")), 0)
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))
429
+ elif info.annotation == Optional[date]: # type: ignore
430
+ 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))
433
+ else:
434
+ raise NotImplementedError
435
+
436
+
437
+ FUNDAMENTAL_ANALYSIS_GROUP = ["income", "cash_flow", "eps"]
438
+
439
+
440
+ def _get_fundamental_analysis_boolean_fields() -> List[str]:
441
+ return [
442
+ name
443
+ for name, info in {
444
+ **YearlyFundamentalAnalysis.model_fields,
445
+ **QuarterlyFundamentalAnalysis.model_fields,
446
+ }.items()
447
+ if info.annotation == Optional[bool]
448
+ ]
449
+
450
+
451
+ def get_boolean_field_group(group: str) -> List[str]:
452
+ groups = FUNDAMENTAL_ANALYSIS_GROUP.copy()
453
+ groups.remove(group)
454
+ return [
455
+ name
456
+ for name in _get_fundamental_analysis_boolean_fields()
457
+ if group in name and not any(g in name for g in groups)
458
+ ]
459
+
460
+
461
+ INCOME_GROUP = get_boolean_field_group("income")
462
+ CASH_FLOW_GROUP = get_boolean_field_group("cash_flow")
463
+ EPS_GROUP = get_boolean_field_group("eps")
464
+ PROPERTIES_GROUP = list(
465
+ set(_get_fundamental_analysis_boolean_fields()).difference(
466
+ {*INCOME_GROUP, *CASH_FLOW_GROUP, *EPS_GROUP}
467
+ )
468
+ )
469
+
470
+ GROUP_MAPPING = {
471
+ "income": INCOME_GROUP,
472
+ "cash_flow": CASH_FLOW_GROUP,
473
+ "eps": EPS_GROUP,
474
+ "properties": PROPERTIES_GROUP,
475
+ "country": get_args(Country),
476
+ "industry": get_args(Industry),
477
+ "industry_group": get_args(IndustryGroup),
478
+ "sector": get_args(Sector),
479
+ }
480
+
481
+
482
+ def _create_fundamental_analysis_model() -> Type[BaseModel]:
483
+ 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)),
488
+ }
489
+ remaining_fields = {
490
+ 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]
496
+ }
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
+ **{
508
+ name: _get_type(name, info)
509
+ for name, info in TechnicalAnalysis.model_fields.items()
510
+ },
511
+ )
512
+ FundamentalAnalysisFilter = _create_fundamental_analysis_model()
513
+
514
+
515
+ class GeneralFilter(BaseModel):
516
+ country: Optional[List[str]] = None
517
+ industry: Optional[List[str]] = None
518
+ industry_group: Optional[List[str]] = None
519
+ sector: Optional[List[str]] = None
520
+ market_capitalization: Optional[List[float]] = Field(
521
+ default_factory=lambda: [5e8, 1e12]
522
+ )
523
+
524
+
525
+ class FilterQuery(GeneralFilter, TechnicalAnalysisFilter, FundamentalAnalysisFilter): # type: ignore
526
+
527
+ def valid(self) -> bool:
528
+ return bool(self.model_dump(exclude_defaults=True, exclude_unset=True))
529
+
530
+ def to_query(self) -> str:
531
+ parameters = self.model_dump(exclude_defaults=True, exclude_unset=True)
532
+ query = []
533
+ for parameter, value in parameters.items():
534
+ if not value:
535
+ continue
536
+
537
+ if (
538
+ isinstance(value, list)
539
+ and all(isinstance(item, str) for item in value)
540
+ and parameter not in GeneralFilter.model_fields
541
+ ):
542
+ query.append(" AND ".join([f"{v}=1" for v in value]))
543
+ elif (
544
+ isinstance(value, list)
545
+ and len(value) == SIZE_RANGE
546
+ and all(isinstance(item, (int, float)) for item in value)
547
+ ):
548
+ query.append(f"{parameter} BETWEEN {value[0]} AND {value[1]}")
549
+ elif (
550
+ isinstance(value, list)
551
+ and len(value) == SIZE_RANGE
552
+ and all(isinstance(item, date) for item in value)
553
+ ):
554
+ query.append(f"{parameter} BETWEEN '{value[0]}' AND '{value[1]}'")
555
+ elif (
556
+ isinstance(value, list)
557
+ and all(isinstance(item, str) for item in value)
558
+ and parameter in GeneralFilter.model_fields
559
+ ):
560
+ general_filters = [f"'{v}'" for v in value]
561
+ query.append(f"{parameter} IN ({', '.join(general_filters)})")
562
+ else:
563
+ raise NotImplementedError
564
+ query_ = " AND ".join(query)
565
+ return query_
566
+
567
+
568
+ class FilterQueryStored(FilterQuery): ...
569
+
570
+
571
+ class FilterUpdate(BaseModel):
572
+ window_size: SeriesLength = Field("5d")
573
+ data_age_in_days: int = 1
574
+ update_financials: bool = False
575
+ update_analysis_only: bool = False
576
+
577
+
578
+ class FilteredResults(BaseModel):
579
+ name: str
580
+ filter_query: FilterQueryStored
581
+ symbols: list[str] = Field(
582
+ default_factory=list, description="List of filtered tickers."
583
+ )