bullishpy 0.13.0__py3-none-any.whl → 0.15.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.
- bullish/analysis/analysis.py +35 -3
- bullish/analysis/constants.py +403 -0
- bullish/analysis/filter.py +2 -405
- bullish/analysis/functions.py +18 -28
- bullish/analysis/indicators.py +160 -85
- bullish/analysis/industry_views.py +201 -0
- bullish/analysis/predefined_filters.py +81 -248
- bullish/app/app.py +5 -1
- bullish/database/alembic/versions/040b15fba458_.py +61 -0
- bullish/database/alembic/versions/3e1a14c41916_.py +51 -0
- bullish/database/alembic/versions/5b10ee7604c1_.py +44 -0
- bullish/database/alembic/versions/ec25c8fa449f_.py +63 -0
- bullish/database/crud.py +95 -4
- bullish/database/schemas.py +26 -0
- bullish/figures/figures.py +28 -5
- bullish/interface/interface.py +38 -0
- {bullishpy-0.13.0.dist-info → bullishpy-0.15.0.dist-info}/METADATA +3 -2
- {bullishpy-0.13.0.dist-info → bullishpy-0.15.0.dist-info}/RECORD +20 -14
- {bullishpy-0.13.0.dist-info → bullishpy-0.15.0.dist-info}/WHEEL +0 -0
- {bullishpy-0.13.0.dist-info → bullishpy-0.15.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from typing import (
|
|
3
|
+
Optional,
|
|
4
|
+
Any,
|
|
5
|
+
Annotated,
|
|
6
|
+
Literal,
|
|
7
|
+
Dict,
|
|
8
|
+
List,
|
|
9
|
+
TYPE_CHECKING,
|
|
10
|
+
get_args,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import pandas as pd
|
|
15
|
+
from bearish.models.base import Ticker # type: ignore
|
|
16
|
+
from bearish.models.price.prices import Prices # type: ignore
|
|
17
|
+
from bearish.models.query.query import AssetQuery, Symbols # type: ignore
|
|
18
|
+
from pydantic import BaseModel, BeforeValidator, Field, model_validator
|
|
19
|
+
|
|
20
|
+
from bullish.analysis.constants import Industry, IndustryGroup, Sector, Country
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from bullish.database.crud import BullishDb
|
|
24
|
+
|
|
25
|
+
Type = Literal["Mean"]
|
|
26
|
+
|
|
27
|
+
FUNCTIONS = {"Mean": np.mean}
|
|
28
|
+
BASELINE_DATE = datetime.date.today() - datetime.timedelta(days=60)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def compute_normalized_close(close_: pd.Series) -> pd.Series:
|
|
32
|
+
close = close_.copy()
|
|
33
|
+
close.index = close.index.tz_localize(None) # type: ignore
|
|
34
|
+
closest_ts = close.index[
|
|
35
|
+
close.index.get_indexer([BASELINE_DATE], method="nearest")[0]
|
|
36
|
+
]
|
|
37
|
+
normalized_close = (close / close.loc[closest_ts]).rename("normalized_close")
|
|
38
|
+
normalized_close.index = close_.index
|
|
39
|
+
return normalized_close # type: ignore
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_industry_comparison_data(
|
|
43
|
+
bullish_db: "BullishDb",
|
|
44
|
+
symbol_data: pd.DataFrame,
|
|
45
|
+
type: Type,
|
|
46
|
+
industry: Industry,
|
|
47
|
+
country: Country,
|
|
48
|
+
) -> pd.DataFrame:
|
|
49
|
+
try:
|
|
50
|
+
views = bullish_db.read_returns(type, industry, country)
|
|
51
|
+
industry_data = IndustryViews.from_views(views).to_dataframe()
|
|
52
|
+
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:
|
|
56
|
+
return pd.DataFrame()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class PricesReturns(Prices): # type: ignore
|
|
60
|
+
|
|
61
|
+
def returns(self) -> pd.DataFrame:
|
|
62
|
+
try:
|
|
63
|
+
data = self.to_dataframe()
|
|
64
|
+
data["simple_return"] = data.close.pct_change() * 100
|
|
65
|
+
data["log_return"] = (data.close / data.close.shift(1)).apply(np.log) * 100
|
|
66
|
+
data["normalized_close"] = compute_normalized_close(data.close)
|
|
67
|
+
return data[["simple_return", "log_return", "normalized_close"]] # type: ignore
|
|
68
|
+
except Exception:
|
|
69
|
+
return pd.DataFrame(
|
|
70
|
+
columns=["simple_return", "log_return", "normalized_close"]
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def to_float(value: Any) -> Optional[float]:
|
|
75
|
+
if value == "None":
|
|
76
|
+
return None
|
|
77
|
+
if value is None:
|
|
78
|
+
return None
|
|
79
|
+
if isinstance(value, str):
|
|
80
|
+
try:
|
|
81
|
+
return float(value)
|
|
82
|
+
except ValueError:
|
|
83
|
+
return None
|
|
84
|
+
return float(value)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class Basedate(BaseModel):
|
|
88
|
+
date: datetime.date
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class KPI(BaseModel):
|
|
92
|
+
simple_return: Annotated[float, BeforeValidator(to_float), Field(None)]
|
|
93
|
+
log_return: Annotated[float, BeforeValidator(to_float), Field(None)]
|
|
94
|
+
normalized_close: Annotated[float, BeforeValidator(to_float), Field(None)]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class BaseIndustryView(Basedate, KPI): ...
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class IndustryView(BaseIndustryView):
|
|
101
|
+
created_at: datetime.date
|
|
102
|
+
country: Country
|
|
103
|
+
industry: Industry
|
|
104
|
+
industry_group: Optional[IndustryGroup] = None
|
|
105
|
+
sector: Optional[Sector] = None
|
|
106
|
+
type: Type
|
|
107
|
+
|
|
108
|
+
@model_validator(mode="before")
|
|
109
|
+
def _validate(cls, values: Dict[str, Any]) -> Dict[str, Any]: # noqa: N805
|
|
110
|
+
created_at = datetime.date.today()
|
|
111
|
+
current_date = values.get("date", created_at)
|
|
112
|
+
return (
|
|
113
|
+
{"date": current_date}
|
|
114
|
+
| values
|
|
115
|
+
| {
|
|
116
|
+
"created_at": created_at,
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
@classmethod
|
|
121
|
+
def from_data(
|
|
122
|
+
cls,
|
|
123
|
+
data: pd.DataFrame,
|
|
124
|
+
function_name: Type,
|
|
125
|
+
industry: Industry,
|
|
126
|
+
country: Country,
|
|
127
|
+
) -> List["IndustryView"]:
|
|
128
|
+
function = FUNCTIONS[function_name]
|
|
129
|
+
data_ = []
|
|
130
|
+
for field in KPI.model_fields:
|
|
131
|
+
|
|
132
|
+
data__ = (
|
|
133
|
+
data[field].apply(function, axis=1).rename(field)
|
|
134
|
+
if data[[field]].shape[1] > 1
|
|
135
|
+
else data[field]
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
data_.append(data__)
|
|
139
|
+
|
|
140
|
+
data_final = pd.concat(data_, axis=1)
|
|
141
|
+
data_final["date"] = data_final.index
|
|
142
|
+
return [
|
|
143
|
+
cls.model_validate(
|
|
144
|
+
r | {"industry": industry, "type": function_name, "country": country}
|
|
145
|
+
)
|
|
146
|
+
for r in data_final.to_dict(orient="records")
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def from_db(
|
|
151
|
+
cls, bullish: "BullishDb", industry: Industry, country: Country
|
|
152
|
+
) -> List["IndustryView"]:
|
|
153
|
+
returns = []
|
|
154
|
+
symbols = bullish.read_industry_symbols(industries=[industry], country=country)
|
|
155
|
+
query = AssetQuery(
|
|
156
|
+
symbols=Symbols(equities=[Ticker(symbol=s) for s in symbols])
|
|
157
|
+
)
|
|
158
|
+
data = bullish.read_series(query, months=6)
|
|
159
|
+
raw_data = [
|
|
160
|
+
PricesReturns(prices=[d for d in data if d.symbol == s]).returns()
|
|
161
|
+
for s in symbols
|
|
162
|
+
]
|
|
163
|
+
raw_data = [r for r in raw_data if not r.empty]
|
|
164
|
+
|
|
165
|
+
if raw_data:
|
|
166
|
+
data_ = pd.concat(raw_data, axis=1)
|
|
167
|
+
for function_name in FUNCTIONS:
|
|
168
|
+
returns.extend(cls.from_data(data_, function_name, industry, country)) # type: ignore
|
|
169
|
+
return returns
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class IndustryViews(BaseModel):
|
|
173
|
+
views: List[IndustryView]
|
|
174
|
+
|
|
175
|
+
def to_dataframe(self) -> pd.DataFrame:
|
|
176
|
+
data = pd.DataFrame.from_records(
|
|
177
|
+
[
|
|
178
|
+
p.model_dump(include=set(BaseIndustryView.model_fields))
|
|
179
|
+
for p in self.views
|
|
180
|
+
]
|
|
181
|
+
)
|
|
182
|
+
if data.empty:
|
|
183
|
+
return data
|
|
184
|
+
data = data.set_index("date", inplace=False)
|
|
185
|
+
data = data.sort_index(inplace=False)
|
|
186
|
+
|
|
187
|
+
data.index = pd.to_datetime(data.index, utc=True)
|
|
188
|
+
data = data[~data.index.duplicated(keep="first")]
|
|
189
|
+
return data
|
|
190
|
+
|
|
191
|
+
@classmethod
|
|
192
|
+
def from_views(cls, views: List[IndustryView]) -> "IndustryViews":
|
|
193
|
+
return cls(views=views)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def compute_industry_view(bullish: "BullishDb") -> None:
|
|
197
|
+
for country in get_args(Country):
|
|
198
|
+
for industry in get_args(Industry):
|
|
199
|
+
returns = IndustryView.from_db(bullish, industry, country)
|
|
200
|
+
if returns:
|
|
201
|
+
bullish.write_returns(returns)
|
|
@@ -4,8 +4,9 @@ from typing import Dict, Any, Optional
|
|
|
4
4
|
from bullish.analysis.filter import FilterQuery
|
|
5
5
|
from pydantic import BaseModel, Field
|
|
6
6
|
|
|
7
|
+
|
|
7
8
|
DATE_THRESHOLD = [
|
|
8
|
-
datetime.date.today() - datetime.timedelta(days=
|
|
9
|
+
datetime.date.today() - datetime.timedelta(days=7),
|
|
9
10
|
datetime.date.today(),
|
|
10
11
|
]
|
|
11
12
|
|
|
@@ -60,177 +61,6 @@ GOOD_FUNDAMENTALS = NamedFilterQuery(
|
|
|
60
61
|
rsi_bullish_crossover_30=DATE_THRESHOLD,
|
|
61
62
|
)
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
SHOOTING_STARS = NamedFilterQuery(
|
|
65
|
-
name="Shooting stars",
|
|
66
|
-
cash_flow=["positive_free_cash_flow"],
|
|
67
|
-
properties=["operating_cash_flow_is_higher_than_net_income"],
|
|
68
|
-
market_capitalization=[1e9, 1e12], # 1 billion to 1 trillion
|
|
69
|
-
order_by_desc="median_yearly_growth",
|
|
70
|
-
order_by_asc="last_price",
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
RSI_CROSSOVER_TECH = NamedFilterQuery(
|
|
74
|
-
name="RSI cross-over",
|
|
75
|
-
cash_flow=["positive_free_cash_flow"],
|
|
76
|
-
properties=["operating_cash_flow_is_higher_than_net_income"],
|
|
77
|
-
return_after_rsi_crossover_45_period_90=[0.0, 100],
|
|
78
|
-
rsi_bullish_crossover_45=DATE_THRESHOLD,
|
|
79
|
-
market_capitalization=[5e8, 1e11], # 1 billion to 1 trillion
|
|
80
|
-
order_by_desc="market_capitalization",
|
|
81
|
-
country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
|
|
82
|
-
industry=[
|
|
83
|
-
"Semiconductors",
|
|
84
|
-
"Software - Application",
|
|
85
|
-
"Software - Infrastructure",
|
|
86
|
-
"Biotechnology",
|
|
87
|
-
"Diagnostics & Research",
|
|
88
|
-
"Medical Devices",
|
|
89
|
-
"Health Information Services",
|
|
90
|
-
"Internet Retail",
|
|
91
|
-
"Electronic Gaming & Multimedia",
|
|
92
|
-
"Internet Content & Information",
|
|
93
|
-
"Solar",
|
|
94
|
-
"Information Technology Services",
|
|
95
|
-
"Scientific & Technical Instruments",
|
|
96
|
-
"Semiconductor Equipment & Materials",
|
|
97
|
-
"Diagnostics & Research",
|
|
98
|
-
],
|
|
99
|
-
)
|
|
100
|
-
RSI_CROSSOVER_TECH_PE = NamedFilterQuery(
|
|
101
|
-
name="RSI cross-over P/E",
|
|
102
|
-
cash_flow=["positive_free_cash_flow"],
|
|
103
|
-
properties=["operating_cash_flow_is_higher_than_net_income"],
|
|
104
|
-
price_per_earning_ratio=[5, 30], # P/E ratio between 10 and 100
|
|
105
|
-
rsi_bullish_crossover_45=DATE_THRESHOLD,
|
|
106
|
-
market_capitalization=[5e8, 1e12], # 1 billion to 1 trillion
|
|
107
|
-
order_by_desc="market_capitalization",
|
|
108
|
-
country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
|
|
109
|
-
industry=[
|
|
110
|
-
"Semiconductors",
|
|
111
|
-
"Software - Application",
|
|
112
|
-
"Software - Infrastructure",
|
|
113
|
-
"Biotechnology",
|
|
114
|
-
"Diagnostics & Research",
|
|
115
|
-
"Medical Devices",
|
|
116
|
-
"Health Information Services",
|
|
117
|
-
"Internet Retail",
|
|
118
|
-
"Electronic Gaming & Multimedia",
|
|
119
|
-
"Internet Content & Information",
|
|
120
|
-
"Solar",
|
|
121
|
-
"Information Technology Services",
|
|
122
|
-
"Scientific & Technical Instruments",
|
|
123
|
-
"Semiconductor Equipment & Materials",
|
|
124
|
-
"Diagnostics & Research",
|
|
125
|
-
],
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
MICRO_CAP_EVENT_SPECULATION = NamedFilterQuery(
|
|
129
|
-
name="Micro-Cap Event Speculation",
|
|
130
|
-
description="seeks tiny names where unusual volume and price gaps hint at "
|
|
131
|
-
"pending corporate events (patent win, FDA news, buy-out rumors).",
|
|
132
|
-
positive_adosc_20_day_breakout=DATE_THRESHOLD,
|
|
133
|
-
rate_of_change_30=[20, 100], # 10% to 50% in the last 30 days
|
|
134
|
-
market_capitalization=[0, 5e8],
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
MOMENTUM_BREAKOUT_HUNTER = NamedFilterQuery(
|
|
138
|
-
name="Momentum Breakout Hunter",
|
|
139
|
-
description="A confluence of medium-term (50/200 MA) and "
|
|
140
|
-
"shorter oscillators suggests fresh upside momentum with fuel left.",
|
|
141
|
-
income=[
|
|
142
|
-
"positive_operating_income",
|
|
143
|
-
"positive_net_income",
|
|
144
|
-
],
|
|
145
|
-
cash_flow=["positive_free_cash_flow"],
|
|
146
|
-
golden_cross=DATE_THRESHOLD,
|
|
147
|
-
adx_14_long=DATE_THRESHOLD,
|
|
148
|
-
rate_of_change_30=[0, 100],
|
|
149
|
-
rsi_neutral=DATE_THRESHOLD,
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
DEEP_VALUE_PLUS_CATALYST = NamedFilterQuery(
|
|
153
|
-
name="Deep-Value Plus Catalyst",
|
|
154
|
-
description="Seeks beaten-down names that just printed a bullish "
|
|
155
|
-
"candle and early accumulation signals—often the first leg of a bottom.",
|
|
156
|
-
income=[
|
|
157
|
-
"positive_operating_income",
|
|
158
|
-
"positive_net_income",
|
|
159
|
-
],
|
|
160
|
-
lower_than_200_day_high=DATE_THRESHOLD,
|
|
161
|
-
rate_of_change_30=[3, 100],
|
|
162
|
-
rsi_bullish_crossover_30=DATE_THRESHOLD,
|
|
163
|
-
)
|
|
164
|
-
END_OF_TREND_REVERSAL = NamedFilterQuery(
|
|
165
|
-
name="End of trend reversal",
|
|
166
|
-
description="Layers long-term MA breach with momentum exhaustion and a "
|
|
167
|
-
"bullish candle—classic setup for mean-reversion traders.",
|
|
168
|
-
death_cross=DATE_THRESHOLD,
|
|
169
|
-
rsi_oversold=DATE_THRESHOLD,
|
|
170
|
-
candlesticks=["cdlmorningstart", "cdlabandonedbaby", "cdl3whitesoldiers"],
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
HIGH_QUALITY_CASH_GENERATOR = NamedFilterQuery(
|
|
174
|
-
name="High Quality Cash Generator",
|
|
175
|
-
description="This quartet isolates companies that are profitable, cash-rich, and disciplined with leverage. "
|
|
176
|
-
"Ideal first pass for “quality” or “compounder” "
|
|
177
|
-
"portfolios where downside protection matters as much as upside.",
|
|
178
|
-
income=[
|
|
179
|
-
"positive_net_income",
|
|
180
|
-
],
|
|
181
|
-
cash_flow=["positive_free_cash_flow"],
|
|
182
|
-
properties=[
|
|
183
|
-
"operating_cash_flow_is_higher_than_net_income",
|
|
184
|
-
"positive_return_on_equity",
|
|
185
|
-
"positive_return_on_assets",
|
|
186
|
-
"positive_debt_to_equity",
|
|
187
|
-
],
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
EARNINGS_ACCELERATION_TREND_CONFIRMATION = NamedFilterQuery(
|
|
191
|
-
name="Earnings Acceleration Trend Confirmation",
|
|
192
|
-
description="Pairs fundamental acceleration with momentum confirmation. Research shows this “double positive” "
|
|
193
|
-
"outperforms simple momentum because it filters out purely sentiment-driven rallies.",
|
|
194
|
-
income=[
|
|
195
|
-
"growing_operating_income",
|
|
196
|
-
"positive_net_income",
|
|
197
|
-
],
|
|
198
|
-
eps=["growing_basic_eps"],
|
|
199
|
-
golden_cross=DATE_THRESHOLD,
|
|
200
|
-
macd_12_26_9_bullish_crossover=DATE_THRESHOLD,
|
|
201
|
-
adx_14_long=DATE_THRESHOLD,
|
|
202
|
-
)
|
|
203
|
-
DIVIDEND_GROWTH_COMPOUNDER = NamedFilterQuery(
|
|
204
|
-
name="Dividend-Growth Compounders",
|
|
205
|
-
description="Separates true dividend growers from high-yield traps. "
|
|
206
|
-
"Critical for income portfolios that need both yield and growth to beat inflation.",
|
|
207
|
-
mean_dividend_payout_ratio=[0, 0.6], # 0% to 60% payout ratio
|
|
208
|
-
cash_flow=[
|
|
209
|
-
"positive_free_cash_flow",
|
|
210
|
-
"quarterly_positive_free_cash_flow",
|
|
211
|
-
"growing_operating_cash_flow",
|
|
212
|
-
],
|
|
213
|
-
properties=["quarterly_positive_return_on_equity"],
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
BREAK_OUT_MOMENTUM = NamedFilterQuery(
|
|
217
|
-
name="Break-out Momentum",
|
|
218
|
-
description="Combines price, volume, and pattern confirmation. Great for tactical traders seeking "
|
|
219
|
-
"quick continuation moves with statistically higher follow-through.",
|
|
220
|
-
adosc_crosses_above_0=DATE_THRESHOLD,
|
|
221
|
-
positive_adosc_20_day_breakout=DATE_THRESHOLD,
|
|
222
|
-
rsi_bullish_crossover_30=DATE_THRESHOLD,
|
|
223
|
-
)
|
|
224
|
-
|
|
225
|
-
OVERSOLD_MEAN_REVERSION = NamedFilterQuery(
|
|
226
|
-
name="Oversold Mean Reversion",
|
|
227
|
-
description="Gives contrarian traders a high-probability bounce setup by "
|
|
228
|
-
"stacking three different oversold measures plus a reversal pattern.",
|
|
229
|
-
rsi_oversold=DATE_THRESHOLD,
|
|
230
|
-
stoch_oversold=DATE_THRESHOLD,
|
|
231
|
-
mfi_oversold=DATE_THRESHOLD,
|
|
232
|
-
lower_than_200_day_high=DATE_THRESHOLD,
|
|
233
|
-
)
|
|
234
64
|
RSI_CROSSOVER_30_GROWTH_STOCK_STRONG_FUNDAMENTAL = NamedFilterQuery(
|
|
235
65
|
name="RSI cross-over 30 growth stock strong fundamental",
|
|
236
66
|
income=[
|
|
@@ -241,7 +71,7 @@ RSI_CROSSOVER_30_GROWTH_STOCK_STRONG_FUNDAMENTAL = NamedFilterQuery(
|
|
|
241
71
|
],
|
|
242
72
|
cash_flow=["positive_free_cash_flow"],
|
|
243
73
|
properties=["operating_cash_flow_is_higher_than_net_income"],
|
|
244
|
-
price_per_earning_ratio=[
|
|
74
|
+
price_per_earning_ratio=[10, 100],
|
|
245
75
|
rsi_bullish_crossover_30=DATE_THRESHOLD,
|
|
246
76
|
market_capitalization=[5e8, 1e12],
|
|
247
77
|
order_by_desc="market_capitalization",
|
|
@@ -257,33 +87,16 @@ RSI_CROSSOVER_40_GROWTH_STOCK_STRONG_FUNDAMENTAL = NamedFilterQuery(
|
|
|
257
87
|
],
|
|
258
88
|
cash_flow=["positive_free_cash_flow"],
|
|
259
89
|
properties=["operating_cash_flow_is_higher_than_net_income"],
|
|
260
|
-
price_per_earning_ratio=[
|
|
90
|
+
price_per_earning_ratio=[10, 500],
|
|
261
91
|
rsi_bullish_crossover_40=DATE_THRESHOLD,
|
|
262
92
|
market_capitalization=[5e8, 1e12],
|
|
263
93
|
order_by_desc="market_capitalization",
|
|
264
94
|
country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
|
|
265
95
|
)
|
|
266
|
-
|
|
267
|
-
name="RSI cross-over 45 growth stock strong fundamental",
|
|
268
|
-
income=[
|
|
269
|
-
"positive_operating_income",
|
|
270
|
-
"growing_operating_income",
|
|
271
|
-
"positive_net_income",
|
|
272
|
-
"growing_net_income",
|
|
273
|
-
],
|
|
274
|
-
cash_flow=["positive_free_cash_flow"],
|
|
275
|
-
properties=["operating_cash_flow_is_higher_than_net_income"],
|
|
276
|
-
price_per_earning_ratio=[20, 40],
|
|
277
|
-
rsi_bullish_crossover_45=DATE_THRESHOLD,
|
|
278
|
-
market_capitalization=[5e8, 1e12],
|
|
279
|
-
order_by_desc="market_capitalization",
|
|
280
|
-
country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
|
|
281
|
-
)
|
|
96
|
+
|
|
282
97
|
RSI_CROSSOVER_30_GROWTH_STOCK = NamedFilterQuery(
|
|
283
98
|
name="RSI cross-over 30 growth stock",
|
|
284
|
-
|
|
285
|
-
properties=["operating_cash_flow_is_higher_than_net_income"],
|
|
286
|
-
price_per_earning_ratio=[20, 40],
|
|
99
|
+
price_per_earning_ratio=[10, 500],
|
|
287
100
|
rsi_bullish_crossover_30=DATE_THRESHOLD,
|
|
288
101
|
market_capitalization=[5e8, 1e12],
|
|
289
102
|
order_by_desc="market_capitalization",
|
|
@@ -291,26 +104,38 @@ RSI_CROSSOVER_30_GROWTH_STOCK = NamedFilterQuery(
|
|
|
291
104
|
)
|
|
292
105
|
RSI_CROSSOVER_40_GROWTH_STOCK = NamedFilterQuery(
|
|
293
106
|
name="RSI cross-over 40 growth stock",
|
|
294
|
-
|
|
295
|
-
properties=["operating_cash_flow_is_higher_than_net_income"],
|
|
296
|
-
price_per_earning_ratio=[20, 40],
|
|
107
|
+
price_per_earning_ratio=[10, 500],
|
|
297
108
|
rsi_bullish_crossover_40=DATE_THRESHOLD,
|
|
298
109
|
market_capitalization=[5e8, 1e12],
|
|
299
110
|
order_by_desc="market_capitalization",
|
|
300
111
|
country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
|
|
301
112
|
)
|
|
302
|
-
|
|
303
|
-
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
MOMENTUM_GROWTH_GOOD_FUNDAMENTALS = NamedFilterQuery(
|
|
116
|
+
name="Momentum Growth Good Fundamentals (RSI 30)",
|
|
304
117
|
cash_flow=["positive_free_cash_flow"],
|
|
305
118
|
properties=["operating_cash_flow_is_higher_than_net_income"],
|
|
306
|
-
price_per_earning_ratio=[
|
|
307
|
-
|
|
119
|
+
price_per_earning_ratio=[10, 500],
|
|
120
|
+
rsi_bullish_crossover_30=[
|
|
121
|
+
datetime.date.today() - datetime.timedelta(days=7),
|
|
122
|
+
datetime.date.today(),
|
|
123
|
+
],
|
|
124
|
+
macd_12_26_9_bullish_crossover=[
|
|
125
|
+
datetime.date.today() - datetime.timedelta(days=7),
|
|
126
|
+
datetime.date.today(),
|
|
127
|
+
],
|
|
128
|
+
sma_50_above_sma_200=[
|
|
129
|
+
datetime.date.today() - datetime.timedelta(days=5000),
|
|
130
|
+
datetime.date.today() - datetime.timedelta(days=10),
|
|
131
|
+
],
|
|
308
132
|
market_capitalization=[5e8, 1e12],
|
|
309
|
-
order_by_desc="
|
|
133
|
+
order_by_desc="momentum",
|
|
310
134
|
country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
|
|
311
135
|
)
|
|
312
|
-
|
|
313
|
-
|
|
136
|
+
|
|
137
|
+
MOMENTUM_GROWTH_STRONG_FUNDAMENTALS = NamedFilterQuery(
|
|
138
|
+
name="Momentum Growth Strong Fundamentals (RSI 30)",
|
|
314
139
|
income=[
|
|
315
140
|
"positive_operating_income",
|
|
316
141
|
"growing_operating_income",
|
|
@@ -319,80 +144,88 @@ MOMENTUM_STOCK_STRONG_FUNDAMENTAL = NamedFilterQuery(
|
|
|
319
144
|
],
|
|
320
145
|
cash_flow=["positive_free_cash_flow"],
|
|
321
146
|
properties=["operating_cash_flow_is_higher_than_net_income"],
|
|
322
|
-
price_per_earning_ratio=[10,
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
147
|
+
price_per_earning_ratio=[10, 500],
|
|
148
|
+
rsi_bullish_crossover_30=[
|
|
149
|
+
datetime.date.today() - datetime.timedelta(days=7),
|
|
150
|
+
datetime.date.today(),
|
|
151
|
+
],
|
|
152
|
+
macd_12_26_9_bullish_crossover=[
|
|
153
|
+
datetime.date.today() - datetime.timedelta(days=7),
|
|
154
|
+
datetime.date.today(),
|
|
155
|
+
],
|
|
156
|
+
sma_50_above_sma_200=[
|
|
157
|
+
datetime.date.today() - datetime.timedelta(days=5000),
|
|
158
|
+
datetime.date.today() - datetime.timedelta(days=10),
|
|
159
|
+
],
|
|
160
|
+
market_capitalization=[5e8, 1e12],
|
|
333
161
|
order_by_desc="momentum",
|
|
334
162
|
country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
|
|
335
163
|
)
|
|
336
|
-
|
|
337
|
-
name="Momentum
|
|
164
|
+
MOMENTUM_GROWTH_RSI_30 = NamedFilterQuery(
|
|
165
|
+
name="Momentum Growth Screener (RSI 30)",
|
|
338
166
|
price_per_earning_ratio=[10, 500],
|
|
339
|
-
|
|
167
|
+
rsi_bullish_crossover_30=[
|
|
168
|
+
datetime.date.today() - datetime.timedelta(days=7),
|
|
169
|
+
datetime.date.today(),
|
|
170
|
+
],
|
|
171
|
+
macd_12_26_9_bullish_crossover=[
|
|
172
|
+
datetime.date.today() - datetime.timedelta(days=7),
|
|
173
|
+
datetime.date.today(),
|
|
174
|
+
],
|
|
175
|
+
sma_50_above_sma_200=[
|
|
176
|
+
datetime.date.today() - datetime.timedelta(days=5000),
|
|
177
|
+
datetime.date.today() - datetime.timedelta(days=10),
|
|
178
|
+
],
|
|
179
|
+
market_capitalization=[5e8, 1e12],
|
|
340
180
|
order_by_desc="momentum",
|
|
341
181
|
country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
|
|
342
182
|
)
|
|
343
|
-
|
|
344
|
-
name="Momentum
|
|
183
|
+
MOMENTUM_GROWTH_RSI_40 = NamedFilterQuery(
|
|
184
|
+
name="Momentum Growth Screener (RSI 40)",
|
|
345
185
|
price_per_earning_ratio=[10, 500],
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
datetime.date.today()
|
|
349
|
-
datetime.date.today() - datetime.timedelta(days=31),
|
|
186
|
+
rsi_bullish_crossover_40=[
|
|
187
|
+
datetime.date.today() - datetime.timedelta(days=7),
|
|
188
|
+
datetime.date.today(),
|
|
350
189
|
],
|
|
351
190
|
macd_12_26_9_bullish_crossover=[
|
|
352
|
-
datetime.date.today() - datetime.timedelta(days=
|
|
191
|
+
datetime.date.today() - datetime.timedelta(days=7),
|
|
353
192
|
datetime.date.today(),
|
|
354
193
|
],
|
|
194
|
+
sma_50_above_sma_200=[
|
|
195
|
+
datetime.date.today() - datetime.timedelta(days=5000),
|
|
196
|
+
datetime.date.today() - datetime.timedelta(days=10),
|
|
197
|
+
],
|
|
198
|
+
market_capitalization=[5e8, 1e12],
|
|
355
199
|
order_by_desc="momentum",
|
|
356
200
|
country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
|
|
357
201
|
)
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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"],
|
|
202
|
+
|
|
203
|
+
GOLDEN_CROSS_LAST_SEVEN_DAYS = NamedFilterQuery(
|
|
204
|
+
name="Golden cross in the last five days",
|
|
368
205
|
price_per_earning_ratio=[10, 500],
|
|
369
206
|
last_price=[1, 10000],
|
|
370
|
-
|
|
371
|
-
datetime.date.today() - datetime.timedelta(days=
|
|
372
|
-
datetime.date.today() - datetime.timedelta(days=31),
|
|
373
|
-
],
|
|
374
|
-
macd_12_26_9_bullish_crossover=[
|
|
375
|
-
datetime.date.today() - datetime.timedelta(days=10),
|
|
207
|
+
golden_cross=[
|
|
208
|
+
datetime.date.today() - datetime.timedelta(days=7),
|
|
376
209
|
datetime.date.today(),
|
|
377
210
|
],
|
|
378
|
-
order_by_desc="
|
|
211
|
+
order_by_desc="market_capitalization",
|
|
379
212
|
country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
|
|
380
213
|
)
|
|
381
214
|
|
|
382
215
|
|
|
383
216
|
def predefined_filters() -> list[NamedFilterQuery]:
|
|
384
217
|
return [
|
|
218
|
+
STRONG_FUNDAMENTALS,
|
|
219
|
+
GOOD_FUNDAMENTALS,
|
|
385
220
|
RSI_CROSSOVER_30_GROWTH_STOCK_STRONG_FUNDAMENTAL,
|
|
386
221
|
RSI_CROSSOVER_40_GROWTH_STOCK_STRONG_FUNDAMENTAL,
|
|
387
|
-
RSI_CROSSOVER_45_GROWTH_STOCK_STRONG_FUNDAMENTAL,
|
|
388
222
|
RSI_CROSSOVER_30_GROWTH_STOCK,
|
|
389
223
|
RSI_CROSSOVER_40_GROWTH_STOCK,
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
MOMENTUM_TIME_SPAN_1_MONTH_STRONG_FUNDAMENTALS,
|
|
224
|
+
MOMENTUM_GROWTH_GOOD_FUNDAMENTALS,
|
|
225
|
+
MOMENTUM_GROWTH_STRONG_FUNDAMENTALS,
|
|
226
|
+
MOMENTUM_GROWTH_RSI_30,
|
|
227
|
+
MOMENTUM_GROWTH_RSI_40,
|
|
228
|
+
GOLDEN_CROSS_LAST_SEVEN_DAYS,
|
|
396
229
|
]
|
|
397
230
|
|
|
398
231
|
|
bullish/app/app.py
CHANGED
|
@@ -12,6 +12,7 @@ from bearish.models.price.prices import Prices # type: ignore
|
|
|
12
12
|
from bearish.models.query.query import AssetQuery, Symbols # type: ignore
|
|
13
13
|
from streamlit_file_browser import st_file_browser # type: ignore
|
|
14
14
|
|
|
15
|
+
from bullish.analysis.industry_views import get_industry_comparison_data
|
|
15
16
|
from bullish.analysis.predefined_filters import PredefinedFilters
|
|
16
17
|
from bullish.database.crud import BullishDb
|
|
17
18
|
from bullish.figures.figures import plot
|
|
@@ -83,12 +84,15 @@ def on_table_select() -> None:
|
|
|
83
84
|
return
|
|
84
85
|
|
|
85
86
|
symbol = st.session_state.data.iloc[row]["symbol"].to_numpy()[0]
|
|
87
|
+
country = st.session_state.data.iloc[row]["country"].to_numpy()[0]
|
|
88
|
+
industry = st.session_state.data.iloc[row]["industry"].to_numpy()[0]
|
|
86
89
|
query = AssetQuery(symbols=Symbols(equities=[Ticker(symbol=symbol)]))
|
|
87
90
|
prices = db.read_series(query, months=24)
|
|
88
91
|
data = Prices(prices=prices).to_dataframe()
|
|
89
92
|
dates = db.read_dates(symbol)
|
|
93
|
+
industry_data = get_industry_comparison_data(db, data, "Mean", industry, country)
|
|
90
94
|
|
|
91
|
-
fig = plot(data, symbol, dates=dates)
|
|
95
|
+
fig = plot(data, symbol, dates=dates, industry_data=industry_data)
|
|
92
96
|
|
|
93
97
|
st.session_state.ticker_figure = fig
|
|
94
98
|
|