bullishpy 0.12.0__py3-none-any.whl → 0.14.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 +3 -0
- bullish/analysis/constants.py +403 -0
- bullish/analysis/filter.py +2 -405
- bullish/analysis/functions.py +35 -19
- bullish/analysis/indicators.py +32 -9
- bullish/analysis/industry_views.py +201 -0
- bullish/analysis/predefined_filters.py +109 -202
- bullish/app/app.py +5 -1
- bullish/database/alembic/versions/040b15fba458_.py +61 -0
- bullish/database/alembic/versions/5b10ee7604c1_.py +44 -0
- bullish/database/alembic/versions/b76079e9845f_.py +40 -0
- bullish/database/alembic/versions/bf6b86dd5463_.py +38 -0
- bullish/database/alembic/versions/ec25c8fa449f_.py +63 -0
- bullish/database/crud.py +72 -4
- bullish/database/schemas.py +17 -0
- bullish/figures/figures.py +28 -5
- bullish/interface/interface.py +29 -0
- {bullishpy-0.12.0.dist-info → bullishpy-0.14.0.dist-info}/METADATA +1 -1
- {bullishpy-0.12.0.dist-info → bullishpy-0.14.0.dist-info}/RECORD +21 -14
- {bullishpy-0.12.0.dist-info → bullishpy-0.14.0.dist-info}/WHEEL +0 -0
- {bullishpy-0.12.0.dist-info → bullishpy-0.14.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,21 +104,110 @@ 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)",
|
|
117
|
+
cash_flow=["positive_free_cash_flow"],
|
|
118
|
+
properties=["operating_cash_flow_is_higher_than_net_income"],
|
|
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
|
+
],
|
|
132
|
+
market_capitalization=[5e8, 1e12],
|
|
133
|
+
order_by_desc="momentum",
|
|
134
|
+
country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
MOMENTUM_GROWTH_STRONG_FUNDAMENTALS = NamedFilterQuery(
|
|
138
|
+
name="Momentum Growth Strong Fundamentals (RSI 30)",
|
|
139
|
+
income=[
|
|
140
|
+
"positive_operating_income",
|
|
141
|
+
"growing_operating_income",
|
|
142
|
+
"positive_net_income",
|
|
143
|
+
"growing_net_income",
|
|
144
|
+
],
|
|
304
145
|
cash_flow=["positive_free_cash_flow"],
|
|
305
146
|
properties=["operating_cash_flow_is_higher_than_net_income"],
|
|
306
|
-
price_per_earning_ratio=[
|
|
307
|
-
|
|
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],
|
|
161
|
+
order_by_desc="momentum",
|
|
162
|
+
country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
|
|
163
|
+
)
|
|
164
|
+
MOMENTUM_GROWTH_RSI_30 = NamedFilterQuery(
|
|
165
|
+
name="Momentum Growth Screener (RSI 30)",
|
|
166
|
+
price_per_earning_ratio=[10, 500],
|
|
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],
|
|
180
|
+
order_by_desc="momentum",
|
|
181
|
+
country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
|
|
182
|
+
)
|
|
183
|
+
MOMENTUM_GROWTH_RSI_40 = NamedFilterQuery(
|
|
184
|
+
name="Momentum Growth Screener (RSI 40)",
|
|
185
|
+
price_per_earning_ratio=[10, 500],
|
|
186
|
+
rsi_bullish_crossover_40=[
|
|
187
|
+
datetime.date.today() - datetime.timedelta(days=7),
|
|
188
|
+
datetime.date.today(),
|
|
189
|
+
],
|
|
190
|
+
macd_12_26_9_bullish_crossover=[
|
|
191
|
+
datetime.date.today() - datetime.timedelta(days=7),
|
|
192
|
+
datetime.date.today(),
|
|
193
|
+
],
|
|
194
|
+
sma_50_above_sma_200=[
|
|
195
|
+
datetime.date.today() - datetime.timedelta(days=5000),
|
|
196
|
+
datetime.date.today() - datetime.timedelta(days=10),
|
|
197
|
+
],
|
|
308
198
|
market_capitalization=[5e8, 1e12],
|
|
199
|
+
order_by_desc="momentum",
|
|
200
|
+
country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
GOLDEN_CROSS_LAST_SEVEN_DAYS = NamedFilterQuery(
|
|
204
|
+
name="Golden cross in the last five days",
|
|
205
|
+
price_per_earning_ratio=[10, 500],
|
|
206
|
+
last_price=[1, 10000],
|
|
207
|
+
golden_cross=[
|
|
208
|
+
datetime.date.today() - datetime.timedelta(days=7),
|
|
209
|
+
datetime.date.today(),
|
|
210
|
+
],
|
|
309
211
|
order_by_desc="market_capitalization",
|
|
310
212
|
country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
|
|
311
213
|
)
|
|
@@ -313,12 +215,17 @@ RSI_CROSSOVER_45_GROWTH_STOCK = NamedFilterQuery(
|
|
|
313
215
|
|
|
314
216
|
def predefined_filters() -> list[NamedFilterQuery]:
|
|
315
217
|
return [
|
|
218
|
+
STRONG_FUNDAMENTALS,
|
|
219
|
+
GOOD_FUNDAMENTALS,
|
|
316
220
|
RSI_CROSSOVER_30_GROWTH_STOCK_STRONG_FUNDAMENTAL,
|
|
317
221
|
RSI_CROSSOVER_40_GROWTH_STOCK_STRONG_FUNDAMENTAL,
|
|
318
|
-
RSI_CROSSOVER_45_GROWTH_STOCK_STRONG_FUNDAMENTAL,
|
|
319
222
|
RSI_CROSSOVER_30_GROWTH_STOCK,
|
|
320
223
|
RSI_CROSSOVER_40_GROWTH_STOCK,
|
|
321
|
-
|
|
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,
|
|
322
229
|
]
|
|
323
230
|
|
|
324
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
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
Revision ID: 040b15fba458
|
|
4
|
+
Revises: ec25c8fa449f
|
|
5
|
+
Create Date: 2025-07-15 09:22:11.456381
|
|
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
|
+
import sqlmodel
|
|
15
|
+
|
|
16
|
+
# revision identifiers, used by Alembic.
|
|
17
|
+
revision: str = "040b15fba458"
|
|
18
|
+
down_revision: Union[str, None] = "ec25c8fa449f"
|
|
19
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
20
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def upgrade() -> None:
|
|
24
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
25
|
+
op.create_table(
|
|
26
|
+
"industryview",
|
|
27
|
+
sa.Column("simple_return", sa.Float(), nullable=True),
|
|
28
|
+
sa.Column("log_return", sa.Float(), nullable=True),
|
|
29
|
+
sa.Column("normalized_close", sa.Float(), nullable=True),
|
|
30
|
+
sa.Column("date", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
|
31
|
+
sa.Column("created_at", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
|
32
|
+
sa.Column("country", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
|
33
|
+
sa.Column("industry", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
|
34
|
+
sa.Column("industry_group", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
|
35
|
+
sa.Column("sector", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
|
36
|
+
sa.Column("type", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
|
37
|
+
sa.PrimaryKeyConstraint("date", "country", "industry", "type"),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
op.drop_table("industryreturns")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def downgrade() -> None:
|
|
44
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
45
|
+
|
|
46
|
+
op.create_table(
|
|
47
|
+
"industryreturns",
|
|
48
|
+
sa.Column("date", sa.VARCHAR(), nullable=False),
|
|
49
|
+
sa.Column("created_at", sa.VARCHAR(), nullable=True),
|
|
50
|
+
sa.Column("simple_return", sa.FLOAT(), nullable=True),
|
|
51
|
+
sa.Column("log_return", sa.FLOAT(), nullable=True),
|
|
52
|
+
sa.Column("country", sa.VARCHAR(), nullable=False),
|
|
53
|
+
sa.Column("industry", sa.VARCHAR(), nullable=False),
|
|
54
|
+
sa.Column("industry_group", sa.VARCHAR(), nullable=True),
|
|
55
|
+
sa.Column("sector", sa.VARCHAR(), nullable=True),
|
|
56
|
+
sa.Column("type", sa.VARCHAR(), nullable=False),
|
|
57
|
+
sa.PrimaryKeyConstraint("date", "country", "industry", "type"),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
op.drop_table("industryview")
|
|
61
|
+
# ### end Alembic commands ###
|