bullishpy 0.16.0__py3-none-any.whl → 0.18.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.

@@ -114,6 +114,13 @@ class TechnicalAnalysisBase(BaseModel):
114
114
  default=None,
115
115
  ),
116
116
  ]
117
+ max_year_loss: Annotated[
118
+ Optional[float],
119
+ BeforeValidator(to_float),
120
+ Field(
121
+ default=None,
122
+ ),
123
+ ]
117
124
 
118
125
 
119
126
  TechnicalAnalysisModels = [*IndicatorModels, TechnicalAnalysisBase]
@@ -131,7 +138,10 @@ class TechnicalAnalysis(*TechnicalAnalysisModels): # type: ignore
131
138
  return cls()
132
139
  try:
133
140
  res = Indicators().compute(prices)
134
- return cls(last_price=prices.close.iloc[-1], **res)
141
+ last_price = prices.close.iloc[-1]
142
+ max_price = prices.close.iloc[-253 * 2 :].max()
143
+ max_year_loss = (max_price - last_price) / max_price
144
+ return cls(last_price=last_price, max_year_loss=max_year_loss, **res)
135
145
  except Exception as e:
136
146
  logger.error(
137
147
  f"Failing to calculate technical analysis for {ticker.symbol}: {e}",
@@ -467,6 +477,13 @@ class AnalysisView(BaseModel):
467
477
  default=None,
468
478
  ),
469
479
  ]
480
+ max_year_loss: Annotated[
481
+ Optional[float],
482
+ BeforeValidator(to_float),
483
+ Field(
484
+ default=None,
485
+ ),
486
+ ]
470
487
  median_yearly_growth: Optional[float] = None
471
488
  median_weekly_growth: Optional[float] = None
472
489
  median_monthly_growth: Optional[float] = None
@@ -308,6 +308,8 @@ Sector = Literal[
308
308
  "Financial Services",
309
309
  "Conglomerates",
310
310
  ]
311
+
312
+ SubCountry = Literal["United kingdom", "United states", "Germany", "Belgium", "France"]
311
313
  Country = Literal[
312
314
  "Australia",
313
315
  "China",
@@ -1,4 +1,5 @@
1
1
  import datetime
2
+ import logging
2
3
  from typing import (
3
4
  Optional,
4
5
  Any,
@@ -17,11 +18,18 @@ from bearish.models.price.prices import Prices # type: ignore
17
18
  from bearish.models.query.query import AssetQuery, Symbols # type: ignore
18
19
  from pydantic import BaseModel, BeforeValidator, Field, model_validator
19
20
 
20
- from bullish.analysis.constants import Industry, IndustryGroup, Sector, Country
21
+ from bullish.analysis.constants import (
22
+ Industry,
23
+ IndustryGroup,
24
+ Sector,
25
+ Country,
26
+ SubCountry,
27
+ )
21
28
 
22
29
  if TYPE_CHECKING:
23
30
  from bullish.database.crud import BullishDb
24
31
 
32
+ logger = logging.getLogger(__name__)
25
33
  Type = Literal["Mean"]
26
34
 
27
35
  FUNCTIONS = {"Mean": np.mean}
@@ -47,12 +55,23 @@ def get_industry_comparison_data(
47
55
  country: Country,
48
56
  ) -> pd.DataFrame:
49
57
  try:
58
+
50
59
  views = bullish_db.read_returns(type, industry, country)
51
60
  industry_data = IndustryViews.from_views(views).to_dataframe()
52
61
  normalized_symbol = compute_normalized_close(symbol_data.close).rename("symbol")
53
- normalized_industry = industry_data.normalized_close.rename("industry")
54
- return pd.concat([normalized_symbol, normalized_industry], axis=1)
55
- except Exception:
62
+ normalized_industry = industry_data.normalized_close.rename(industry)
63
+ data = [normalized_symbol, normalized_industry]
64
+ for country in get_args(SubCountry):
65
+ views = bullish_db.read_returns(type, industry, country)
66
+ if views:
67
+ industry_data = IndustryViews.from_views(views).to_dataframe()
68
+ normalized_industry = industry_data.normalized_close.rename(
69
+ f"{industry}-{country}"
70
+ )
71
+ data.append(normalized_industry)
72
+ return pd.concat(data, axis=1)
73
+ except Exception as e:
74
+ logger.error(e)
56
75
  return pd.DataFrame()
57
76
 
58
77
 
@@ -176,7 +176,7 @@ RSI_CROSSOVER_30_GROWTH_STOCK = NamedFilterQuery(
176
176
  name="RSI cross-over 30 growth stock",
177
177
  price_per_earning_ratio=[10, 500],
178
178
  rsi_bullish_crossover_30=DATE_THRESHOLD,
179
- market_capitalization=[5e8, 1e12],
179
+ market_capitalization=[1e10, 1e13],
180
180
  order_by_desc="market_capitalization",
181
181
  country=[
182
182
  "Germany",
@@ -188,11 +188,25 @@ RSI_CROSSOVER_30_GROWTH_STOCK = NamedFilterQuery(
188
188
  "Belgium",
189
189
  ],
190
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
+ )
191
205
  RSI_CROSSOVER_40_GROWTH_STOCK = NamedFilterQuery(
192
206
  name="RSI cross-over 40 growth stock",
193
207
  price_per_earning_ratio=[10, 500],
194
208
  rsi_bullish_crossover_40=DATE_THRESHOLD,
195
- market_capitalization=[5e8, 1e12],
209
+ market_capitalization=[1e10, 1e13],
196
210
  order_by_desc="market_capitalization",
197
211
  country=[
198
212
  "Germany",
@@ -360,6 +374,7 @@ def predefined_filters() -> list[NamedFilterQuery]:
360
374
  MOMENTUM_GROWTH_RSI_30,
361
375
  MOMENTUM_GROWTH_RSI_40,
362
376
  GOLDEN_CROSS_LAST_SEVEN_DAYS,
377
+ MEDIAN_YEARLY_GROWTH,
363
378
  ]
364
379
 
365
380
 
@@ -0,0 +1,39 @@
1
+ """
2
+
3
+ Revision ID: 79bc71ec6f9e
4
+ Revises: 6d252e23f543
5
+ Create Date: 2025-08-02 15:07:58.266933
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 = "79bc71ec6f9e"
17
+ down_revision: Union[str, None] = "6d252e23f543"
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("max_year_loss", sa.Float(), nullable=True))
27
+ batch_op.create_index(
28
+ "ix_analysis_max_year_loss", ["max_year_loss"], unique=False
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_max_year_loss")
37
+ batch_op.drop_column("max_year_loss")
38
+
39
+ # ### end Alembic commands ###
bullish/database/crud.py CHANGED
@@ -261,10 +261,11 @@ class BullishDb(BearishDb, BullishDbBase): # type: ignore
261
261
  self, type: Type, industry: Industry, country: Country
262
262
  ) -> List[IndustryView]:
263
263
  with Session(self._engine) as session:
264
- stmt = select(IndustryViewORM).where(
265
- IndustryViewORM.industry == industry,
266
- IndustryViewORM.country == country,
267
- )
264
+ stmt = select(IndustryViewORM)
265
+ if industry:
266
+ stmt = stmt.where(IndustryViewORM.industry == industry)
267
+ if country:
268
+ stmt = stmt.where(IndustryViewORM.country == country)
268
269
  result = session.exec(stmt).all()
269
270
  return [IndustryView.model_validate(r) for r in result]
270
271
 
@@ -326,4 +327,7 @@ class BullishDb(BearishDb, BullishDbBase): # type: ignore
326
327
  with Session(self._engine) as session:
327
328
  stmt = select(BacktestResultORM)
328
329
  results = session.exec(stmt).all()
329
- return [BacktestResult.model_validate(r) for r in results]
330
+ if results:
331
+ return [BacktestResult.model_validate(r) for r in results]
332
+ else:
333
+ return []
@@ -37,7 +37,7 @@ def plot(
37
37
  f"MACD ({symbol} [{name}])",
38
38
  f"ADX ({symbol} [{name}])",
39
39
  f"OBV ({symbol} [{name}])",
40
- f"ATR ({symbol} [{name}])",
40
+ f"Industry ({symbol} [{name}])",
41
41
  ),
42
42
  )
43
43
  # Row 1: Candlestick + SMAs
@@ -123,33 +123,20 @@ def plot(
123
123
  row=6,
124
124
  col=1,
125
125
  )
126
- if (
127
- industry_data is not None
128
- and not industry_data.empty
129
- and "symbol" in industry_data.columns
130
- and "industry" in industry_data.columns
131
- ):
132
- fig.add_trace(
133
- go.Scatter(
134
- x=industry_data.index,
135
- y=industry_data.symbol,
136
- name="Symbol",
137
- mode="lines",
138
- ),
139
- row=7,
140
- col=1,
141
- )
142
- fig.add_trace(
143
- go.Scatter(
144
- x=industry_data.index,
145
- y=industry_data.industry,
146
- name="Industry",
147
- mode="lines",
148
- opacity=0.5,
149
- ),
150
- row=7,
151
- col=1,
152
- )
126
+ if industry_data is not None and not industry_data.empty:
127
+ for c in industry_data.columns:
128
+ fig.add_trace(
129
+ go.Scatter(
130
+ x=industry_data.index,
131
+ y=industry_data[c],
132
+ name=c,
133
+ mode="lines",
134
+ opacity=0.5 if c != "symbol" else 1.0,
135
+ ),
136
+ row=7,
137
+ col=1,
138
+ )
139
+
153
140
  if dates is not None and dates:
154
141
  for date in dates:
155
142
  if (
bullish/jobs/tasks.py CHANGED
@@ -13,6 +13,7 @@ from huey.api import Task # type: ignore
13
13
  from .models import JobTrackerStatus, JobTracker, JobType
14
14
  from ..analysis.analysis import run_analysis, run_signal_series_analysis
15
15
  from ..analysis.backtest import run_many_tests, BackTestConfig
16
+ from ..analysis.industry_views import compute_industry_view
16
17
  from ..analysis.predefined_filters import predefined_filters
17
18
  from ..database.crud import BullishDb
18
19
  from bullish.analysis.filter import FilterUpdate
@@ -85,6 +86,7 @@ def analysis(
85
86
  ) -> None:
86
87
  bullish_db = BullishDb(database_path=database_path)
87
88
  run_analysis(bullish_db)
89
+ compute_industry_view(bullish_db)
88
90
 
89
91
 
90
92
  @huey.task(context=True) # type: ignore
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: bullishpy
3
- Version: 0.16.0
3
+ Version: 0.18.0
4
4
  Summary:
5
5
  Author: aan
6
6
  Author-email: andoludovic.andriamamonjy@gmail.com
@@ -1,13 +1,13 @@
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=SqlLrbhMzm2dvK89Ip5KCyTOoLS41vNf1WJolevLKfc,21638
3
+ bullish/analysis/analysis.py,sha256=9a1j1-Fe0XW5_fiNXtgs7XM_0Rl0mQOWvXQv9nUirdg,22124
4
4
  bullish/analysis/backtest.py,sha256=u3zzdP0IzpcxSzoI_wWT-YkmBulwVCFfhusttquTjSQ,14291
5
- bullish/analysis/constants.py,sha256=tVDPQEufH8lytMj4DdUdvXt79b7cvWaDwSUOpeqMWts,9851
5
+ bullish/analysis/constants.py,sha256=OpjmrMWHKstoqi314GdJU0vg23IgWqa_rrFU_Lsms0Q,9940
6
6
  bullish/analysis/filter.py,sha256=wBVkHhQPfG8wJ0TR3KLo_Bb7cRZFPyHlF4UK2mpG6S0,8495
7
7
  bullish/analysis/functions.py,sha256=JSxYCuua_sMGLosN83j0GcY0Ls_gsE4OZLLGInxG9RA,14354
8
8
  bullish/analysis/indicators.py,sha256=Dpps-v4bfQ3KF-C8zjMlArw1DJgZo-_EedYwihIiFJ0,24462
9
- bullish/analysis/industry_views.py,sha256=1B5V39Fm9rNQEsun1xrwELfOiKlGdTie0ZolS2UBh2w,6247
10
- bullish/analysis/predefined_filters.py,sha256=5G75u4uDLHJR-iEtGZ2s-gkAqNIRUYu5msK8G-LWmD0,12074
9
+ bullish/analysis/industry_views.py,sha256=3TwruMdRYtmBC5bRYFptLt18AN9PQSY9d-uWnTiJKgY,6787
10
+ bullish/analysis/predefined_filters.py,sha256=FnEyWcVKlCSlDhE8LHGXGY3tFZmFTnExQmg78BW9ICs,12417
11
11
  bullish/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  bullish/app/app.py,sha256=rIvz4A0aJu7wGXBSCqO3EBA4QGhV8GPYVOYLOd9WjnY,14253
13
13
  bullish/cli.py,sha256=uYLZmGDAolZKWzduZ58bP-xul1adg0oKfeUQtZMXTvA,1958
@@ -27,13 +27,14 @@ bullish/database/alembic/versions/4b0a2f40b7d3_.py,sha256=G0K7w7pOPYjPZkXTB8LWhx
27
27
  bullish/database/alembic/versions/5b10ee7604c1_.py,sha256=YlqaagPasR3RKASv7acME1jPS8p26VoTE2BvpOwdCpY,1463
28
28
  bullish/database/alembic/versions/6d252e23f543_.py,sha256=izF-ejdXk733INkAokGqjA2U_M0_c1f_ruihZ-cgP7s,1525
29
29
  bullish/database/alembic/versions/73564b60fe24_.py,sha256=MTlDRDNHj3E9gK7IMeAzv2UxxxYtWiu3gI_9xTLE-wg,1008
30
+ bullish/database/alembic/versions/79bc71ec6f9e_.py,sha256=4nShut2NEd1F3piSckIIBtke0GEsFAxYw5TZl5YYRzc,1140
30
31
  bullish/database/alembic/versions/b76079e9845f_.py,sha256=W8eeTABjI9tT1dp3hlK7g7tiKqDhmA8AoUX9Sw-ykLI,1165
31
32
  bullish/database/alembic/versions/bf6b86dd5463_.py,sha256=fKB8knCprGmiL6AEyFdhybVmB7QX_W4MPFF9sPzUrSM,1094
32
33
  bullish/database/alembic/versions/d663166c531d_.py,sha256=U92l6QXqPniAYrPeu2Bt77ReDbXveLj4aGXtgd806JY,1915
33
34
  bullish/database/alembic/versions/ec25c8fa449f_.py,sha256=8Yts74KEjK4jg20zIo90_0atw-sOBuE3hgCKl-rfS5E,2271
34
35
  bullish/database/alembic/versions/ee5baabb35f8_.py,sha256=nBMEY-_C8AsSXVPyaDdUkwrFFo2gxShzJhmrjejDwtc,1632
35
36
  bullish/database/alembic/versions/fc191121f522_.py,sha256=0sstF6TpAJ09-Mt-Vek9SdSWksvi4C58a5D92rBtuY8,1894
36
- bullish/database/crud.py,sha256=g7gA0NKmAw7SLF-Wk-r5m1BJFOtcxDVC9idPYYKICjk,12565
37
+ bullish/database/crud.py,sha256=mu4Ddvg94gLNUENGTw1dbT6dgW5AiS7O3JpehB5vOWo,12701
37
38
  bullish/database/schemas.py,sha256=3uRcNKuobqWC3mCfInzo-4KhrZp3DH6yx_0TEbLoHME,3428
38
39
  bullish/database/scripts/create_revision.py,sha256=rggIf-3koPqJNth8FIg89EOfnIM7a9QrvL8X7UJsP0g,628
39
40
  bullish/database/scripts/stamp.py,sha256=PWgVUEBumjNUMjTnGw46qmU3p221LeN-KspnW_gFuu4,839
@@ -41,16 +42,16 @@ bullish/database/scripts/upgrade.py,sha256=-Gz7aFNPEt9y9e1kltqXE76-j_8QeNtet_Vlw
41
42
  bullish/database/settings.py,sha256=nMudufmF7iC_62_PHrGSMjlqDLN2I0qTbtz9JKZHSko,164
42
43
  bullish/exceptions.py,sha256=4z_i-dD-CDz1bkGmZH9DOf1L_awlCPCgdUDPF7dhWAI,106
43
44
  bullish/figures/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
- bullish/figures/figures.py,sha256=imrvIIcL9L-z-3vzWK5hDEsNttZs60QxlFI-PLw0hJQ,4829
45
+ bullish/figures/figures.py,sha256=EpJQOiSqSp7cHvZoGlZrF6UVpyv-fFyDApAfskqdUkU,4562
45
46
  bullish/interface/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
47
  bullish/interface/interface.py,sha256=9bhXOKlYtoCbbbDBzmwlVK2HuAzfO-1kE8mk_MMG0MM,5046
47
48
  bullish/jobs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
49
  bullish/jobs/app.py,sha256=5MJ5KXUo7JSNAvOPgkpIMasD11VTrjQvGzM7vmCY65E,77
49
50
  bullish/jobs/models.py,sha256=S2yvBf69lmt4U-5OU5CjXCMSw0s9Ubh9xkrB3k2qOZo,764
50
- bullish/jobs/tasks.py,sha256=6IH45-JV3GFM6Q9uIsuLNjCvdburx3gJZJLI3x0Dn2Y,3593
51
+ bullish/jobs/tasks.py,sha256=vXPbISYFUZlrYkMVUb7y-g-z0BBi91wQLm8RbYqR4I0,3691
51
52
  bullish/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
53
  bullish/utils/checks.py,sha256=Va10_xDVVnxYkOD2hafvyQ-TFV8FQpOkr4huJ7XgpDM,2188
53
- bullishpy-0.16.0.dist-info/METADATA,sha256=db70yObjbrRpbGeqwITzNA1XGHjKZ_KXOOUTRimxgi0,828
54
- bullishpy-0.16.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
55
- bullishpy-0.16.0.dist-info/entry_points.txt,sha256=eaPpmL6vmSBFo0FBtwibCXGqAW4LFJ83whJzT1VjD-0,43
56
- bullishpy-0.16.0.dist-info/RECORD,,
54
+ bullishpy-0.18.0.dist-info/METADATA,sha256=fK-rvdh92W54Rk10x5_uJzWGG8zG15KYB8jyiAuLZak,828
55
+ bullishpy-0.18.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
56
+ bullishpy-0.18.0.dist-info/entry_points.txt,sha256=eaPpmL6vmSBFo0FBtwibCXGqAW4LFJ83whJzT1VjD-0,43
57
+ bullishpy-0.18.0.dist-info/RECORD,,