bullishpy 0.7.0__tar.gz → 0.8.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.7.0 → bullishpy-0.8.0}/PKG-INFO +1 -1
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/analysis/functions.py +20 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/analysis/indicators.py +34 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/analysis/predefined_filters.py +65 -1
- bullishpy-0.8.0/bullish/database/alembic/versions/fc191121f522_.py +57 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/pyproject.toml +1 -1
- {bullishpy-0.7.0 → bullishpy-0.8.0}/README.md +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/__init__.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/analysis/__init__.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/analysis/analysis.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/analysis/filter.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/app/__init__.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/app/app.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/cli.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/database/__init__.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/database/alembic/README +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/database/alembic/alembic.ini +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/database/alembic/env.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/database/alembic/script.py.mako +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/database/alembic/versions/037dbd721317_.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/database/alembic/versions/08ac1116e055_.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/database/alembic/versions/11d35a452b40_.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/database/alembic/versions/49c83f9eb5ac_.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/database/alembic/versions/4b0a2f40b7d3_.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/database/alembic/versions/73564b60fe24_.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/database/alembic/versions/ee5baabb35f8_.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/database/crud.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/database/schemas.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/database/scripts/create_revision.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/database/scripts/stamp.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/database/scripts/upgrade.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/database/settings.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/exceptions.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/figures/__init__.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/figures/figures.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/interface/__init__.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/interface/interface.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/jobs/__init__.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/jobs/app.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/jobs/models.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/jobs/tasks.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/utils/__init__.py +0 -0
- {bullishpy-0.7.0 → bullishpy-0.8.0}/bullish/utils/checks.py +0 -0
|
@@ -244,6 +244,16 @@ def compute_patterns(data: pd.DataFrame) -> pd.DataFrame:
|
|
|
244
244
|
return results
|
|
245
245
|
|
|
246
246
|
|
|
247
|
+
def compute_price(data: pd.DataFrame) -> pd.DataFrame:
|
|
248
|
+
results = pd.DataFrame(index=data.index)
|
|
249
|
+
results["200_DAY_HIGH"] = data.close.rolling(window=200).max()
|
|
250
|
+
results["200_DAY_LOW"] = data.close.rolling(window=200).min()
|
|
251
|
+
results["20_DAY_HIGH"] = data.close.rolling(window=20).max()
|
|
252
|
+
results["20_DAY_LOW"] = data.close.rolling(window=20).min()
|
|
253
|
+
results["LAST_PRICE"] = data.close
|
|
254
|
+
return results
|
|
255
|
+
|
|
256
|
+
|
|
247
257
|
class IndicatorFunction(BaseModel):
|
|
248
258
|
expected_columns: list[str]
|
|
249
259
|
functions: list[Callable[[pd.DataFrame], pd.DataFrame]]
|
|
@@ -330,6 +340,16 @@ TRANGE = IndicatorFunction(
|
|
|
330
340
|
expected_columns=["TRANGE"],
|
|
331
341
|
functions=[compute_trange, compute_pandas_ta_trange],
|
|
332
342
|
)
|
|
343
|
+
PRICE = IndicatorFunction(
|
|
344
|
+
expected_columns=[
|
|
345
|
+
"200_DAY_HIGH",
|
|
346
|
+
"200_DAY_LOW",
|
|
347
|
+
"20_DAY_HIGH",
|
|
348
|
+
"20_DAY_LOW",
|
|
349
|
+
"LAST_PRICE",
|
|
350
|
+
],
|
|
351
|
+
functions=[compute_price],
|
|
352
|
+
)
|
|
333
353
|
|
|
334
354
|
|
|
335
355
|
def add_indicators(data: pd.DataFrame) -> pd.DataFrame:
|
|
@@ -18,6 +18,7 @@ from bullish.analysis.functions import (
|
|
|
18
18
|
CANDLESTOCK_PATTERNS,
|
|
19
19
|
SMA,
|
|
20
20
|
ADOSC,
|
|
21
|
+
PRICE,
|
|
21
22
|
)
|
|
22
23
|
|
|
23
24
|
logger = logging.getLogger(__name__)
|
|
@@ -186,6 +187,13 @@ def indicators_factory() -> List[Indicator]:
|
|
|
186
187
|
type=Optional[date],
|
|
187
188
|
function=lambda d: d[(d.RSI < 100) & (d.RSI > 70)].index[-1],
|
|
188
189
|
),
|
|
190
|
+
Signal(
|
|
191
|
+
name="RSI_NEUTRAL",
|
|
192
|
+
description="RSI Neutral Signal",
|
|
193
|
+
type_info="Overbought",
|
|
194
|
+
type=Optional[date],
|
|
195
|
+
function=lambda d: d[(d.RSI < 60) & (d.RSI > 40)].index[-1],
|
|
196
|
+
),
|
|
189
197
|
],
|
|
190
198
|
),
|
|
191
199
|
Indicator(
|
|
@@ -254,6 +262,32 @@ def indicators_factory() -> List[Indicator]:
|
|
|
254
262
|
),
|
|
255
263
|
],
|
|
256
264
|
),
|
|
265
|
+
Indicator(
|
|
266
|
+
name="PRICE",
|
|
267
|
+
description="Price based indicators",
|
|
268
|
+
expected_columns=PRICE.expected_columns,
|
|
269
|
+
function=PRICE.call,
|
|
270
|
+
signals=[
|
|
271
|
+
Signal(
|
|
272
|
+
name="LOWER_THAN_200_DAY_HIGH",
|
|
273
|
+
description="Current price is lower than the 200-day high",
|
|
274
|
+
type_info="Oversold",
|
|
275
|
+
type=Optional[date],
|
|
276
|
+
function=lambda d: d[0.6 * d["200_DAY_HIGH"] > d.LAST_PRICE].index[
|
|
277
|
+
-1
|
|
278
|
+
],
|
|
279
|
+
),
|
|
280
|
+
Signal(
|
|
281
|
+
name="LOWER_THAN_20_DAY_HIGH",
|
|
282
|
+
description="Current price is lower than the 20-day high",
|
|
283
|
+
type_info="Oversold",
|
|
284
|
+
type=Optional[date],
|
|
285
|
+
function=lambda d: d[0.6 * d["20_DAY_HIGH"] > d.LAST_PRICE].index[
|
|
286
|
+
-1
|
|
287
|
+
],
|
|
288
|
+
),
|
|
289
|
+
],
|
|
290
|
+
),
|
|
257
291
|
Indicator(
|
|
258
292
|
name="ROC",
|
|
259
293
|
description="Rate Of Change",
|
|
@@ -69,9 +69,73 @@ MICRO_CAP_EVENT_SPECULATION = NamedFilterQuery(
|
|
|
69
69
|
market_capitalization=[0, 5e8],
|
|
70
70
|
)
|
|
71
71
|
|
|
72
|
+
MOMENTUM_BREAKOUT_HUNTER = NamedFilterQuery(
|
|
73
|
+
name="Momentum Breakout Hunter",
|
|
74
|
+
description="A confluence of medium-term (50/200 MA) and "
|
|
75
|
+
"shorter oscillators suggests fresh upside momentum with fuel left.",
|
|
76
|
+
income=[
|
|
77
|
+
"positive_operating_income",
|
|
78
|
+
"positive_net_income",
|
|
79
|
+
],
|
|
80
|
+
cash_flow=["positive_free_cash_flow"],
|
|
81
|
+
golden_cross=[
|
|
82
|
+
datetime.date.today() - datetime.timedelta(days=5),
|
|
83
|
+
datetime.date.today(),
|
|
84
|
+
],
|
|
85
|
+
adx_14_long=[
|
|
86
|
+
datetime.date.today() - datetime.timedelta(days=5),
|
|
87
|
+
datetime.date.today(),
|
|
88
|
+
],
|
|
89
|
+
rate_of_change_30=[0, 100],
|
|
90
|
+
rsi_neutral=[
|
|
91
|
+
datetime.date.today() - datetime.timedelta(days=5),
|
|
92
|
+
datetime.date.today(),
|
|
93
|
+
],
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
DEEP_VALUE_PLUS_CATALYST = NamedFilterQuery(
|
|
97
|
+
name="Deep-Value Plus Catalyst",
|
|
98
|
+
description="Seeks beaten-down names that just printed a bullish "
|
|
99
|
+
"candle and early accumulation signals—often the first leg of a bottom.",
|
|
100
|
+
income=[
|
|
101
|
+
"positive_operating_income",
|
|
102
|
+
"positive_net_income",
|
|
103
|
+
],
|
|
104
|
+
lower_than_200_day_high=[
|
|
105
|
+
datetime.date.today() - datetime.timedelta(days=5),
|
|
106
|
+
datetime.date.today(),
|
|
107
|
+
],
|
|
108
|
+
rate_of_change_30=[3, 100],
|
|
109
|
+
rsi_bullish_crossover=[
|
|
110
|
+
datetime.date.today() - datetime.timedelta(days=5),
|
|
111
|
+
datetime.date.today(),
|
|
112
|
+
],
|
|
113
|
+
)
|
|
114
|
+
END_OF_TREND_REVERSAL = NamedFilterQuery(
|
|
115
|
+
name="End of trend reversal",
|
|
116
|
+
description="Layers long-term MA breach with momentum exhaustion and a "
|
|
117
|
+
"bullish candle—classic setup for mean-reversion traders.",
|
|
118
|
+
death_cross=[
|
|
119
|
+
datetime.date.today() - datetime.timedelta(days=5),
|
|
120
|
+
datetime.date.today(),
|
|
121
|
+
],
|
|
122
|
+
rsi_oversold=[
|
|
123
|
+
datetime.date.today() - datetime.timedelta(days=5),
|
|
124
|
+
datetime.date.today(),
|
|
125
|
+
],
|
|
126
|
+
candlesticks=["cdlmorningstart", "cdlabandonedbaby", "cdl3whitesoldiers"],
|
|
127
|
+
)
|
|
128
|
+
|
|
72
129
|
|
|
73
130
|
def predefined_filters() -> list[NamedFilterQuery]:
|
|
74
|
-
return [
|
|
131
|
+
return [
|
|
132
|
+
STRONG_FUNDAMENTALS,
|
|
133
|
+
GOOD_FUNDAMENTALS,
|
|
134
|
+
MICRO_CAP_EVENT_SPECULATION,
|
|
135
|
+
MOMENTUM_BREAKOUT_HUNTER,
|
|
136
|
+
DEEP_VALUE_PLUS_CATALYST,
|
|
137
|
+
END_OF_TREND_REVERSAL,
|
|
138
|
+
]
|
|
75
139
|
|
|
76
140
|
|
|
77
141
|
class PredefinedFilters(BaseModel):
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
Revision ID: fc191121f522
|
|
4
|
+
Revises: 49c83f9eb5ac
|
|
5
|
+
Create Date: 2025-07-06 17:33:02.231374
|
|
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 = "fc191121f522"
|
|
17
|
+
down_revision: Union[str, None] = "49c83f9eb5ac"
|
|
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(
|
|
26
|
+
sa.Column("lower_than_200_day_high", sa.Date(), nullable=True)
|
|
27
|
+
)
|
|
28
|
+
batch_op.add_column(
|
|
29
|
+
sa.Column("lower_than_20_day_high", sa.Date(), nullable=True)
|
|
30
|
+
)
|
|
31
|
+
batch_op.add_column(sa.Column("rsi_neutral", sa.Date(), nullable=True))
|
|
32
|
+
batch_op.create_index(
|
|
33
|
+
"ix_analysis_lower_than_200_day_high",
|
|
34
|
+
["lower_than_200_day_high"],
|
|
35
|
+
unique=False,
|
|
36
|
+
)
|
|
37
|
+
batch_op.create_index(
|
|
38
|
+
"ix_analysis_lower_than_20_day_high",
|
|
39
|
+
["lower_than_20_day_high"],
|
|
40
|
+
unique=False,
|
|
41
|
+
)
|
|
42
|
+
batch_op.create_index("ix_analysis_rsi_neutral", ["rsi_neutral"], unique=False)
|
|
43
|
+
|
|
44
|
+
# ### end Alembic commands ###
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def downgrade() -> None:
|
|
48
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
49
|
+
with op.batch_alter_table("analysis", schema=None) as batch_op:
|
|
50
|
+
batch_op.drop_index("ix_analysis_rsi_neutral")
|
|
51
|
+
batch_op.drop_index("ix_analysis_lower_than_20_day_high")
|
|
52
|
+
batch_op.drop_index("ix_analysis_lower_than_200_day_high")
|
|
53
|
+
batch_op.drop_column("rsi_neutral")
|
|
54
|
+
batch_op.drop_column("lower_than_20_day_high")
|
|
55
|
+
batch_op.drop_column("lower_than_200_day_high")
|
|
56
|
+
|
|
57
|
+
# ### 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
|