bullishpy 0.66.0__tar.gz → 0.67.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 (63) hide show
  1. {bullishpy-0.66.0 → bullishpy-0.67.0}/PKG-INFO +1 -1
  2. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/analysis/filter.py +0 -1
  3. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/analysis/functions.py +35 -0
  4. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/analysis/indicators.py +14 -0
  5. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/analysis/predefined_filters.py +28 -12
  6. bullishpy-0.67.0/bullish/database/alembic/versions/cc28171c21a4_.py +43 -0
  7. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/figures/figures.py +28 -18
  8. {bullishpy-0.66.0 → bullishpy-0.67.0}/pyproject.toml +1 -1
  9. {bullishpy-0.66.0 → bullishpy-0.67.0}/LICENSE +0 -0
  10. {bullishpy-0.66.0 → bullishpy-0.67.0}/README.md +0 -0
  11. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/__init__.py +0 -0
  12. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/analysis/__init__.py +0 -0
  13. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/analysis/analysis.py +0 -0
  14. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/analysis/backtest.py +0 -0
  15. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/analysis/constants.py +0 -0
  16. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/analysis/industry_views.py +0 -0
  17. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/app/__init__.py +0 -0
  18. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/app/app.py +0 -0
  19. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/cli.py +0 -0
  20. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/__init__.py +0 -0
  21. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/alembic/README +0 -0
  22. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/alembic/alembic.ini +0 -0
  23. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/alembic/env.py +0 -0
  24. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/alembic/script.py.mako +0 -0
  25. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/alembic/versions/037dbd721317_.py +0 -0
  26. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/alembic/versions/040b15fba458_.py +0 -0
  27. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/alembic/versions/08ac1116e055_.py +0 -0
  28. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/alembic/versions/11d35a452b40_.py +0 -0
  29. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/alembic/versions/12889a2cbd7d_.py +0 -0
  30. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/alembic/versions/17e51420e7ad_.py +0 -0
  31. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/alembic/versions/260fcff7212e_.py +0 -0
  32. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/alembic/versions/49c83f9eb5ac_.py +0 -0
  33. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/alembic/versions/4b0a2f40b7d3_.py +0 -0
  34. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/alembic/versions/4ee82b171449_.py +0 -0
  35. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/alembic/versions/5b10ee7604c1_.py +0 -0
  36. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/alembic/versions/6d252e23f543_.py +0 -0
  37. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/alembic/versions/73564b60fe24_.py +0 -0
  38. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/alembic/versions/79bc71ec6f9e_.py +0 -0
  39. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/alembic/versions/ae444f338124_.py +0 -0
  40. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/alembic/versions/b76079e9845f_.py +0 -0
  41. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/alembic/versions/bf6b86dd5463_.py +0 -0
  42. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/alembic/versions/d0e58e050845_.py +0 -0
  43. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/alembic/versions/d663166c531d_.py +0 -0
  44. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/alembic/versions/ec25c8fa449f_.py +0 -0
  45. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/alembic/versions/ee5baabb35f8_.py +0 -0
  46. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/alembic/versions/fc191121f522_.py +0 -0
  47. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/alembic/versions/ff0cc4ba40ec_.py +0 -0
  48. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/crud.py +0 -0
  49. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/schemas.py +0 -0
  50. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/scripts/create_revision.py +0 -0
  51. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/scripts/stamp.py +0 -0
  52. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/scripts/upgrade.py +0 -0
  53. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/database/settings.py +0 -0
  54. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/exceptions.py +0 -0
  55. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/figures/__init__.py +0 -0
  56. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/interface/__init__.py +0 -0
  57. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/interface/interface.py +0 -0
  58. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/jobs/__init__.py +0 -0
  59. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/jobs/app.py +0 -0
  60. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/jobs/models.py +0 -0
  61. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/jobs/tasks.py +0 -0
  62. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/utils/__init__.py +0 -0
  63. {bullishpy-0.66.0 → bullishpy-0.67.0}/bullish/utils/checks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: bullishpy
3
- Version: 0.66.0
3
+ Version: 0.67.0
4
4
  Summary:
5
5
  Author: aan
6
6
  Author-email: andoludovic.andriamamonjy@gmail.com
@@ -180,7 +180,6 @@ class GeneralFilter(BaseModel):
180
180
  price_per_earning_ratio: Optional[List[float]] = Field(default=[0.0, 1000.0])
181
181
 
182
182
 
183
-
184
183
  class FilterQuery(GeneralFilter, *TechnicalAnalysisFilters, *FundamentalAnalysisFilters): # type: ignore
185
184
 
186
185
  def valid(self) -> bool:
@@ -434,3 +434,38 @@ def add_indicators(data: pd.DataFrame) -> pd.DataFrame:
434
434
  f"Expected columns {expected_columns} not found in data columns {data.columns.tolist()}"
435
435
  )
436
436
  return data
437
+
438
+
439
+ class Line(BaseModel):
440
+ value: float
441
+ previous: float
442
+
443
+
444
+ class SupportResistance(BaseModel):
445
+ support: Line
446
+ resistance: Line
447
+
448
+
449
+ def support_resistance(df: pd.DataFrame, window: int = 5) -> SupportResistance:
450
+
451
+ w = window * 2 + 1
452
+ highs = df.high.rolling(w, center=True).max()
453
+ lows = df.low.rolling(w, center=True).min()
454
+ swing_high_mask = df.high == highs
455
+ swing_low_mask = df.low == lows
456
+
457
+ raw_res = df.loc[swing_high_mask, "high"].to_numpy()
458
+ raw_sup = df.loc[swing_low_mask, "low"].to_numpy()
459
+ return SupportResistance(
460
+ support=Line(value=float(raw_sup[-1]), previous=float(raw_sup[-2])),
461
+ resistance=Line(value=float(raw_res[-1]), previous=float(raw_res[-2])),
462
+ )
463
+
464
+
465
+ def bollinger_bands(
466
+ data: pd.DataFrame, window: int = 20, std_dev: float = 2.0
467
+ ) -> pd.DataFrame:
468
+ bbands = ta.bbands(
469
+ data.close, timeperiod=window, nbdevup=std_dev, nbdevdn=std_dev, matype=0
470
+ )
471
+ return bbands # type: ignore
@@ -175,6 +175,20 @@ def indicators_factory() -> List[Indicator]:
175
175
  type=Optional[date],
176
176
  function=lambda d: (d.ADX_14 > 20) & (d.MINUS_DI > d.PLUS_DI),
177
177
  ),
178
+ Signal(
179
+ name="ADX_14",
180
+ description="ADX 14",
181
+ type_info="Short",
182
+ type=Optional[date],
183
+ function=lambda d: (d.ADX_14 > 25),
184
+ ),
185
+ Signal(
186
+ name="ADX_14_OVERBOUGHT",
187
+ description="ADX 14 OVERBOUGHT",
188
+ type_info="Short",
189
+ type=Optional[date],
190
+ function=lambda d: (d.ADX_14 > 50),
191
+ ),
178
192
  ],
179
193
  ),
180
194
  Indicator(
@@ -218,6 +218,12 @@ class NamedFilterQuery(FilterQuery):
218
218
  def rsi_oversold_(self) -> "NamedFilterQuery":
219
219
  return self.update_indicator_filter("RSI Oversold", "rsi_oversold")
220
220
 
221
+ def rsi_overbought_(self) -> "NamedFilterQuery":
222
+ return self.update_indicator_filter("RSI Overbought", "rsi_overbought")
223
+
224
+ def adx(self) -> "NamedFilterQuery":
225
+ return self.update_indicator_filter("ADX 14", "adx_14")
226
+
221
227
  def earnings_date(self) -> "NamedFilterQuery":
222
228
  return NamedFilterQuery.model_validate(
223
229
  self.model_dump()
@@ -231,25 +237,35 @@ class NamedFilterQuery(FilterQuery):
231
237
  )
232
238
 
233
239
  def variants(
234
- self, variants: Optional[List[List[str]]] = None
240
+ self,
241
+ variants: Optional[List[List[str]]] = None,
242
+ filters: Optional[List[str]] = None,
235
243
  ) -> List["NamedFilterQuery"]:
244
+ if filters and self.name not in filters:
245
+ return [self]
236
246
  variants = variants or [["europe"], ["us"]]
237
247
 
238
248
  _variants = {v for variant in variants for v in _get_variants(variant)}
239
- filters = []
249
+ filters_ = []
240
250
  for attributes in _variants:
241
- filter = self
251
+ filter__ = self
242
252
  for attr in attributes:
243
- filter = getattr(filter, attr)()
244
- filters.append(filter)
253
+ filter__ = getattr(filter__, attr)()
254
+ filters_.append(filter__)
245
255
 
246
- return filters
256
+ return filters_
247
257
 
248
258
 
249
259
  def load_custom_filters() -> List[NamedFilterQuery]:
250
260
  if "CUSTOM_FILTERS_PATH" in os.environ:
251
261
  custom_filters_path = os.environ["CUSTOM_FILTERS_PATH"]
252
- return read_custom_filters(Path(custom_filters_path))
262
+ return [
263
+ variant
264
+ for f in read_custom_filters(Path(custom_filters_path))
265
+ for variant in f.variants(
266
+ variants=[["rsi_overbought_"]], filters=["portfolio", "Portfolio"]
267
+ )
268
+ ]
253
269
  return []
254
270
 
255
271
 
@@ -283,11 +299,11 @@ LARGE_CAPS = NamedFilterQuery(
283
299
  ).variants(
284
300
  variants=[
285
301
  ["europe", "rsi_oversold_", "macd", "fundamentals"],
286
- ["us", "rsi_oversold_", "macd", "fundamentals"],
287
- ["europe", "rsi_neutral_", "macd", "fundamentals"],
288
- ["us", "rsi_neutral_", "macd", "fundamentals"],
289
- ["europe", "rsi_30", "macd", "fundamentals"],
290
- ["us", "rsi_30", "macd", "fundamentals"],
302
+ ["us", "rsi_oversold_", "macd", "adx", "fundamentals"],
303
+ ["europe", "rsi_neutral_", "macd", "adx", "fundamentals"],
304
+ ["us", "rsi_neutral_", "macd", "adx", "fundamentals"],
305
+ ["europe", "rsi_30", "macd", "adx", "fundamentals"],
306
+ ["us", "rsi_30", "macd", "adx", "fundamentals"],
291
307
  ["europe", "top_performers", "cheap"],
292
308
  ["us", "top_performers", "cheap"],
293
309
  ["europe", "earnings_date"],
@@ -0,0 +1,43 @@
1
+ """
2
+
3
+ Revision ID: cc28171c21a4
4
+ Revises: 260fcff7212e
5
+ Create Date: 2025-08-15 17:04:59.467407
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 = "cc28171c21a4"
17
+ down_revision: Union[str, None] = "260fcff7212e"
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
+ with op.batch_alter_table("analysis", schema=None) as batch_op:
25
+ batch_op.add_column(sa.Column("adx_14", sa.Date(), nullable=True))
26
+ batch_op.add_column(sa.Column("adx_14_overbought", sa.Date(), nullable=True))
27
+ batch_op.create_index("ix_analysis_adx_14", ["adx_14"], unique=False)
28
+ batch_op.create_index(
29
+ "ix_analysis_adx_14_overbought", ["adx_14_overbought"], unique=False
30
+ )
31
+
32
+ # ### end Alembic commands ###
33
+
34
+
35
+ def downgrade() -> None:
36
+ # ### commands auto generated by Alembic - please adjust! ###
37
+ with op.batch_alter_table("analysis", schema=None) as batch_op:
38
+ batch_op.drop_index("ix_analysis_adx_14_overbought")
39
+ batch_op.drop_index("ix_analysis_adx_14")
40
+ batch_op.drop_column("adx_14_overbought")
41
+ batch_op.drop_column("adx_14")
42
+
43
+ # ### end Alembic commands ###
@@ -5,7 +5,10 @@ import pandas as pd
5
5
  import plotly.graph_objects as go
6
6
  from plotly.subplots import make_subplots
7
7
 
8
- from bullish.analysis.functions import add_indicators
8
+ from bullish.analysis.functions import (
9
+ add_indicators,
10
+ support_resistance,
11
+ )
9
12
  from datetime import date
10
13
 
11
14
 
@@ -17,6 +20,8 @@ def plot(
17
20
  industry_data: Optional[pd.DataFrame] = None,
18
21
  ) -> go.Figure:
19
22
  data = add_indicators(data)
23
+ supports = support_resistance(data)
24
+
20
25
  fig = make_subplots(
21
26
  rows=7,
22
27
  cols=1,
@@ -36,8 +41,8 @@ def plot(
36
41
  f"RSI ({symbol} [{name}])",
37
42
  f"MACD ({symbol} [{name}])",
38
43
  f"ADX ({symbol} [{name}])",
39
- f"OBV ({symbol} [{name}])",
40
- f"Industry ({symbol} [{name}])",
44
+ f"ATR ({symbol} [{name}])",
45
+ f"ADOSC ({symbol} [{name}])",
41
46
  ),
42
47
  )
43
48
  # Row 1: Candlestick + SMAs
@@ -114,28 +119,15 @@ def plot(
114
119
  col=1,
115
120
  )
116
121
  fig.add_trace(
117
- go.Scatter(x=data.index, y=data.OBV, name="OBV", mode="lines"),
122
+ go.Scatter(x=data.index, y=data.ATR, name="ATR", mode="lines"),
118
123
  row=6,
119
124
  col=1,
120
125
  )
121
126
  fig.add_trace(
122
127
  go.Scatter(x=data.index, y=data.ADOSC, name="ADOSC", mode="lines"),
123
- row=6,
128
+ row=7,
124
129
  col=1,
125
130
  )
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
131
 
140
132
  if dates is not None and dates:
141
133
  for date in dates:
@@ -166,5 +158,23 @@ def plot(
166
158
  fig.add_hline(y=70, line_dash="dash", line_color="red", row=3, col=1)
167
159
  fig.add_hline(y=30, line_dash="dash", line_color="green", row=3, col=1)
168
160
  fig.add_hline(y=25, line_dash="dash", line_color="red", row=5, col=1)
161
+ fig.add_hline(
162
+ y=supports.support.value,
163
+ line_dash="dash",
164
+ line_color="rgba(26, 188, 156, 1)", # teal, fully opaque
165
+ annotation_text=f"Support ({supports.support.value:.2f})",
166
+ line_width=0.75,
167
+ row=1,
168
+ col=1,
169
+ )
170
+ fig.add_hline(
171
+ y=supports.resistance.value,
172
+ line_dash="dash",
173
+ line_color="rgba(230, 126, 34, 1)", # orange, fully opaque
174
+ annotation_text=f"Resistance ({supports.resistance.value:.2f})",
175
+ line_width=0.75,
176
+ row=1,
177
+ col=1,
178
+ )
169
179
 
170
180
  return fig
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "bullishpy"
3
- version = "0.66.0"
3
+ version = "0.67.0"
4
4
  description = ""
5
5
  authors = ["aan <andoludovic.andriamamonjy@gmail.com>"]
6
6
  readme = "README.md"
File without changes
File without changes
File without changes