bullishpy 0.10.0__py3-none-any.whl → 0.12.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of bullishpy might be problematic. Click here for more details.

@@ -1,4 +1,7 @@
1
1
  import logging
2
+ import time
3
+ from itertools import batched
4
+ from pathlib import Path
2
5
  from typing import (
3
6
  Annotated,
4
7
  Any,
@@ -40,6 +43,7 @@ from bearish.types import TickerOnlySources # type: ignore
40
43
  from pydantic import BaseModel, BeforeValidator, Field, create_model
41
44
 
42
45
  from bullish.analysis.indicators import Indicators, IndicatorModels
46
+ from joblib import Parallel, delayed # type: ignore
43
47
 
44
48
  if TYPE_CHECKING:
45
49
  from bullish.database.crud import BullishDb
@@ -482,10 +486,27 @@ class Analysis(AnalysisView, BaseEquity, TechnicalAnalysis, FundamentalAnalysis)
482
486
  )
483
487
 
484
488
 
489
+ def compute_analysis(database_path: Path, ticker: Ticker) -> Analysis:
490
+ from bullish.database.crud import BullishDb
491
+
492
+ bullish_db = BullishDb(database_path=database_path)
493
+ return Analysis.from_ticker(bullish_db, ticker)
494
+
495
+
485
496
  def run_analysis(bullish_db: "BullishDb") -> None:
486
497
  price_trackers = set(bullish_db._read_tracker(TrackerQuery(), PriceTracker))
487
498
  finance_trackers = set(bullish_db._read_tracker(TrackerQuery(), FinancialsTracker))
488
499
  tickers = list(price_trackers.intersection(finance_trackers))
489
- for ticker in tickers:
490
- analysis = Analysis.from_ticker(bullish_db, ticker)
491
- bullish_db.write_analysis(analysis)
500
+ parallel = Parallel(n_jobs=-1)
501
+
502
+ for batch_ticker in batched(tickers, 1000):
503
+ start = time.perf_counter()
504
+ many_analysis = parallel(
505
+ delayed(compute_analysis)(bullish_db.database_path, ticker)
506
+ for ticker in batch_ticker
507
+ )
508
+ bullish_db.write_many_analysis(many_analysis)
509
+ elapsed_time = time.perf_counter() - start
510
+ print(
511
+ f"Computed analysis for {len(batch_ticker)} tickers in {elapsed_time:.2f} seconds."
512
+ )
@@ -70,15 +70,61 @@ SHOOTING_STARS = NamedFilterQuery(
70
70
  order_by_asc="last_price",
71
71
  )
72
72
 
73
- RSI_CROSSOVER = NamedFilterQuery(
73
+ RSI_CROSSOVER_TECH = NamedFilterQuery(
74
74
  name="RSI cross-over",
75
75
  cash_flow=["positive_free_cash_flow"],
76
76
  properties=["operating_cash_flow_is_higher_than_net_income"],
77
77
  return_after_rsi_crossover_45_period_90=[0.0, 100],
78
78
  rsi_bullish_crossover_45=DATE_THRESHOLD,
79
- market_capitalization=[1e9, 1e12], # 1 billion to 1 trillion
79
+ market_capitalization=[5e8, 1e11], # 1 billion to 1 trillion
80
+ order_by_desc="market_capitalization",
81
+ country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
82
+ industry=[
83
+ "Semiconductors",
84
+ "Software - Application",
85
+ "Software - Infrastructure",
86
+ "Biotechnology",
87
+ "Diagnostics & Research",
88
+ "Medical Devices",
89
+ "Health Information Services",
90
+ "Internet Retail",
91
+ "Electronic Gaming & Multimedia",
92
+ "Internet Content & Information",
93
+ "Solar",
94
+ "Information Technology Services",
95
+ "Scientific & Technical Instruments",
96
+ "Semiconductor Equipment & Materials",
97
+ "Diagnostics & Research",
98
+ ],
99
+ )
100
+ RSI_CROSSOVER_TECH_PE = NamedFilterQuery(
101
+ name="RSI cross-over P/E",
102
+ cash_flow=["positive_free_cash_flow"],
103
+ properties=["operating_cash_flow_is_higher_than_net_income"],
104
+ price_per_earning_ratio=[5, 30], # P/E ratio between 10 and 100
105
+ rsi_bullish_crossover_45=DATE_THRESHOLD,
106
+ market_capitalization=[5e8, 1e12], # 1 billion to 1 trillion
80
107
  order_by_desc="market_capitalization",
108
+ country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
109
+ industry=[
110
+ "Semiconductors",
111
+ "Software - Application",
112
+ "Software - Infrastructure",
113
+ "Biotechnology",
114
+ "Diagnostics & Research",
115
+ "Medical Devices",
116
+ "Health Information Services",
117
+ "Internet Retail",
118
+ "Electronic Gaming & Multimedia",
119
+ "Internet Content & Information",
120
+ "Solar",
121
+ "Information Technology Services",
122
+ "Scientific & Technical Instruments",
123
+ "Semiconductor Equipment & Materials",
124
+ "Diagnostics & Research",
125
+ ],
81
126
  )
127
+
82
128
  MICRO_CAP_EVENT_SPECULATION = NamedFilterQuery(
83
129
  name="Micro-Cap Event Speculation",
84
130
  description="seeks tiny names where unusual volume and price gaps hint at "
@@ -185,23 +231,94 @@ OVERSOLD_MEAN_REVERSION = NamedFilterQuery(
185
231
  mfi_oversold=DATE_THRESHOLD,
186
232
  lower_than_200_day_high=DATE_THRESHOLD,
187
233
  )
234
+ RSI_CROSSOVER_30_GROWTH_STOCK_STRONG_FUNDAMENTAL = NamedFilterQuery(
235
+ name="RSI cross-over 30 growth stock strong fundamental",
236
+ income=[
237
+ "positive_operating_income",
238
+ "growing_operating_income",
239
+ "positive_net_income",
240
+ "growing_net_income",
241
+ ],
242
+ cash_flow=["positive_free_cash_flow"],
243
+ properties=["operating_cash_flow_is_higher_than_net_income"],
244
+ price_per_earning_ratio=[20, 40],
245
+ rsi_bullish_crossover_30=DATE_THRESHOLD,
246
+ market_capitalization=[5e8, 1e12],
247
+ order_by_desc="market_capitalization",
248
+ country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
249
+ )
250
+ RSI_CROSSOVER_40_GROWTH_STOCK_STRONG_FUNDAMENTAL = NamedFilterQuery(
251
+ name="RSI cross-over 40 growth stock strong fundamental",
252
+ income=[
253
+ "positive_operating_income",
254
+ "growing_operating_income",
255
+ "positive_net_income",
256
+ "growing_net_income",
257
+ ],
258
+ cash_flow=["positive_free_cash_flow"],
259
+ properties=["operating_cash_flow_is_higher_than_net_income"],
260
+ price_per_earning_ratio=[20, 40],
261
+ rsi_bullish_crossover_40=DATE_THRESHOLD,
262
+ market_capitalization=[5e8, 1e12],
263
+ order_by_desc="market_capitalization",
264
+ country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
265
+ )
266
+ RSI_CROSSOVER_45_GROWTH_STOCK_STRONG_FUNDAMENTAL = NamedFilterQuery(
267
+ name="RSI cross-over 45 growth stock strong fundamental",
268
+ income=[
269
+ "positive_operating_income",
270
+ "growing_operating_income",
271
+ "positive_net_income",
272
+ "growing_net_income",
273
+ ],
274
+ cash_flow=["positive_free_cash_flow"],
275
+ properties=["operating_cash_flow_is_higher_than_net_income"],
276
+ price_per_earning_ratio=[20, 40],
277
+ rsi_bullish_crossover_45=DATE_THRESHOLD,
278
+ market_capitalization=[5e8, 1e12],
279
+ order_by_desc="market_capitalization",
280
+ country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
281
+ )
282
+ RSI_CROSSOVER_30_GROWTH_STOCK = NamedFilterQuery(
283
+ name="RSI cross-over 30 growth stock",
284
+ cash_flow=["positive_free_cash_flow"],
285
+ properties=["operating_cash_flow_is_higher_than_net_income"],
286
+ price_per_earning_ratio=[20, 40],
287
+ rsi_bullish_crossover_30=DATE_THRESHOLD,
288
+ market_capitalization=[5e8, 1e12],
289
+ order_by_desc="market_capitalization",
290
+ country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
291
+ )
292
+ RSI_CROSSOVER_40_GROWTH_STOCK = NamedFilterQuery(
293
+ name="RSI cross-over 40 growth stock",
294
+ cash_flow=["positive_free_cash_flow"],
295
+ properties=["operating_cash_flow_is_higher_than_net_income"],
296
+ price_per_earning_ratio=[20, 40],
297
+ rsi_bullish_crossover_40=DATE_THRESHOLD,
298
+ market_capitalization=[5e8, 1e12],
299
+ order_by_desc="market_capitalization",
300
+ country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
301
+ )
302
+ RSI_CROSSOVER_45_GROWTH_STOCK = NamedFilterQuery(
303
+ name="RSI cross-over 45 growth stock",
304
+ cash_flow=["positive_free_cash_flow"],
305
+ properties=["operating_cash_flow_is_higher_than_net_income"],
306
+ price_per_earning_ratio=[20, 40],
307
+ rsi_bullish_crossover_45=DATE_THRESHOLD,
308
+ market_capitalization=[5e8, 1e12],
309
+ order_by_desc="market_capitalization",
310
+ country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
311
+ )
188
312
 
189
313
 
190
314
  def predefined_filters() -> list[NamedFilterQuery]:
191
315
  return [
192
- STRONG_FUNDAMENTALS,
193
- GOOD_FUNDAMENTALS,
194
- MICRO_CAP_EVENT_SPECULATION,
195
- MOMENTUM_BREAKOUT_HUNTER,
196
- DEEP_VALUE_PLUS_CATALYST,
197
- END_OF_TREND_REVERSAL,
198
- HIGH_QUALITY_CASH_GENERATOR,
199
- EARNINGS_ACCELERATION_TREND_CONFIRMATION,
200
- DIVIDEND_GROWTH_COMPOUNDER,
201
- BREAK_OUT_MOMENTUM,
202
- OVERSOLD_MEAN_REVERSION,
203
- SHOOTING_STARS,
204
- RSI_CROSSOVER,
316
+ RSI_CROSSOVER_30_GROWTH_STOCK_STRONG_FUNDAMENTAL,
317
+ RSI_CROSSOVER_40_GROWTH_STOCK_STRONG_FUNDAMENTAL,
318
+ RSI_CROSSOVER_45_GROWTH_STOCK_STRONG_FUNDAMENTAL,
319
+ RSI_CROSSOVER_30_GROWTH_STOCK,
320
+ RSI_CROSSOVER_40_GROWTH_STOCK,
321
+ RSI_CROSSOVER_45_GROWTH_STOCK,
205
322
  ]
206
323
 
207
324
 
bullish/app/app.py CHANGED
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  import shelve
2
3
  import uuid
3
4
  from pathlib import Path
@@ -37,6 +38,7 @@ CACHE_SHELVE = "user_cache"
37
38
  DB_KEY = "db_path"
38
39
 
39
40
  st.set_page_config(layout="wide")
41
+ logger = logging.getLogger(__name__)
40
42
 
41
43
 
42
44
  @st.cache_resource
@@ -146,10 +148,11 @@ def build_filter(model: Type[BaseModel], data: Dict[str, Any]) -> Dict[str, Any]
146
148
  key=hash((model.__name__, field)),
147
149
  )
148
150
  elif info.annotation == Optional[str]: # type: ignore
151
+ options = ["", *groups_mapping()[field]]
149
152
  data[field] = st.selectbox(
150
153
  name,
151
- ["", *groups_mapping()[field]],
152
- index=0 if not default else groups_mapping()[field].index(default),
154
+ options,
155
+ index=0 if not default else options.index(default),
153
156
  key=hash((model.__name__, field)),
154
157
  )
155
158
 
@@ -162,11 +165,22 @@ def build_filter(model: Type[BaseModel], data: Dict[str, Any]) -> Dict[str, Any]
162
165
  (item.le for item in info.metadata if hasattr(item, "le")),
163
166
  info.default[1] if info.default and len(info.default) == 2 else None,
164
167
  )
165
- data[field] = list(
166
- st.slider( # type: ignore
167
- name, ge, le, tuple(default), key=hash((model.__name__, field))
168
+ if info.annotation == Optional[List[float]]: # type: ignore
169
+ ge = int(ge) # type: ignore
170
+ le = int(le) # type: ignore
171
+ default = [int(d) for d in default]
172
+ try:
173
+ data[field] = list(
174
+ st.slider( # type: ignore
175
+ name, ge, le, tuple(default), key=hash((model.__name__, field))
176
+ )
168
177
  )
169
- )
178
+ except Exception as e:
179
+ logger.error(
180
+ f"Error building filter for {model.__name__}.{field} "
181
+ f"with the parameters {(info.annotation, name, ge, le, tuple(default))}: {e}"
182
+ )
183
+ raise e
170
184
  return data
171
185
 
172
186
 
bullish/database/crud.py CHANGED
@@ -67,6 +67,16 @@ class BullishDb(BearishDb, BullishDbBase): # type: ignore
67
67
  session.exec(stmt) # type: ignore
68
68
  session.commit()
69
69
 
70
+ def _write_many_analysis(self, many_analysis: List[Analysis]) -> None:
71
+ with Session(self._engine) as session:
72
+ stmt = (
73
+ insert(AnalysisORM)
74
+ .prefix_with("OR REPLACE")
75
+ .values([a.model_dump() for a in many_analysis])
76
+ )
77
+ session.exec(stmt) # type: ignore
78
+ session.commit()
79
+
70
80
  def _read_analysis(self, ticker: Ticker) -> Optional[Analysis]:
71
81
  with Session(self._engine) as session:
72
82
  query = select(AnalysisORM).where(AnalysisORM.symbol == ticker.symbol)
@@ -19,6 +19,9 @@ class BullishDbBase(BearishDbBase): # type: ignore
19
19
  def write_analysis(self, analysis: "Analysis") -> None:
20
20
  return self._write_analysis(analysis)
21
21
 
22
+ def write_many_analysis(self, many_analysis: List["Analysis"]) -> None:
23
+ return self._write_many_analysis(many_analysis)
24
+
22
25
  def read_analysis(self, ticker: Ticker) -> Optional["Analysis"]:
23
26
  return self._read_analysis(ticker)
24
27
 
@@ -62,6 +65,9 @@ class BullishDbBase(BearishDbBase): # type: ignore
62
65
  @abc.abstractmethod
63
66
  def _write_analysis(self, analysis: "Analysis") -> None: ...
64
67
 
68
+ @abc.abstractmethod
69
+ def _write_many_analysis(self, many_analysis: List["Analysis"]) -> None: ...
70
+
65
71
  @abc.abstractmethod
66
72
  def _read_analysis(self, ticker: Ticker) -> Optional["Analysis"]: ...
67
73
 
@@ -1,15 +1,16 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: bullishpy
3
- Version: 0.10.0
3
+ Version: 0.12.0
4
4
  Summary:
5
5
  Author: aan
6
6
  Author-email: andoludovic.andriamamonjy@gmail.com
7
7
  Requires-Python: >=3.12,<3.13
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3.12
10
- Requires-Dist: bearishpy (>=0.20.0,<0.21.0)
10
+ Requires-Dist: bearishpy (>=0.22.0,<0.23.0)
11
11
  Requires-Dist: click (>=7.0,<=8.1)
12
12
  Requires-Dist: huey (>=2.5.3,<3.0.0)
13
+ Requires-Dist: joblib (>=1.5.1,<2.0.0)
13
14
  Requires-Dist: pandas-ta (>=0.3.14b0,<0.4.0)
14
15
  Requires-Dist: plotly (>=6.1.2,<7.0.0)
15
16
  Requires-Dist: streamlit (>=1.45.1,<2.0.0)
@@ -1,12 +1,12 @@
1
1
  bullish/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  bullish/analysis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- bullish/analysis/analysis.py,sha256=9ugJp2fGuA6xFUlGIJVIHWZ8d8E6TdbQHWjg5zWPIwY,18445
3
+ bullish/analysis/analysis.py,sha256=v5MFGhQUpMwGAVE0dg2293ldRSgQmqy5CiF8MQFTibM,19165
4
4
  bullish/analysis/filter.py,sha256=S8TuxoTAUY0U8ARPjNHE0tSSE_ToWkfZazAgnfgswk4,18136
5
5
  bullish/analysis/functions.py,sha256=KKz_0C7maQmcGu2tGwZvioxzmh-JcB-YNpPQGjyyheA,13825
6
6
  bullish/analysis/indicators.py,sha256=hZgzTq-80XPP6x7dXGhxd-Zzgra-6D-g3pVxUBYOW44,20167
7
- bullish/analysis/predefined_filters.py,sha256=oEmlMW0jvBtHeOjbc7tmTEN1M8ymhRchLC6jqmULwCw,7524
7
+ bullish/analysis/predefined_filters.py,sha256=3O244FCZkbnTSaReh6w6cwEIXdLzQZmbkBS0uRW-Y0M,12391
8
8
  bullish/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- bullish/app/app.py,sha256=LA-NpNkAz1jsaKVJaQnbf7yQ4IjhA575ZagTGJsDuZY,12733
9
+ bullish/app/app.py,sha256=E0H78LOODl1H6s308jXpQGTUoFPoLOJkPBXOLQGLCeA,13331
10
10
  bullish/cli.py,sha256=uYLZmGDAolZKWzduZ58bP-xul1adg0oKfeUQtZMXTvA,1958
11
11
  bullish/database/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  bullish/database/alembic/README,sha256=heMzebYwlGhnE8_4CWJ4LS74WoEZjBy-S-mIJRxAEKI,39
@@ -23,7 +23,7 @@ bullish/database/alembic/versions/73564b60fe24_.py,sha256=MTlDRDNHj3E9gK7IMeAzv2
23
23
  bullish/database/alembic/versions/d663166c531d_.py,sha256=U92l6QXqPniAYrPeu2Bt77ReDbXveLj4aGXtgd806JY,1915
24
24
  bullish/database/alembic/versions/ee5baabb35f8_.py,sha256=nBMEY-_C8AsSXVPyaDdUkwrFFo2gxShzJhmrjejDwtc,1632
25
25
  bullish/database/alembic/versions/fc191121f522_.py,sha256=0sstF6TpAJ09-Mt-Vek9SdSWksvi4C58a5D92rBtuY8,1894
26
- bullish/database/crud.py,sha256=6-Fb1AjGZqsrmwwl2Qay_leqQ9_-RAIjZ8D0efe8nKA,7022
26
+ bullish/database/crud.py,sha256=0AMrs07MMeDq5AVft_pf43KWPFOusMNzUmxuXwMX6K4,7400
27
27
  bullish/database/schemas.py,sha256=bU-DW49NqpBp--1VN486LUdDmLeScrI8TF69afzjoTc,1507
28
28
  bullish/database/scripts/create_revision.py,sha256=rggIf-3koPqJNth8FIg89EOfnIM7a9QrvL8X7UJsP0g,628
29
29
  bullish/database/scripts/stamp.py,sha256=PWgVUEBumjNUMjTnGw46qmU3p221LeN-KspnW_gFuu4,839
@@ -33,14 +33,14 @@ bullish/exceptions.py,sha256=4z_i-dD-CDz1bkGmZH9DOf1L_awlCPCgdUDPF7dhWAI,106
33
33
  bullish/figures/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
34
  bullish/figures/figures.py,sha256=SWTTiEoVyWMZeIIxg0ERi23v7s4tySB5BLKyPu12jC4,4193
35
35
  bullish/interface/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
- bullish/interface/interface.py,sha256=-3V4M1J2VVn0ugwbaCKzMuBYRHH7eFmz9gij0hUTNUw,3013
36
+ bullish/interface/interface.py,sha256=6HXSK-N-vOuOGVserizknP4CeUGbUhB8P642oZmgpk0,3252
37
37
  bullish/jobs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
38
  bullish/jobs/app.py,sha256=5MJ5KXUo7JSNAvOPgkpIMasD11VTrjQvGzM7vmCY65E,77
39
39
  bullish/jobs/models.py,sha256=ndrGTMP08S57yGLGEG9TQt8Uw2slc4HvbG-TZtEEuN0,744
40
40
  bullish/jobs/tasks.py,sha256=V_b0c8_GQC0-KIxaHDlLFhtkclQJOsck0gXaW6OlC_w,3055
41
41
  bullish/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
42
  bullish/utils/checks.py,sha256=Va10_xDVVnxYkOD2hafvyQ-TFV8FQpOkr4huJ7XgpDM,2188
43
- bullishpy-0.10.0.dist-info/METADATA,sha256=pAoW7ZWqHdzTZygnklF37z9CW9dxHVfqDNCKIube1fU,745
44
- bullishpy-0.10.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
45
- bullishpy-0.10.0.dist-info/entry_points.txt,sha256=eaPpmL6vmSBFo0FBtwibCXGqAW4LFJ83whJzT1VjD-0,43
46
- bullishpy-0.10.0.dist-info/RECORD,,
43
+ bullishpy-0.12.0.dist-info/METADATA,sha256=hmEQ5ZWQ8ROXE5_Vm0l3npGSM6BfuBQD1-glStH16us,784
44
+ bullishpy-0.12.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
45
+ bullishpy-0.12.0.dist-info/entry_points.txt,sha256=eaPpmL6vmSBFo0FBtwibCXGqAW4LFJ83whJzT1VjD-0,43
46
+ bullishpy-0.12.0.dist-info/RECORD,,