bullishpy 0.12.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.
- {bullishpy-0.12.0 → bullishpy-0.13.0}/PKG-INFO +1 -1
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/analysis/functions.py +27 -2
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/analysis/indicators.py +16 -1
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/analysis/predefined_filters.py +74 -0
- bullishpy-0.13.0/bullish/database/alembic/versions/b76079e9845f_.py +40 -0
- bullishpy-0.13.0/bullish/database/alembic/versions/bf6b86dd5463_.py +38 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/pyproject.toml +1 -1
- {bullishpy-0.12.0 → bullishpy-0.13.0}/README.md +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/__init__.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/analysis/__init__.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/analysis/analysis.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/analysis/filter.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/app/__init__.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/app/app.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/cli.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/database/__init__.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/database/alembic/README +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/database/alembic/alembic.ini +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/database/alembic/env.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/database/alembic/script.py.mako +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/database/alembic/versions/037dbd721317_.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/database/alembic/versions/08ac1116e055_.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/database/alembic/versions/11d35a452b40_.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/database/alembic/versions/17e51420e7ad_.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/database/alembic/versions/49c83f9eb5ac_.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/database/alembic/versions/4b0a2f40b7d3_.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/database/alembic/versions/73564b60fe24_.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/database/alembic/versions/d663166c531d_.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/database/alembic/versions/ee5baabb35f8_.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/database/alembic/versions/fc191121f522_.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/database/crud.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/database/schemas.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/database/scripts/create_revision.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/database/scripts/stamp.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/database/scripts/upgrade.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/database/settings.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/exceptions.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/figures/__init__.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/figures/figures.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/interface/__init__.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/interface/interface.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/jobs/__init__.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/jobs/app.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/jobs/models.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/jobs/tasks.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/utils/__init__.py +0 -0
- {bullishpy-0.12.0 → bullishpy-0.13.0}/bullish/utils/checks.py +0 -0
|
@@ -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=
|
|
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 ###
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|