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.

@@ -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=10),
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=[20, 40],
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=[20, 40],
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
- RSI_CROSSOVER_45_GROWTH_STOCK_STRONG_FUNDAMENTAL = NamedFilterQuery(
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
- cash_flow=["positive_free_cash_flow"],
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
- cash_flow=["positive_free_cash_flow"],
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
- RSI_CROSSOVER_45_GROWTH_STOCK = NamedFilterQuery(
303
- name="RSI cross-over 45 growth stock",
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=[20, 40],
307
- rsi_bullish_crossover_45=DATE_THRESHOLD,
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="market_capitalization",
133
+ order_by_desc="momentum",
310
134
  country=["Germany", "United states", "France", "United kingdom", "Canada", "Japan"],
311
135
  )
312
- MOMENTUM_STOCK_STRONG_FUNDAMENTAL = NamedFilterQuery(
313
- name="Momentum stock strong fundamental",
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, 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],
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
- MOMENTUM_STOCK_NO_FUNDAMENTAL_CHECKS = NamedFilterQuery(
337
- name="Momentum stock no fundamental checks",
164
+ MOMENTUM_GROWTH_RSI_30 = NamedFilterQuery(
165
+ name="Momentum Growth Screener (RSI 30)",
338
166
  price_per_earning_ratio=[10, 500],
339
- last_price=[1, 10000],
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
- MOMENTUM_TIME_SPAN_1_MONTH = NamedFilterQuery(
344
- name="Momentum 1 month",
183
+ MOMENTUM_GROWTH_RSI_40 = NamedFilterQuery(
184
+ name="Momentum Growth Screener (RSI 40)",
345
185
  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),
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=10),
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
- 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"],
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
- 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),
207
+ golden_cross=[
208
+ datetime.date.today() - datetime.timedelta(days=7),
376
209
  datetime.date.today(),
377
210
  ],
378
- order_by_desc="momentum",
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
- 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,
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