bullishpy 0.11.0__tar.gz → 0.13.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 (47) hide show
  1. {bullishpy-0.11.0 → bullishpy-0.13.0}/PKG-INFO +3 -2
  2. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/analysis/analysis.py +24 -3
  3. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/analysis/functions.py +27 -2
  4. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/analysis/indicators.py +16 -1
  5. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/analysis/predefined_filters.py +74 -0
  6. bullishpy-0.13.0/bullish/database/alembic/versions/b76079e9845f_.py +40 -0
  7. bullishpy-0.13.0/bullish/database/alembic/versions/bf6b86dd5463_.py +38 -0
  8. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/database/crud.py +10 -0
  9. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/interface/interface.py +6 -0
  10. {bullishpy-0.11.0 → bullishpy-0.13.0}/pyproject.toml +3 -2
  11. {bullishpy-0.11.0 → bullishpy-0.13.0}/README.md +0 -0
  12. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/__init__.py +0 -0
  13. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/analysis/__init__.py +0 -0
  14. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/analysis/filter.py +0 -0
  15. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/app/__init__.py +0 -0
  16. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/app/app.py +0 -0
  17. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/cli.py +0 -0
  18. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/database/__init__.py +0 -0
  19. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/database/alembic/README +0 -0
  20. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/database/alembic/alembic.ini +0 -0
  21. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/database/alembic/env.py +0 -0
  22. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/database/alembic/script.py.mako +0 -0
  23. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/database/alembic/versions/037dbd721317_.py +0 -0
  24. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/database/alembic/versions/08ac1116e055_.py +0 -0
  25. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/database/alembic/versions/11d35a452b40_.py +0 -0
  26. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/database/alembic/versions/17e51420e7ad_.py +0 -0
  27. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/database/alembic/versions/49c83f9eb5ac_.py +0 -0
  28. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/database/alembic/versions/4b0a2f40b7d3_.py +0 -0
  29. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/database/alembic/versions/73564b60fe24_.py +0 -0
  30. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/database/alembic/versions/d663166c531d_.py +0 -0
  31. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/database/alembic/versions/ee5baabb35f8_.py +0 -0
  32. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/database/alembic/versions/fc191121f522_.py +0 -0
  33. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/database/schemas.py +0 -0
  34. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/database/scripts/create_revision.py +0 -0
  35. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/database/scripts/stamp.py +0 -0
  36. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/database/scripts/upgrade.py +0 -0
  37. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/database/settings.py +0 -0
  38. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/exceptions.py +0 -0
  39. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/figures/__init__.py +0 -0
  40. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/figures/figures.py +0 -0
  41. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/interface/__init__.py +0 -0
  42. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/jobs/__init__.py +0 -0
  43. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/jobs/app.py +0 -0
  44. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/jobs/models.py +0 -0
  45. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/jobs/tasks.py +0 -0
  46. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/utils/__init__.py +0 -0
  47. {bullishpy-0.11.0 → bullishpy-0.13.0}/bullish/utils/checks.py +0 -0
@@ -1,15 +1,16 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: bullishpy
3
- Version: 0.11.0
3
+ Version: 0.13.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
+ )
@@ -122,6 +122,9 @@ def compute_roc(data: pd.DataFrame) -> pd.DataFrame:
122
122
  results["ROC_7"] = talib.ROC(data.close, timeperiod=7) # type: ignore
123
123
  results["ROC_1"] = talib.ROC(data.close, timeperiod=1) # type: ignore
124
124
  results["ROC_30"] = talib.ROC(data.close, timeperiod=30) # type: ignore
125
+ mom = talib.MOM(data.close, timeperiod=252) # type: ignore
126
+ results["MOM"] = mom.shift(21) # type: ignore
127
+
125
128
  return results
126
129
 
127
130
 
@@ -137,6 +140,7 @@ def compute_sma(data: pd.DataFrame) -> pd.DataFrame:
137
140
  results = pd.DataFrame(index=data.index)
138
141
  results["SMA_50"] = talib.SMA(data.close, timeperiod=50) # type: ignore
139
142
  results["SMA_200"] = talib.SMA(data.close, timeperiod=200) # type: ignore
143
+ results["CLOSE"] = data.close
140
144
  return results
141
145
 
142
146
 
@@ -144,6 +148,8 @@ def compute_pandas_ta_sma(data: pd.DataFrame) -> pd.DataFrame:
144
148
  results = pd.DataFrame(index=data.index)
145
149
  results["SMA_50"] = ta.sma(data.close, length=50)
146
150
  results["SMA_200"] = ta.sma(data.close, length=200)
151
+ results["CLOSE"] = data.close
152
+
147
153
  return results
148
154
 
149
155
 
@@ -294,6 +300,25 @@ def compute_percentile_return_after_rsi_crossover(
294
300
  return float(np.percentile(values, 30))
295
301
 
296
302
 
303
+ def find_last_true_run_start(series: pd.Series) -> Optional[date]:
304
+ if not series.iloc[-1]:
305
+ return None
306
+ arr = series.to_numpy()
307
+ change_points = np.flatnonzero(np.r_[True, arr[1:] != arr[:-1]])
308
+ run_starts = change_points
309
+ true_runs = run_starts[arr[run_starts]]
310
+ last_true_run_start = true_runs[-1]
311
+ return series.index[last_true_run_start].date() # type: ignore
312
+
313
+
314
+ def momentum(data: pd.DataFrame) -> Optional[date]:
315
+ date_1 = find_last_true_run_start(data.SMA_50 < data.CLOSE)
316
+ date_2 = find_last_true_run_start(data.SMA_200 < data.SMA_50)
317
+ if date_1 is None or date_2 is None:
318
+ return None
319
+ return max(date_1, date_2)
320
+
321
+
297
322
  class IndicatorFunction(BaseModel):
298
323
  expected_columns: list[str]
299
324
  functions: list[Callable[[pd.DataFrame], pd.DataFrame]]
@@ -334,7 +359,7 @@ MFI = IndicatorFunction(
334
359
  expected_columns=["MFI"], functions=[compute_mfi, compute_pandas_ta_mfi]
335
360
  )
336
361
  ROC = IndicatorFunction(
337
- expected_columns=["ROC_7", "ROC_1", "ROC_30"],
362
+ expected_columns=["ROC_7", "ROC_1", "ROC_30", "MOM"],
338
363
  functions=[compute_roc, compute_pandas_ta_roc],
339
364
  )
340
365
  CANDLESTOCK_PATTERNS = IndicatorFunction(
@@ -351,7 +376,7 @@ CANDLESTOCK_PATTERNS = IndicatorFunction(
351
376
  )
352
377
 
353
378
  SMA = IndicatorFunction(
354
- expected_columns=["SMA_50", "SMA_200"],
379
+ expected_columns=["SMA_50", "SMA_200", "CLOSE"],
355
380
  functions=[compute_sma, compute_pandas_ta_sma],
356
381
  )
357
382
 
@@ -20,6 +20,7 @@ from bullish.analysis.functions import (
20
20
  ADOSC,
21
21
  PRICE,
22
22
  compute_percentile_return_after_rsi_crossover,
23
+ momentum,
23
24
  )
24
25
 
25
26
  logger = logging.getLogger(__name__)
@@ -282,6 +283,13 @@ def indicators_factory() -> List[Indicator]:
282
283
  type=Optional[date],
283
284
  function=lambda d: cross(d.SMA_50, d.SMA_200, above=False),
284
285
  ),
286
+ Signal(
287
+ name="MOMENTUM_TIME_SPAN",
288
+ description="Momentum time span",
289
+ type_info="Overbought",
290
+ type=Optional[date],
291
+ function=lambda d: momentum(d),
292
+ ),
285
293
  ],
286
294
  ),
287
295
  Indicator(
@@ -334,7 +342,7 @@ def indicators_factory() -> List[Indicator]:
334
342
  Indicator(
335
343
  name="ROC",
336
344
  description="Rate Of Change",
337
- expected_columns=["ROC_7", "ROC_30", "ROC_1"],
345
+ expected_columns=ROC.expected_columns,
338
346
  function=ROC.call,
339
347
  signals=[
340
348
  Signal(
@@ -379,6 +387,13 @@ def indicators_factory() -> List[Indicator]:
379
387
  type=Optional[float],
380
388
  function=lambda d: d.ROC_7.tolist()[-1],
381
389
  ),
390
+ Signal(
391
+ name="MOMENTUM",
392
+ type_info="Value",
393
+ description="7-day Rate of Change",
394
+ type=Optional[float],
395
+ function=lambda d: d.MOM.iloc[-1],
396
+ ),
382
397
  ],
383
398
  ),
384
399
  Indicator(
@@ -309,6 +309,75 @@ RSI_CROSSOVER_45_GROWTH_STOCK = NamedFilterQuery(
309
309
  order_by_desc="market_capitalization",
310
310
  country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
311
311
  )
312
+ MOMENTUM_STOCK_STRONG_FUNDAMENTAL = NamedFilterQuery(
313
+ name="Momentum stock strong fundamental",
314
+ income=[
315
+ "positive_operating_income",
316
+ "growing_operating_income",
317
+ "positive_net_income",
318
+ "growing_net_income",
319
+ ],
320
+ cash_flow=["positive_free_cash_flow"],
321
+ properties=["operating_cash_flow_is_higher_than_net_income"],
322
+ price_per_earning_ratio=[10, 400],
323
+ last_price=[1, 70],
324
+ order_by_desc="momentum",
325
+ country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
326
+ )
327
+ MOMENTUM_STOCK = NamedFilterQuery(
328
+ name="Momentum stock",
329
+ cash_flow=["positive_free_cash_flow"],
330
+ properties=["operating_cash_flow_is_higher_than_net_income"],
331
+ price_per_earning_ratio=[10, 400],
332
+ last_price=[1, 70],
333
+ order_by_desc="momentum",
334
+ country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
335
+ )
336
+ MOMENTUM_STOCK_NO_FUNDAMENTAL_CHECKS = NamedFilterQuery(
337
+ name="Momentum stock no fundamental checks",
338
+ price_per_earning_ratio=[10, 500],
339
+ last_price=[1, 10000],
340
+ order_by_desc="momentum",
341
+ country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
342
+ )
343
+ MOMENTUM_TIME_SPAN_1_MONTH = NamedFilterQuery(
344
+ name="Momentum 1 month",
345
+ price_per_earning_ratio=[10, 500],
346
+ last_price=[1, 10000],
347
+ momentum_time_span=[
348
+ datetime.date.today() - datetime.timedelta(days=90),
349
+ datetime.date.today() - datetime.timedelta(days=31),
350
+ ],
351
+ macd_12_26_9_bullish_crossover=[
352
+ datetime.date.today() - datetime.timedelta(days=10),
353
+ datetime.date.today(),
354
+ ],
355
+ order_by_desc="momentum",
356
+ country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
357
+ )
358
+ MOMENTUM_TIME_SPAN_1_MONTH_STRONG_FUNDAMENTALS = NamedFilterQuery(
359
+ name="Momentum 1 month strong fundamentals",
360
+ income=[
361
+ "positive_operating_income",
362
+ "growing_operating_income",
363
+ "positive_net_income",
364
+ "growing_net_income",
365
+ ],
366
+ cash_flow=["positive_free_cash_flow"],
367
+ properties=["operating_cash_flow_is_higher_than_net_income"],
368
+ price_per_earning_ratio=[10, 500],
369
+ last_price=[1, 10000],
370
+ momentum_time_span=[
371
+ datetime.date.today() - datetime.timedelta(days=90),
372
+ datetime.date.today() - datetime.timedelta(days=31),
373
+ ],
374
+ macd_12_26_9_bullish_crossover=[
375
+ datetime.date.today() - datetime.timedelta(days=10),
376
+ datetime.date.today(),
377
+ ],
378
+ order_by_desc="momentum",
379
+ country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
380
+ )
312
381
 
313
382
 
314
383
  def predefined_filters() -> list[NamedFilterQuery]:
@@ -319,6 +388,11 @@ def predefined_filters() -> list[NamedFilterQuery]:
319
388
  RSI_CROSSOVER_30_GROWTH_STOCK,
320
389
  RSI_CROSSOVER_40_GROWTH_STOCK,
321
390
  RSI_CROSSOVER_45_GROWTH_STOCK,
391
+ MOMENTUM_STOCK_STRONG_FUNDAMENTAL,
392
+ MOMENTUM_STOCK,
393
+ MOMENTUM_STOCK_NO_FUNDAMENTAL_CHECKS,
394
+ MOMENTUM_TIME_SPAN_1_MONTH,
395
+ MOMENTUM_TIME_SPAN_1_MONTH_STRONG_FUNDAMENTALS,
322
396
  ]
323
397
 
324
398
 
@@ -0,0 +1,40 @@
1
+ """
2
+
3
+ Revision ID: b76079e9845f
4
+ Revises: bf6b86dd5463
5
+ Create Date: 2025-07-12 21:32:08.865721
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 = "b76079e9845f"
17
+ down_revision: Union[str, None] = "bf6b86dd5463"
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
+
25
+ with op.batch_alter_table("analysis", schema=None) as batch_op:
26
+ batch_op.add_column(sa.Column("momentum_time_span", sa.Date(), nullable=True))
27
+ batch_op.create_index(
28
+ "ix_analysis_momentum_time_span", ["momentum_time_span"], unique=False
29
+ )
30
+
31
+ # ### end Alembic commands ###
32
+
33
+
34
+ def downgrade() -> None:
35
+ # ### commands auto generated by Alembic - please adjust! ###
36
+ with op.batch_alter_table("analysis", schema=None) as batch_op:
37
+ batch_op.drop_index("ix_analysis_momentum_time_span")
38
+ batch_op.drop_column("momentum_time_span")
39
+
40
+ # ### end Alembic commands ###
@@ -0,0 +1,38 @@
1
+ """
2
+
3
+ Revision ID: bf6b86dd5463
4
+ Revises: 17e51420e7ad
5
+ Create Date: 2025-07-11 18:32:21.450156
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 = "bf6b86dd5463"
17
+ down_revision: Union[str, None] = "17e51420e7ad"
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
+
25
+ with op.batch_alter_table("analysis", schema=None) as batch_op:
26
+ batch_op.add_column(sa.Column("momentum", sa.Float(), nullable=True))
27
+ batch_op.create_index("ix_analysis_momentum", ["momentum"], unique=False)
28
+
29
+ # ### end Alembic commands ###
30
+
31
+
32
+ def downgrade() -> None:
33
+ # ### commands auto generated by Alembic - please adjust! ###
34
+ with op.batch_alter_table("analysis", schema=None) as batch_op:
35
+ batch_op.drop_index("ix_analysis_momentum")
36
+ batch_op.drop_column("momentum")
37
+
38
+ # ### end Alembic commands ###
@@ -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.11.0"
3
+ version = "0.13.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