bullishpy 0.12.0__py3-none-any.whl → 0.13.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.
@@ -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 ###
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: bullishpy
3
- Version: 0.12.0
3
+ Version: 0.13.0
4
4
  Summary:
5
5
  Author: aan
6
6
  Author-email: andoludovic.andriamamonjy@gmail.com
@@ -2,9 +2,9 @@ bullish/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  bullish/analysis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  bullish/analysis/analysis.py,sha256=v5MFGhQUpMwGAVE0dg2293ldRSgQmqy5CiF8MQFTibM,19165
4
4
  bullish/analysis/filter.py,sha256=S8TuxoTAUY0U8ARPjNHE0tSSE_ToWkfZazAgnfgswk4,18136
5
- bullish/analysis/functions.py,sha256=KKz_0C7maQmcGu2tGwZvioxzmh-JcB-YNpPQGjyyheA,13825
6
- bullish/analysis/indicators.py,sha256=hZgzTq-80XPP6x7dXGhxd-Zzgra-6D-g3pVxUBYOW44,20167
7
- bullish/analysis/predefined_filters.py,sha256=3O244FCZkbnTSaReh6w6cwEIXdLzQZmbkBS0uRW-Y0M,12391
5
+ bullish/analysis/functions.py,sha256=DYqx5ZGR-zjCwDwhDQJqRFH8LpmzvFEcfItojbIcddU,14699
6
+ bullish/analysis/indicators.py,sha256=JXqXsRDn-hiXcrBqqzJ3-xxANAherwaCZy38XykjJBA,20726
7
+ bullish/analysis/predefined_filters.py,sha256=MvIuGug-RWO7QtWNEtOFROf-sY8IXu444g174cE-5m0,15200
8
8
  bullish/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  bullish/app/app.py,sha256=E0H78LOODl1H6s308jXpQGTUoFPoLOJkPBXOLQGLCeA,13331
10
10
  bullish/cli.py,sha256=uYLZmGDAolZKWzduZ58bP-xul1adg0oKfeUQtZMXTvA,1958
@@ -20,6 +20,8 @@ bullish/database/alembic/versions/17e51420e7ad_.py,sha256=xeiVIm1YUZb08opE9rocHZ
20
20
  bullish/database/alembic/versions/49c83f9eb5ac_.py,sha256=kCBItp7KmqpJ03roy5ikQjhefZia1oKgfZwournQDq8,3890
21
21
  bullish/database/alembic/versions/4b0a2f40b7d3_.py,sha256=G0K7w7pOPYjPZkXTB8LWhxoxuWBPcPwOfnubTBtdeEY,1827
22
22
  bullish/database/alembic/versions/73564b60fe24_.py,sha256=MTlDRDNHj3E9gK7IMeAzv2UxxxYtWiu3gI_9xTLE-wg,1008
23
+ bullish/database/alembic/versions/b76079e9845f_.py,sha256=W8eeTABjI9tT1dp3hlK7g7tiKqDhmA8AoUX9Sw-ykLI,1165
24
+ bullish/database/alembic/versions/bf6b86dd5463_.py,sha256=fKB8knCprGmiL6AEyFdhybVmB7QX_W4MPFF9sPzUrSM,1094
23
25
  bullish/database/alembic/versions/d663166c531d_.py,sha256=U92l6QXqPniAYrPeu2Bt77ReDbXveLj4aGXtgd806JY,1915
24
26
  bullish/database/alembic/versions/ee5baabb35f8_.py,sha256=nBMEY-_C8AsSXVPyaDdUkwrFFo2gxShzJhmrjejDwtc,1632
25
27
  bullish/database/alembic/versions/fc191121f522_.py,sha256=0sstF6TpAJ09-Mt-Vek9SdSWksvi4C58a5D92rBtuY8,1894
@@ -40,7 +42,7 @@ bullish/jobs/models.py,sha256=ndrGTMP08S57yGLGEG9TQt8Uw2slc4HvbG-TZtEEuN0,744
40
42
  bullish/jobs/tasks.py,sha256=V_b0c8_GQC0-KIxaHDlLFhtkclQJOsck0gXaW6OlC_w,3055
41
43
  bullish/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
44
  bullish/utils/checks.py,sha256=Va10_xDVVnxYkOD2hafvyQ-TFV8FQpOkr4huJ7XgpDM,2188
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,,
45
+ bullishpy-0.13.0.dist-info/METADATA,sha256=5aWLV_c8a5ys2AjiyEGgx-CnRK1Qs3EBXdCbu77wSIw,784
46
+ bullishpy-0.13.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
47
+ bullishpy-0.13.0.dist-info/entry_points.txt,sha256=eaPpmL6vmSBFo0FBtwibCXGqAW4LFJ83whJzT1VjD-0,43
48
+ bullishpy-0.13.0.dist-info/RECORD,,