bullishpy 0.10.0__tar.gz → 0.12.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 (45) hide show
  1. {bullishpy-0.10.0 → bullishpy-0.12.0}/PKG-INFO +3 -2
  2. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/analysis/analysis.py +24 -3
  3. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/analysis/predefined_filters.py +132 -15
  4. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/app/app.py +20 -6
  5. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/database/crud.py +10 -0
  6. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/interface/interface.py +6 -0
  7. {bullishpy-0.10.0 → bullishpy-0.12.0}/pyproject.toml +3 -2
  8. {bullishpy-0.10.0 → bullishpy-0.12.0}/README.md +0 -0
  9. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/__init__.py +0 -0
  10. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/analysis/__init__.py +0 -0
  11. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/analysis/filter.py +0 -0
  12. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/analysis/functions.py +0 -0
  13. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/analysis/indicators.py +0 -0
  14. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/app/__init__.py +0 -0
  15. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/cli.py +0 -0
  16. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/database/__init__.py +0 -0
  17. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/database/alembic/README +0 -0
  18. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/database/alembic/alembic.ini +0 -0
  19. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/database/alembic/env.py +0 -0
  20. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/database/alembic/script.py.mako +0 -0
  21. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/database/alembic/versions/037dbd721317_.py +0 -0
  22. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/database/alembic/versions/08ac1116e055_.py +0 -0
  23. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/database/alembic/versions/11d35a452b40_.py +0 -0
  24. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/database/alembic/versions/17e51420e7ad_.py +0 -0
  25. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/database/alembic/versions/49c83f9eb5ac_.py +0 -0
  26. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/database/alembic/versions/4b0a2f40b7d3_.py +0 -0
  27. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/database/alembic/versions/73564b60fe24_.py +0 -0
  28. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/database/alembic/versions/d663166c531d_.py +0 -0
  29. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/database/alembic/versions/ee5baabb35f8_.py +0 -0
  30. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/database/alembic/versions/fc191121f522_.py +0 -0
  31. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/database/schemas.py +0 -0
  32. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/database/scripts/create_revision.py +0 -0
  33. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/database/scripts/stamp.py +0 -0
  34. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/database/scripts/upgrade.py +0 -0
  35. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/database/settings.py +0 -0
  36. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/exceptions.py +0 -0
  37. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/figures/__init__.py +0 -0
  38. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/figures/figures.py +0 -0
  39. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/interface/__init__.py +0 -0
  40. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/jobs/__init__.py +0 -0
  41. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/jobs/app.py +0 -0
  42. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/jobs/models.py +0 -0
  43. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/jobs/tasks.py +0 -0
  44. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/utils/__init__.py +0 -0
  45. {bullishpy-0.10.0 → bullishpy-0.12.0}/bullish/utils/checks.py +0 -0
@@ -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,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
 
@@ -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
 
@@ -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,13 +1,13 @@
1
1
  [tool.poetry]
2
2
  name = "bullishpy"
3
- version = "0.10.0"
3
+ version = "0.12.0"
4
4
  description = ""
5
5
  authors = ["aan <andoludovic.andriamamonjy@gmail.com>"]
6
6
  readme = "README.md"
7
7
  packages = [{ include = "bullish" }]
8
8
  [tool.poetry.dependencies]
9
9
  python = ">=3.12,<3.13"
10
- bearishpy = "^0.20.0"
10
+ bearishpy = "^0.22.0"
11
11
  tickermood = "^0.4.0"
12
12
  streamlit = "^1.45.1"
13
13
  streamlit-pydantic = "^v0.6.1-rc.3"
@@ -17,6 +17,7 @@ pandas-ta = "^0.3.14b0"
17
17
  plotly = "^6.1.2"
18
18
  ta-lib = "^0.6.4"
19
19
  click = ">=7.0,<=8.1"
20
+ joblib = "^1.5.1"
20
21
 
21
22
  [tool.poetry.scripts]
22
23
  bullish = "bullish.cli:app"
File without changes
File without changes