bullishpy 0.55.0__py3-none-any.whl → 0.75.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 +28 -3
- bullish/analysis/constants.py +54 -0
- bullish/analysis/filter.py +3 -1
- bullish/analysis/functions.py +37 -2
- bullish/analysis/indicators.py +23 -2
- bullish/analysis/openai.py +91 -0
- bullish/analysis/predefined_filters.py +177 -109
- bullish/app/app.py +15 -4
- bullish/database/alembic/versions/65662e214031_.py +48 -0
- bullish/database/alembic/versions/660897c02c00_.py +36 -0
- bullish/database/alembic/versions/b36c310f49ec_.py +43 -0
- bullish/database/alembic/versions/c828e29e1105_.py +87 -0
- bullish/database/alembic/versions/cc28171c21a4_.py +43 -0
- bullish/database/crud.py +53 -4
- bullish/database/schemas.py +9 -0
- bullish/figures/figures.py +28 -18
- bullish/interface/interface.py +7 -1
- bullish/jobs/tasks.py +45 -10
- {bullishpy-0.55.0.dist-info → bullishpy-0.75.0.dist-info}/METADATA +8 -6
- {bullishpy-0.55.0.dist-info → bullishpy-0.75.0.dist-info}/RECORD +23 -17
- {bullishpy-0.55.0.dist-info → bullishpy-0.75.0.dist-info}/WHEEL +1 -1
- {bullishpy-0.55.0.dist-info → bullishpy-0.75.0.dist-info}/entry_points.txt +0 -0
- {bullishpy-0.55.0.dist-info → bullishpy-0.75.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -3,7 +3,7 @@ import json
|
|
|
3
3
|
import os
|
|
4
4
|
from datetime import timedelta
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Dict, Any, Optional, List, Union, get_args
|
|
6
|
+
from typing import Dict, Any, Optional, List, Union, get_args, Tuple
|
|
7
7
|
|
|
8
8
|
from bullish.analysis.analysis import AnalysisView
|
|
9
9
|
from bullish.analysis.backtest import (
|
|
@@ -12,7 +12,12 @@ from bullish.analysis.backtest import (
|
|
|
12
12
|
BacktestQueryRange,
|
|
13
13
|
BacktestQuerySelection,
|
|
14
14
|
)
|
|
15
|
-
from bullish.analysis.constants import
|
|
15
|
+
from bullish.analysis.constants import (
|
|
16
|
+
Europe,
|
|
17
|
+
Us,
|
|
18
|
+
HighGrowthIndustry,
|
|
19
|
+
DefensiveIndustries,
|
|
20
|
+
)
|
|
16
21
|
from bullish.analysis.filter import FilterQuery, BOOLEAN_GROUP_MAPPING
|
|
17
22
|
from pydantic import BaseModel, Field
|
|
18
23
|
|
|
@@ -20,11 +25,15 @@ from bullish.analysis.indicators import Indicators
|
|
|
20
25
|
from bullish.database.crud import BullishDb
|
|
21
26
|
|
|
22
27
|
DATE_THRESHOLD = [
|
|
23
|
-
datetime.date.today() - datetime.timedelta(days=
|
|
28
|
+
datetime.date.today() - datetime.timedelta(days=2),
|
|
24
29
|
datetime.date.today(),
|
|
25
30
|
]
|
|
26
31
|
|
|
27
32
|
|
|
33
|
+
def _get_variants(variants: List[str]) -> List[Tuple[str, ...]]:
|
|
34
|
+
return [tuple(variants[:i]) for i in range(1, len(variants) + 1)]
|
|
35
|
+
|
|
36
|
+
|
|
28
37
|
class NamedFilterQuery(FilterQuery):
|
|
29
38
|
name: str
|
|
30
39
|
description: Optional[str] = None
|
|
@@ -126,17 +135,29 @@ class NamedFilterQuery(FilterQuery):
|
|
|
126
135
|
self.model_dump() | {"name": f"{self.name} ({suffix})", **properties}
|
|
127
136
|
)
|
|
128
137
|
|
|
129
|
-
def
|
|
138
|
+
def week_top_performers(self) -> "NamedFilterQuery":
|
|
139
|
+
properties = {
|
|
140
|
+
"volume_above_average": DATE_THRESHOLD,
|
|
141
|
+
"weekly_growth": [1, 100],
|
|
142
|
+
}
|
|
143
|
+
return self._custom_variant("Week Top Performers", properties)
|
|
144
|
+
|
|
145
|
+
def month_top_performers(self) -> "NamedFilterQuery":
|
|
146
|
+
properties = {
|
|
147
|
+
"monthly_growth": [8, 100],
|
|
148
|
+
}
|
|
149
|
+
return self._custom_variant("Month Top Performers", properties)
|
|
150
|
+
|
|
151
|
+
def year_top_performers(self) -> "NamedFilterQuery":
|
|
130
152
|
properties = {
|
|
131
153
|
"volume_above_average": DATE_THRESHOLD,
|
|
132
154
|
"sma_50_above_sma_200": [
|
|
133
155
|
datetime.date.today() - datetime.timedelta(days=5000),
|
|
134
156
|
datetime.date.today(),
|
|
135
157
|
],
|
|
136
|
-
"
|
|
137
|
-
"monthly_growth": [8, 100],
|
|
158
|
+
"yearly_growth": [30, 100],
|
|
138
159
|
}
|
|
139
|
-
return self._custom_variant("Top Performers", properties)
|
|
160
|
+
return self._custom_variant("Yearly Top Performers", properties)
|
|
140
161
|
|
|
141
162
|
def poor_performers(self) -> "NamedFilterQuery":
|
|
142
163
|
properties = {
|
|
@@ -152,102 +173,154 @@ class NamedFilterQuery(FilterQuery):
|
|
|
152
173
|
}
|
|
153
174
|
return self._custom_variant("Poor Performers", properties)
|
|
154
175
|
|
|
155
|
-
def
|
|
176
|
+
def yearly_fundamentals(self) -> "NamedFilterQuery":
|
|
156
177
|
properties = {
|
|
157
178
|
"income": [
|
|
158
179
|
"positive_operating_income",
|
|
159
180
|
"positive_net_income",
|
|
181
|
+
"growing_net_income",
|
|
182
|
+
"growing_operating_income",
|
|
183
|
+
],
|
|
184
|
+
"cash_flow": ["positive_free_cash_flow", "growing_operating_cash_flow"],
|
|
185
|
+
"properties": [
|
|
186
|
+
"positive_return_on_equity",
|
|
187
|
+
"operating_cash_flow_is_higher_than_net_income",
|
|
188
|
+
],
|
|
189
|
+
}
|
|
190
|
+
return self._custom_variant("Yearly Fundamentals", properties)
|
|
191
|
+
|
|
192
|
+
def quarterly_fundamentals(self) -> "NamedFilterQuery":
|
|
193
|
+
properties = {
|
|
194
|
+
"income": [
|
|
160
195
|
"quarterly_positive_operating_income",
|
|
161
196
|
"quarterly_positive_net_income",
|
|
162
197
|
],
|
|
163
198
|
"cash_flow": [
|
|
164
|
-
"positive_free_cash_flow",
|
|
165
199
|
"quarterly_positive_free_cash_flow",
|
|
166
200
|
],
|
|
167
|
-
"eps": [
|
|
168
|
-
"positive_basic_eps",
|
|
169
|
-
"positive_diluted_eps",
|
|
170
|
-
"quarterly_positive_basic_eps",
|
|
171
|
-
"quarterly_positive_diluted_eps",
|
|
172
|
-
],
|
|
173
201
|
"properties": [
|
|
174
|
-
"positive_return_on_assets",
|
|
175
|
-
"positive_return_on_equity",
|
|
176
|
-
"positive_debt_to_equity",
|
|
177
|
-
"operating_cash_flow_is_higher_than_net_income",
|
|
178
|
-
"quarterly_positive_return_on_assets",
|
|
179
|
-
"quarterly_positive_return_on_equity",
|
|
180
|
-
"quarterly_positive_debt_to_equity",
|
|
181
202
|
"quarterly_operating_cash_flow_is_higher_than_net_income",
|
|
182
203
|
],
|
|
183
204
|
}
|
|
184
|
-
return self._custom_variant("
|
|
205
|
+
return self._custom_variant("Quarterly Fundamentals", properties)
|
|
185
206
|
|
|
186
|
-
def
|
|
207
|
+
def growing_quarterly_fundamentals(self) -> "NamedFilterQuery":
|
|
187
208
|
properties = {
|
|
188
209
|
"income": [
|
|
189
|
-
"
|
|
190
|
-
"
|
|
210
|
+
"quarterly_positive_operating_income",
|
|
211
|
+
"quarterly_positive_net_income",
|
|
191
212
|
"quarterly_growing_net_income",
|
|
192
|
-
"quarterly_growing_operating_income",
|
|
193
213
|
],
|
|
194
214
|
"cash_flow": [
|
|
195
|
-
"
|
|
215
|
+
"quarterly_positive_free_cash_flow",
|
|
196
216
|
"quarterly_growing_operating_cash_flow",
|
|
197
217
|
],
|
|
218
|
+
"properties": [
|
|
219
|
+
"quarterly_operating_cash_flow_is_higher_than_net_income",
|
|
220
|
+
],
|
|
221
|
+
}
|
|
222
|
+
return self._custom_variant("Growing Quarterly Fundamentals", properties)
|
|
223
|
+
|
|
224
|
+
def min_fundamentals(self) -> "NamedFilterQuery":
|
|
225
|
+
properties = {
|
|
226
|
+
"income": [
|
|
227
|
+
"positive_operating_income",
|
|
228
|
+
"positive_net_income",
|
|
229
|
+
],
|
|
230
|
+
"cash_flow": [
|
|
231
|
+
"positive_free_cash_flow",
|
|
232
|
+
],
|
|
198
233
|
"eps": [
|
|
199
|
-
"
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
"
|
|
234
|
+
"positive_diluted_eps", # or positive_basic_eps if diluted not available
|
|
235
|
+
],
|
|
236
|
+
"properties": [
|
|
237
|
+
"positive_return_on_equity",
|
|
238
|
+
"operating_cash_flow_is_higher_than_net_income",
|
|
203
239
|
],
|
|
204
240
|
}
|
|
205
|
-
return self._custom_variant("
|
|
206
|
-
|
|
207
|
-
def
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
241
|
+
return self._custom_variant("Min Fundamentals", properties)
|
|
242
|
+
|
|
243
|
+
def high_growth(self) -> "NamedFilterQuery":
|
|
244
|
+
properties = {"industry": list(get_args(HighGrowthIndustry))}
|
|
245
|
+
return self._custom_variant("Growth", properties)
|
|
246
|
+
|
|
247
|
+
def defensive(self) -> "NamedFilterQuery":
|
|
248
|
+
properties = {"industry": list(get_args(DefensiveIndustries))}
|
|
249
|
+
return self._custom_variant("Defensive", properties)
|
|
250
|
+
|
|
251
|
+
def cheap(self) -> "NamedFilterQuery":
|
|
252
|
+
properties = {"last_price": [1, 30]}
|
|
253
|
+
return self._custom_variant("Cheap", properties)
|
|
254
|
+
|
|
255
|
+
def europe(self) -> "NamedFilterQuery":
|
|
256
|
+
return self.country_variant("Europe", list(get_args(Europe)))
|
|
257
|
+
|
|
258
|
+
def us(self) -> "NamedFilterQuery":
|
|
259
|
+
return self.country_variant("Us", list(get_args(Us)))
|
|
260
|
+
|
|
261
|
+
def rsi_30(self) -> "NamedFilterQuery":
|
|
262
|
+
return self.update_indicator_filter("RSI 30", "rsi_bullish_crossover_30")
|
|
263
|
+
|
|
264
|
+
def rsi_40(self) -> "NamedFilterQuery":
|
|
265
|
+
return self.update_indicator_filter("RSI 40", "rsi_bullish_crossover_40")
|
|
266
|
+
|
|
267
|
+
def macd(self) -> "NamedFilterQuery":
|
|
268
|
+
return self.update_indicator_filter("MACD", "macd_12_26_9_bullish_crossover")
|
|
269
|
+
|
|
270
|
+
def rsi_neutral_(self) -> "NamedFilterQuery":
|
|
271
|
+
return self.update_indicator_filter("RSI Neutral", "rsi_neutral")
|
|
272
|
+
|
|
273
|
+
def rsi_oversold_(self) -> "NamedFilterQuery":
|
|
274
|
+
return self.update_indicator_filter("RSI Oversold", "rsi_oversold")
|
|
275
|
+
|
|
276
|
+
def rsi_overbought_(self) -> "NamedFilterQuery":
|
|
277
|
+
return self.update_indicator_filter("RSI Overbought", "rsi_overbought")
|
|
278
|
+
|
|
279
|
+
def adx(self) -> "NamedFilterQuery":
|
|
280
|
+
return self.update_indicator_filter("ADX 14", "adx_14")
|
|
281
|
+
|
|
282
|
+
def earnings_date(self) -> "NamedFilterQuery":
|
|
283
|
+
return NamedFilterQuery.model_validate(
|
|
284
|
+
self.model_dump()
|
|
285
|
+
| {
|
|
286
|
+
"name": f"{self.name} (Earnings Date)",
|
|
287
|
+
"next_earnings_date": [
|
|
288
|
+
datetime.date.today(),
|
|
289
|
+
datetime.date.today() + timedelta(days=20),
|
|
290
|
+
],
|
|
291
|
+
}
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def variants(
|
|
295
|
+
self,
|
|
296
|
+
variants: Optional[List[List[str]]] = None,
|
|
297
|
+
filters: Optional[List[str]] = None,
|
|
298
|
+
) -> List["NamedFilterQuery"]:
|
|
299
|
+
if filters and self.name not in filters:
|
|
300
|
+
return [self]
|
|
301
|
+
variants = variants or [["europe"], ["us"]]
|
|
302
|
+
|
|
303
|
+
_variants = {v for variant in variants for v in _get_variants(variant)}
|
|
304
|
+
filters_ = []
|
|
305
|
+
for attributes in _variants:
|
|
306
|
+
filter__ = self
|
|
307
|
+
for attr in attributes:
|
|
308
|
+
filter__ = getattr(filter__, attr)()
|
|
309
|
+
filters_.append(filter__)
|
|
310
|
+
|
|
311
|
+
return [self, *filters_]
|
|
245
312
|
|
|
246
313
|
|
|
247
314
|
def load_custom_filters() -> List[NamedFilterQuery]:
|
|
248
315
|
if "CUSTOM_FILTERS_PATH" in os.environ:
|
|
249
316
|
custom_filters_path = os.environ["CUSTOM_FILTERS_PATH"]
|
|
250
|
-
return
|
|
317
|
+
return [
|
|
318
|
+
variant
|
|
319
|
+
for f in read_custom_filters(Path(custom_filters_path))
|
|
320
|
+
for variant in f.variants(
|
|
321
|
+
variants=[["rsi_overbought_"]], filters=["portfolio", "Portfolio"]
|
|
322
|
+
)
|
|
323
|
+
]
|
|
251
324
|
return []
|
|
252
325
|
|
|
253
326
|
|
|
@@ -264,57 +337,52 @@ SMALL_CAP = NamedFilterQuery(
|
|
|
264
337
|
market_capitalization=[5e7, 5e8],
|
|
265
338
|
properties=["positive_debt_to_equity"],
|
|
266
339
|
average_volume_30=[50000, 5e9],
|
|
267
|
-
volume_above_average=DATE_THRESHOLD,
|
|
268
|
-
sma_50_above_sma_200=[
|
|
269
|
-
datetime.date.today() - datetime.timedelta(days=5000),
|
|
270
|
-
datetime.date.today(),
|
|
271
|
-
],
|
|
272
|
-
weekly_growth=[1, 100],
|
|
273
|
-
monthly_growth=[8, 100],
|
|
274
340
|
order_by_desc="market_capitalization",
|
|
275
|
-
).variants(
|
|
341
|
+
).variants(
|
|
342
|
+
variants=[
|
|
343
|
+
["week_top_performers", "min_fundamentals"],
|
|
344
|
+
["month_top_performers", "min_fundamentals"],
|
|
345
|
+
["earnings_date", "min_fundamentals"],
|
|
346
|
+
["rsi_oversold_", "min_fundamentals"],
|
|
347
|
+
]
|
|
348
|
+
)
|
|
276
349
|
|
|
277
350
|
LARGE_CAPS = NamedFilterQuery(
|
|
278
|
-
name="Large
|
|
351
|
+
name="Large Cap",
|
|
279
352
|
order_by_desc="market_capitalization",
|
|
280
353
|
market_capitalization=[1e10, 1e14],
|
|
281
|
-
).variants(
|
|
354
|
+
).variants(
|
|
355
|
+
variants=[
|
|
356
|
+
["rsi_oversold_", "macd", "yearly_fundamentals"],
|
|
357
|
+
["rsi_neutral_", "macd", "adx", "yearly_fundamentals"],
|
|
358
|
+
["rsi_30", "macd", "adx", "yearly_fundamentals"],
|
|
359
|
+
["rsi_oversold_", "macd", "quarterly_fundamentals"],
|
|
360
|
+
["rsi_neutral_", "macd", "adx", "quarterly_fundamentals"],
|
|
361
|
+
["rsi_30", "macd", "adx", "quarterly_fundamentals"],
|
|
362
|
+
["earnings_date", "quarterly_fundamentals", "yearly_fundamentals"],
|
|
363
|
+
]
|
|
364
|
+
)
|
|
282
365
|
|
|
283
366
|
MID_CAPS = NamedFilterQuery(
|
|
284
|
-
name="Mid
|
|
367
|
+
name="Mid Cap",
|
|
285
368
|
order_by_desc="market_capitalization",
|
|
286
369
|
market_capitalization=[5e8, 1e10],
|
|
287
|
-
).variants(
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
],
|
|
296
|
-
).variants()
|
|
297
|
-
SEMICONDUCTORS = NamedFilterQuery(
|
|
298
|
-
name="Semiconductors",
|
|
299
|
-
order_by_desc="market_capitalization",
|
|
300
|
-
industry=["Semiconductors"],
|
|
301
|
-
).variants()
|
|
302
|
-
SOFTWARE = NamedFilterQuery(
|
|
303
|
-
name="Software - Application",
|
|
304
|
-
order_by_desc="market_capitalization",
|
|
305
|
-
industry=["Software - Application"],
|
|
306
|
-
).variants()
|
|
370
|
+
).variants(
|
|
371
|
+
variants=[
|
|
372
|
+
["week_top_performers"],
|
|
373
|
+
["month_top_performers"],
|
|
374
|
+
["earnings_date", "quarterly_fundamentals", "yearly_fundamentals"],
|
|
375
|
+
["rsi_oversold_", "macd", "adx"],
|
|
376
|
+
]
|
|
377
|
+
)
|
|
307
378
|
|
|
308
379
|
|
|
309
380
|
def predefined_filters() -> list[NamedFilterQuery]:
|
|
310
381
|
return [
|
|
382
|
+
*load_custom_filters(),
|
|
311
383
|
*SMALL_CAP,
|
|
312
|
-
*LARGE_CAPS,
|
|
313
|
-
*NEXT_EARNINGS_DATE,
|
|
314
384
|
*MID_CAPS,
|
|
315
|
-
*
|
|
316
|
-
*SOFTWARE,
|
|
317
|
-
*load_custom_filters(),
|
|
385
|
+
*LARGE_CAPS,
|
|
318
386
|
]
|
|
319
387
|
|
|
320
388
|
|
bullish/app/app.py
CHANGED
|
@@ -35,6 +35,7 @@ from bullish.utils.checks import (
|
|
|
35
35
|
compatible_bullish_database,
|
|
36
36
|
empty_analysis_table,
|
|
37
37
|
)
|
|
38
|
+
from mysec.services import sec # type: ignore
|
|
38
39
|
|
|
39
40
|
CACHE_SHELVE = "user_cache"
|
|
40
41
|
DB_KEY = "db_path"
|
|
@@ -80,7 +81,7 @@ def on_table_select() -> None:
|
|
|
80
81
|
|
|
81
82
|
db = bearish_db(st.session_state.database_path)
|
|
82
83
|
if st.session_state.data.empty or (
|
|
83
|
-
not st.session_state.data.iloc[row]["symbol"].to_numpy()
|
|
84
|
+
not st.session_state.data.iloc[row]["symbol"].to_numpy().size > 0
|
|
84
85
|
):
|
|
85
86
|
return
|
|
86
87
|
|
|
@@ -123,7 +124,7 @@ def dialog_pick_database() -> None:
|
|
|
123
124
|
f"The database {db_path} has not the necessary data to run this application. "
|
|
124
125
|
"A backround job will be started to update the data."
|
|
125
126
|
)
|
|
126
|
-
analysis(db_path)
|
|
127
|
+
analysis(db_path, "Update analysis")
|
|
127
128
|
st.rerun()
|
|
128
129
|
if event is None:
|
|
129
130
|
st.stop()
|
|
@@ -290,15 +291,21 @@ def dialog_plot_figure() -> None:
|
|
|
290
291
|
<div class="news-hover" >
|
|
291
292
|
📰 <span class="label">News</span>
|
|
292
293
|
<div class="tooltip">
|
|
293
|
-
<h2>Date: {st.session_state.ticker_news.
|
|
294
|
+
<h2>Date: {st.session_state.ticker_news.to_date()}</h2>
|
|
294
295
|
<h2>Price targets</h2>
|
|
295
296
|
<p>High price target: {st.session_state.ticker_news.high_price_target}</p>
|
|
296
297
|
<p>Low price target: {st.session_state.ticker_news.low_price_target}</p>
|
|
298
|
+
<p>OpenAI High price target: {st.session_state.ticker_news.oai_high_price_target}</p>
|
|
299
|
+
<p>OpenAI Low price target: {st.session_state.ticker_news.oai_low_price_target}</p>
|
|
297
300
|
<h2>Recommendation: {st.session_state.ticker_news.recommendation}</h2>
|
|
301
|
+
<h2>OpenAI Recommendation: {st.session_state.ticker_news.oai_recommendation}</h2>
|
|
298
302
|
<h2>Consensus: {st.session_state.ticker_news.consensus}</h2>
|
|
299
303
|
<h2>Explanation & reasons</h2>
|
|
300
304
|
<p>{st.session_state.ticker_news.explanation}</p>
|
|
301
305
|
<p>{st.session_state.ticker_news.reason}</p>
|
|
306
|
+
<p>{st.session_state.ticker_news.oai_explanation}</p>
|
|
307
|
+
<h2>Recent news</h2>
|
|
308
|
+
<p>{st.session_state.ticker_news.oai_recent_news}</p>
|
|
302
309
|
<h2>News summaries</h2>
|
|
303
310
|
{st.session_state.ticker_news.to_news()}
|
|
304
311
|
</div>
|
|
@@ -414,13 +421,15 @@ def main() -> None:
|
|
|
414
421
|
|
|
415
422
|
if st.session_state.database_path is None:
|
|
416
423
|
dialog_pick_database()
|
|
424
|
+
if "initialized" not in st.session_state:
|
|
417
425
|
initialize(
|
|
418
426
|
database_path=st.session_state.database_path,
|
|
419
427
|
job_type="Initialize",
|
|
420
428
|
)
|
|
429
|
+
st.session_state.initialized = True
|
|
421
430
|
bearish_db_ = bearish_db(st.session_state.database_path)
|
|
422
431
|
|
|
423
|
-
charts_tab, jobs_tab = st.tabs(["Charts", "Jobs"])
|
|
432
|
+
charts_tab, jobs_tab, sec_tab = st.tabs(["Charts", "Jobs", "Sec"])
|
|
424
433
|
if "data" not in st.session_state:
|
|
425
434
|
st.session_state.data = load_analysis_data(bearish_db_)
|
|
426
435
|
|
|
@@ -471,6 +480,8 @@ def main() -> None:
|
|
|
471
480
|
use_container_width=True,
|
|
472
481
|
hide_index=True,
|
|
473
482
|
)
|
|
483
|
+
with sec_tab:
|
|
484
|
+
st.plotly_chart(sec(bearish_db_), use_container_width=True)
|
|
474
485
|
|
|
475
486
|
|
|
476
487
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
Revision ID: 65662e214031
|
|
4
|
+
Revises: 660897c02c00
|
|
5
|
+
Create Date: 2025-08-20 17:30:47.973725
|
|
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 = "65662e214031"
|
|
17
|
+
down_revision: Union[str, None] = "660897c02c00"
|
|
18
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
19
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def upgrade() -> None:
|
|
23
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
24
|
+
|
|
25
|
+
with op.batch_alter_table("analysis", schema=None) as batch_op:
|
|
26
|
+
batch_op.add_column(sa.Column("downside", sa.Float(), nullable=True))
|
|
27
|
+
batch_op.add_column(sa.Column("oai_moat", sa.Boolean(), nullable=True))
|
|
28
|
+
batch_op.create_index("ix_analysis_downside", ["downside"], unique=False)
|
|
29
|
+
batch_op.create_index("ix_analysis_oai_moat", ["oai_moat"], unique=False)
|
|
30
|
+
|
|
31
|
+
with op.batch_alter_table("openai", schema=None) as batch_op:
|
|
32
|
+
batch_op.add_column(sa.Column("moat", sa.Boolean(), nullable=True))
|
|
33
|
+
|
|
34
|
+
# ### end Alembic commands ###
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def downgrade() -> None:
|
|
38
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
39
|
+
with op.batch_alter_table("openai", schema=None) as batch_op:
|
|
40
|
+
batch_op.drop_column("moat")
|
|
41
|
+
|
|
42
|
+
with op.batch_alter_table("analysis", schema=None) as batch_op:
|
|
43
|
+
batch_op.drop_index("ix_analysis_oai_moat")
|
|
44
|
+
batch_op.drop_index("ix_analysis_downside")
|
|
45
|
+
batch_op.drop_column("oai_moat")
|
|
46
|
+
batch_op.drop_column("downside")
|
|
47
|
+
|
|
48
|
+
# ### end Alembic commands ###
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
Revision ID: 660897c02c00
|
|
4
|
+
Revises: c828e29e1105
|
|
5
|
+
Create Date: 2025-08-20 17:19:05.423318
|
|
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 = "660897c02c00"
|
|
17
|
+
down_revision: Union[str, None] = "c828e29e1105"
|
|
18
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
19
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def upgrade() -> None:
|
|
23
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
24
|
+
|
|
25
|
+
with op.batch_alter_table("analysis", schema=None) as batch_op:
|
|
26
|
+
batch_op.add_column(sa.Column("rsi", sa.Float(), nullable=True))
|
|
27
|
+
batch_op.create_index("ix_analysis_rsi", ["rsi"], unique=False)
|
|
28
|
+
|
|
29
|
+
# ### end Alembic commands ###
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def downgrade() -> None:
|
|
33
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
34
|
+
with op.batch_alter_table("analysis", schema=None) as batch_op:
|
|
35
|
+
batch_op.drop_index("ix_analysis_rsi")
|
|
36
|
+
batch_op.drop_column("rsi")
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
Revision ID: b36c310f49ec
|
|
4
|
+
Revises: 260fcff7212e
|
|
5
|
+
Create Date: 2025-08-14 22:39:38.207093
|
|
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 = "b36c310f49ec"
|
|
18
|
+
down_revision: Union[str, None] = "cc28171c21a4"
|
|
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
|
+
"openai",
|
|
27
|
+
sa.Column("symbol", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
|
28
|
+
sa.Column("news_date", sa.Date(), nullable=False),
|
|
29
|
+
sa.Column("high_price_target", sa.Float(), nullable=True),
|
|
30
|
+
sa.Column("low_price_target", sa.Float(), nullable=True),
|
|
31
|
+
sa.Column("recent_news", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
|
32
|
+
sa.Column("recommendation", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
|
33
|
+
sa.Column("explanation", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
|
34
|
+
sa.PrimaryKeyConstraint("symbol", "news_date"),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# ### end Alembic commands ###
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def downgrade() -> None:
|
|
41
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
42
|
+
op.drop_table("openai")
|
|
43
|
+
# ### end Alembic commands ###
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
Revision ID: c828e29e1105
|
|
4
|
+
Revises: b36c310f49ec
|
|
5
|
+
Create Date: 2025-08-15 17:57:09.541454
|
|
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 = "c828e29e1105"
|
|
18
|
+
down_revision: Union[str, None] = "b36c310f49ec"
|
|
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
|
+
|
|
26
|
+
with op.batch_alter_table("analysis", schema=None) as batch_op:
|
|
27
|
+
batch_op.add_column(
|
|
28
|
+
sa.Column("oai_high_price_target", sa.Float(), nullable=True)
|
|
29
|
+
)
|
|
30
|
+
batch_op.add_column(
|
|
31
|
+
sa.Column("oai_low_price_target", sa.Float(), nullable=True)
|
|
32
|
+
)
|
|
33
|
+
batch_op.add_column(sa.Column("oai_news_date", sa.DateTime(), nullable=True))
|
|
34
|
+
batch_op.add_column(
|
|
35
|
+
sa.Column(
|
|
36
|
+
"oai_recent_news", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
batch_op.add_column(
|
|
40
|
+
sa.Column(
|
|
41
|
+
"oai_recommendation", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
batch_op.add_column(
|
|
45
|
+
sa.Column(
|
|
46
|
+
"oai_explanation", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
batch_op.create_index(
|
|
50
|
+
"ix_analysis_oai_explanation", ["oai_explanation"], unique=False
|
|
51
|
+
)
|
|
52
|
+
batch_op.create_index(
|
|
53
|
+
"ix_analysis_oai_high_price_target", ["oai_high_price_target"], unique=False
|
|
54
|
+
)
|
|
55
|
+
batch_op.create_index(
|
|
56
|
+
"ix_analysis_oai_low_price_target", ["oai_low_price_target"], unique=False
|
|
57
|
+
)
|
|
58
|
+
batch_op.create_index(
|
|
59
|
+
"ix_analysis_oai_news_date", ["oai_news_date"], unique=False
|
|
60
|
+
)
|
|
61
|
+
batch_op.create_index(
|
|
62
|
+
"ix_analysis_oai_recent_news", ["oai_recent_news"], unique=False
|
|
63
|
+
)
|
|
64
|
+
batch_op.create_index(
|
|
65
|
+
"ix_analysis_oai_recommendation", ["oai_recommendation"], unique=False
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# ### end Alembic commands ###
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def downgrade() -> None:
|
|
72
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
73
|
+
with op.batch_alter_table("analysis", schema=None) as batch_op:
|
|
74
|
+
batch_op.drop_index("ix_analysis_oai_recommendation")
|
|
75
|
+
batch_op.drop_index("ix_analysis_oai_recent_news")
|
|
76
|
+
batch_op.drop_index("ix_analysis_oai_news_date")
|
|
77
|
+
batch_op.drop_index("ix_analysis_oai_low_price_target")
|
|
78
|
+
batch_op.drop_index("ix_analysis_oai_high_price_target")
|
|
79
|
+
batch_op.drop_index("ix_analysis_oai_explanation")
|
|
80
|
+
batch_op.drop_column("oai_explanation")
|
|
81
|
+
batch_op.drop_column("oai_recommendation")
|
|
82
|
+
batch_op.drop_column("oai_recent_news")
|
|
83
|
+
batch_op.drop_column("oai_news_date")
|
|
84
|
+
batch_op.drop_column("oai_low_price_target")
|
|
85
|
+
batch_op.drop_column("oai_high_price_target")
|
|
86
|
+
|
|
87
|
+
# ### end Alembic commands ###
|