bullishpy 0.67.0__py3-none-any.whl → 0.76.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.
- bullish/analysis/analysis.py +28 -3
- bullish/analysis/functions.py +4 -4
- bullish/analysis/indicators.py +7 -0
- bullish/analysis/openai.py +91 -0
- bullish/analysis/predefined_filters.py +83 -31
- bullish/app/app.py +13 -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/crud.py +53 -4
- bullish/database/schemas.py +9 -0
- bullish/interface/interface.py +7 -1
- bullish/jobs/tasks.py +30 -10
- {bullishpy-0.67.0.dist-info → bullishpy-0.76.0.dist-info}/METADATA +6 -4
- {bullishpy-0.67.0.dist-info → bullishpy-0.76.0.dist-info}/RECORD +19 -14
- {bullishpy-0.67.0.dist-info → bullishpy-0.76.0.dist-info}/WHEEL +1 -1
- {bullishpy-0.67.0.dist-info → bullishpy-0.76.0.dist-info}/entry_points.txt +0 -0
- {bullishpy-0.67.0.dist-info → bullishpy-0.76.0.dist-info/licenses}/LICENSE +0 -0
bullish/analysis/analysis.py
CHANGED
|
@@ -497,6 +497,11 @@ class AnalysisView(BaseModel):
|
|
|
497
497
|
weekly_growth: Optional[float] = None
|
|
498
498
|
monthly_growth: Optional[float] = None
|
|
499
499
|
upside: Optional[float] = None
|
|
500
|
+
oai_high_price_target: Optional[float] = None
|
|
501
|
+
oai_low_price_target: Optional[float] = None
|
|
502
|
+
rsi: Optional[float] = None
|
|
503
|
+
oai_recommendation: Optional[str] = None
|
|
504
|
+
oai_moat: Optional[bool] = None
|
|
500
505
|
|
|
501
506
|
|
|
502
507
|
def json_loads(value: Any) -> Any:
|
|
@@ -527,11 +532,26 @@ class SubjectAnalysis(BaseModel):
|
|
|
527
532
|
] = None
|
|
528
533
|
summary: Annotated[Optional[Dict[str, Any]], BeforeValidator(json_loads)] = None
|
|
529
534
|
upside: Optional[float] = None
|
|
535
|
+
downside: Optional[float] = None
|
|
536
|
+
|
|
537
|
+
oai_high_price_target: Optional[float] = None
|
|
538
|
+
oai_low_price_target: Optional[float] = None
|
|
539
|
+
oai_news_date: Optional[datetime] = None
|
|
540
|
+
oai_recent_news: Optional[str] = None
|
|
541
|
+
oai_recommendation: Optional[str] = None
|
|
542
|
+
oai_explanation: Optional[str] = None
|
|
543
|
+
oai_moat: Optional[bool] = None
|
|
530
544
|
|
|
531
545
|
def compute_upside(self, last_price: float) -> None:
|
|
532
|
-
if self.
|
|
546
|
+
if self.oai_high_price_target is not None:
|
|
533
547
|
self.upside = (
|
|
534
|
-
(float(self.
|
|
548
|
+
(float(self.oai_high_price_target) - float(last_price))
|
|
549
|
+
* 100
|
|
550
|
+
/ float(last_price)
|
|
551
|
+
)
|
|
552
|
+
if self.oai_low_price_target is not None:
|
|
553
|
+
self.downside = (
|
|
554
|
+
(float(last_price) - float(self.oai_low_price_target))
|
|
535
555
|
* 100
|
|
536
556
|
/ float(last_price)
|
|
537
557
|
)
|
|
@@ -541,12 +561,17 @@ class SubjectAnalysis(BaseModel):
|
|
|
541
561
|
return None
|
|
542
562
|
return "".join(
|
|
543
563
|
[
|
|
544
|
-
f"<p>{
|
|
564
|
+
f"<p>{t.get('content').replace("\n","")}</p>" # type: ignore
|
|
545
565
|
for t in self.news_summary
|
|
546
566
|
if t.get("content")
|
|
547
567
|
]
|
|
548
568
|
)
|
|
549
569
|
|
|
570
|
+
def to_date(self) -> Optional[date]:
|
|
571
|
+
if self.news_date:
|
|
572
|
+
return self.news_date.date()
|
|
573
|
+
return None
|
|
574
|
+
|
|
550
575
|
|
|
551
576
|
class Analysis(SubjectAnalysis, AnalysisEarningsDate, AnalysisView, BaseEquity, TechnicalAnalysis, FundamentalAnalysis): # type: ignore
|
|
552
577
|
|
bullish/analysis/functions.py
CHANGED
|
@@ -4,7 +4,7 @@ from typing import Optional, Callable, cast
|
|
|
4
4
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
import pandas as pd
|
|
7
|
-
import pandas_ta as ta
|
|
7
|
+
import pandas_ta as ta
|
|
8
8
|
|
|
9
9
|
from pydantic import BaseModel
|
|
10
10
|
|
|
@@ -18,7 +18,7 @@ except Exception:
|
|
|
18
18
|
def cross_simple(
|
|
19
19
|
series_a: pd.Series, series_b: pd.Series, above: bool = True
|
|
20
20
|
) -> pd.Series:
|
|
21
|
-
crossing = ta.cross(
|
|
21
|
+
crossing = ta.cross(x=series_a, y=series_b, above=above) # type: ignore
|
|
22
22
|
return crossing # type: ignore
|
|
23
23
|
|
|
24
24
|
|
|
@@ -466,6 +466,6 @@ def bollinger_bands(
|
|
|
466
466
|
data: pd.DataFrame, window: int = 20, std_dev: float = 2.0
|
|
467
467
|
) -> pd.DataFrame:
|
|
468
468
|
bbands = ta.bbands(
|
|
469
|
-
data.close, timeperiod=window, nbdevup=std_dev, nbdevdn=std_dev, matype=0
|
|
469
|
+
data.close, timeperiod=window, nbdevup=std_dev, nbdevdn=std_dev, matype=0 # type: ignore
|
|
470
470
|
)
|
|
471
|
-
return bbands
|
|
471
|
+
return bbands
|
bullish/analysis/indicators.py
CHANGED
|
@@ -297,6 +297,13 @@ def indicators_factory() -> List[Indicator]:
|
|
|
297
297
|
type=Optional[date],
|
|
298
298
|
function=lambda d: (d.RSI < 60) & (d.RSI > 30),
|
|
299
299
|
),
|
|
300
|
+
Signal(
|
|
301
|
+
name="RSI",
|
|
302
|
+
description="RSI value",
|
|
303
|
+
type_info="Overbought",
|
|
304
|
+
type=Optional[float],
|
|
305
|
+
function=lambda d: d.RSI,
|
|
306
|
+
),
|
|
300
307
|
],
|
|
301
308
|
),
|
|
302
309
|
Indicator(
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from datetime import date
|
|
5
|
+
from typing import Optional, List, TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
from openai import OpenAI
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from bullish.database.crud import BullishDb
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def prompt(ticker: str) -> str:
|
|
17
|
+
return f"""
|
|
18
|
+
You are a financial analysis assistant.
|
|
19
|
+
|
|
20
|
+
Using the latest reliable public data from the web — including analyst price targets from multiple reputable
|
|
21
|
+
sources — analyze the stock ticker {ticker}.
|
|
22
|
+
|
|
23
|
+
Return ONLY valid JSON matching EXACTLY the schema below — no explanations, no preamble, no markdown, no code
|
|
24
|
+
fences, no extra text:
|
|
25
|
+
|
|
26
|
+
{{
|
|
27
|
+
"high_price_target": float, // Analyst consensus high price target in USD (based on multiple sources)
|
|
28
|
+
"low_price_target": float, // Analyst consensus low price target in USD (based on multiple sources)
|
|
29
|
+
"recent_news": str, // Detailed, multi-sentence summary of recent news affecting the company;
|
|
30
|
+
include credible source names inline
|
|
31
|
+
"recommendation": str, // One of: "Strong Buy", "Buy", "Hold", "Sell", "Strong Sell"
|
|
32
|
+
"explanation": str // Concise explanation for the recommendation above, covering key pros/cons
|
|
33
|
+
for investors
|
|
34
|
+
"moat": bool // Give as a boolean true or false if the company has a strong economic moat
|
|
35
|
+
}}
|
|
36
|
+
|
|
37
|
+
Formatting rules:
|
|
38
|
+
- Output must be a single valid JSON object with no surrounding text or formatting.
|
|
39
|
+
- Use plain numbers for high_price_target and low_price_target (no currency symbols, no commas).
|
|
40
|
+
- All text fields must be professional, investor-oriented, and reference credible named sources in `recent_news`.
|
|
41
|
+
- If exact data is unavailable, estimate based on web search results and note uncertainty in the relevant field.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class OpenAINews(BaseModel):
|
|
46
|
+
symbol: str
|
|
47
|
+
news_date: date = Field(default_factory=date.today)
|
|
48
|
+
high_price_target: Optional[float] = None
|
|
49
|
+
low_price_target: Optional[float] = None
|
|
50
|
+
recent_news: Optional[str] = None
|
|
51
|
+
recommendation: Optional[str] = None
|
|
52
|
+
explanation: Optional[str] = None
|
|
53
|
+
moat: Optional[bool] = None
|
|
54
|
+
|
|
55
|
+
def valid(self) -> bool:
|
|
56
|
+
return bool(
|
|
57
|
+
self.model_dump(
|
|
58
|
+
exclude_none=True,
|
|
59
|
+
exclude_unset=True,
|
|
60
|
+
exclude_defaults=True,
|
|
61
|
+
exclude={"symbol"},
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def from_ticker(cls, ticker: str) -> "OpenAINews":
|
|
67
|
+
if "OPENAI_API_KEY" not in os.environ:
|
|
68
|
+
return cls(symbol=ticker)
|
|
69
|
+
print(f"Fetching OpenAI news for {ticker}...")
|
|
70
|
+
client = OpenAI()
|
|
71
|
+
resp = client.responses.create(
|
|
72
|
+
model="gpt-4o", input=prompt(ticker), tools=[{"type": "web_search"}] # type: ignore
|
|
73
|
+
)
|
|
74
|
+
try:
|
|
75
|
+
return cls.model_validate(json.loads(resp.output_text) | {"symbol": ticker})
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logger.error(f"Failed to parse OpenAI response for {ticker}: {e}")
|
|
78
|
+
return cls(symbol=ticker)
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_tickers(cls, tickers: List[str]) -> List["OpenAINews"]:
|
|
82
|
+
return [cls.from_ticker(t) for t in tickers]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_open_ai_news(bullish_db: "BullishDb", tickers: List[str]) -> bool:
|
|
86
|
+
news = OpenAINews.from_tickers(tickers)
|
|
87
|
+
valid_news = [n for n in news if n.valid()]
|
|
88
|
+
if valid_news:
|
|
89
|
+
bullish_db.write_many_openai_news(valid_news)
|
|
90
|
+
return True
|
|
91
|
+
return False
|
|
@@ -135,17 +135,29 @@ class NamedFilterQuery(FilterQuery):
|
|
|
135
135
|
self.model_dump() | {"name": f"{self.name} ({suffix})", **properties}
|
|
136
136
|
)
|
|
137
137
|
|
|
138
|
-
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":
|
|
139
152
|
properties = {
|
|
140
153
|
"volume_above_average": DATE_THRESHOLD,
|
|
141
154
|
"sma_50_above_sma_200": [
|
|
142
155
|
datetime.date.today() - datetime.timedelta(days=5000),
|
|
143
156
|
datetime.date.today(),
|
|
144
157
|
],
|
|
145
|
-
"
|
|
146
|
-
"monthly_growth": [8, 100],
|
|
158
|
+
"yearly_growth": [30, 100],
|
|
147
159
|
}
|
|
148
|
-
return self._custom_variant("Top Performers", properties)
|
|
160
|
+
return self._custom_variant("Yearly Top Performers", properties)
|
|
149
161
|
|
|
150
162
|
def poor_performers(self) -> "NamedFilterQuery":
|
|
151
163
|
properties = {
|
|
@@ -161,7 +173,7 @@ class NamedFilterQuery(FilterQuery):
|
|
|
161
173
|
}
|
|
162
174
|
return self._custom_variant("Poor Performers", properties)
|
|
163
175
|
|
|
164
|
-
def
|
|
176
|
+
def yearly_fundamentals(self) -> "NamedFilterQuery":
|
|
165
177
|
properties = {
|
|
166
178
|
"income": [
|
|
167
179
|
"positive_operating_income",
|
|
@@ -170,20 +182,63 @@ class NamedFilterQuery(FilterQuery):
|
|
|
170
182
|
"growing_operating_income",
|
|
171
183
|
],
|
|
172
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": [
|
|
195
|
+
"quarterly_positive_operating_income",
|
|
196
|
+
"quarterly_positive_net_income",
|
|
197
|
+
],
|
|
198
|
+
"cash_flow": [
|
|
199
|
+
"quarterly_positive_free_cash_flow",
|
|
200
|
+
],
|
|
201
|
+
"properties": [
|
|
202
|
+
"quarterly_operating_cash_flow_is_higher_than_net_income",
|
|
203
|
+
],
|
|
204
|
+
}
|
|
205
|
+
return self._custom_variant("Quarterly Fundamentals", properties)
|
|
206
|
+
|
|
207
|
+
def growing_quarterly_fundamentals(self) -> "NamedFilterQuery":
|
|
208
|
+
properties = {
|
|
209
|
+
"income": [
|
|
210
|
+
"quarterly_positive_operating_income",
|
|
211
|
+
"quarterly_positive_net_income",
|
|
212
|
+
"quarterly_growing_net_income",
|
|
213
|
+
],
|
|
214
|
+
"cash_flow": [
|
|
215
|
+
"quarterly_positive_free_cash_flow",
|
|
216
|
+
"quarterly_growing_operating_cash_flow",
|
|
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
|
+
],
|
|
173
233
|
"eps": [
|
|
174
|
-
"
|
|
175
|
-
"growing_diluted_eps",
|
|
176
|
-
"positive_basic_eps",
|
|
177
|
-
"positive_diluted_eps",
|
|
234
|
+
"positive_diluted_eps", # or positive_basic_eps if diluted not available
|
|
178
235
|
],
|
|
179
236
|
"properties": [
|
|
180
|
-
"positive_return_on_assets",
|
|
181
237
|
"positive_return_on_equity",
|
|
182
|
-
"positive_debt_to_equity",
|
|
183
238
|
"operating_cash_flow_is_higher_than_net_income",
|
|
184
239
|
],
|
|
185
240
|
}
|
|
186
|
-
return self._custom_variant("Fundamentals", properties)
|
|
241
|
+
return self._custom_variant("Min Fundamentals", properties)
|
|
187
242
|
|
|
188
243
|
def high_growth(self) -> "NamedFilterQuery":
|
|
189
244
|
properties = {"industry": list(get_args(HighGrowthIndustry))}
|
|
@@ -253,7 +308,7 @@ class NamedFilterQuery(FilterQuery):
|
|
|
253
308
|
filter__ = getattr(filter__, attr)()
|
|
254
309
|
filters_.append(filter__)
|
|
255
310
|
|
|
256
|
-
return filters_
|
|
311
|
+
return [self, *filters_]
|
|
257
312
|
|
|
258
313
|
|
|
259
314
|
def load_custom_filters() -> List[NamedFilterQuery]:
|
|
@@ -285,10 +340,10 @@ SMALL_CAP = NamedFilterQuery(
|
|
|
285
340
|
order_by_desc="market_capitalization",
|
|
286
341
|
).variants(
|
|
287
342
|
variants=[
|
|
288
|
-
["
|
|
289
|
-
["
|
|
290
|
-
["
|
|
291
|
-
["
|
|
343
|
+
["week_top_performers", "min_fundamentals"],
|
|
344
|
+
["month_top_performers", "min_fundamentals"],
|
|
345
|
+
["earnings_date", "min_fundamentals"],
|
|
346
|
+
["rsi_oversold_", "min_fundamentals"],
|
|
292
347
|
]
|
|
293
348
|
)
|
|
294
349
|
|
|
@@ -298,16 +353,13 @@ LARGE_CAPS = NamedFilterQuery(
|
|
|
298
353
|
market_capitalization=[1e10, 1e14],
|
|
299
354
|
).variants(
|
|
300
355
|
variants=[
|
|
301
|
-
["
|
|
302
|
-
["
|
|
303
|
-
["
|
|
304
|
-
["
|
|
305
|
-
["
|
|
306
|
-
["
|
|
307
|
-
["
|
|
308
|
-
["us", "top_performers", "cheap"],
|
|
309
|
-
["europe", "earnings_date"],
|
|
310
|
-
["us", "earnings_date"],
|
|
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"],
|
|
311
363
|
]
|
|
312
364
|
)
|
|
313
365
|
|
|
@@ -317,10 +369,10 @@ MID_CAPS = NamedFilterQuery(
|
|
|
317
369
|
market_capitalization=[5e8, 1e10],
|
|
318
370
|
).variants(
|
|
319
371
|
variants=[
|
|
320
|
-
["
|
|
321
|
-
["
|
|
322
|
-
["
|
|
323
|
-
["
|
|
372
|
+
["week_top_performers"],
|
|
373
|
+
["month_top_performers"],
|
|
374
|
+
["earnings_date", "quarterly_fundamentals", "yearly_fundamentals"],
|
|
375
|
+
["rsi_oversold_", "macd", "adx"],
|
|
324
376
|
]
|
|
325
377
|
)
|
|
326
378
|
|
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>
|
|
@@ -422,7 +429,7 @@ def main() -> None:
|
|
|
422
429
|
st.session_state.initialized = True
|
|
423
430
|
bearish_db_ = bearish_db(st.session_state.database_path)
|
|
424
431
|
|
|
425
|
-
charts_tab, jobs_tab = st.tabs(["Charts", "Jobs"])
|
|
432
|
+
charts_tab, jobs_tab, sec_tab = st.tabs(["Charts", "Jobs", "Sec"])
|
|
426
433
|
if "data" not in st.session_state:
|
|
427
434
|
st.session_state.data = load_analysis_data(bearish_db_)
|
|
428
435
|
|
|
@@ -473,6 +480,8 @@ def main() -> None:
|
|
|
473
480
|
use_container_width=True,
|
|
474
481
|
hide_index=True,
|
|
475
482
|
)
|
|
483
|
+
with sec_tab:
|
|
484
|
+
st.plotly_chart(sec(bearish_db_), use_container_width=True)
|
|
476
485
|
|
|
477
486
|
|
|
478
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 ###
|
bullish/database/crud.py
CHANGED
|
@@ -3,7 +3,7 @@ import logging
|
|
|
3
3
|
from datetime import date
|
|
4
4
|
from functools import cached_property
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import TYPE_CHECKING, Any, List, Optional
|
|
6
|
+
from typing import TYPE_CHECKING, Any, List, Optional, Dict
|
|
7
7
|
|
|
8
8
|
import pandas as pd
|
|
9
9
|
from bearish.database.crud import BearishDb # type: ignore
|
|
@@ -22,6 +22,7 @@ from bullish.analysis.constants import Industry, IndustryGroup, Sector, Country
|
|
|
22
22
|
from bullish.analysis.filter import FilteredResults
|
|
23
23
|
from bullish.analysis.indicators import SignalSeries
|
|
24
24
|
from bullish.analysis.industry_views import Type, IndustryView
|
|
25
|
+
|
|
25
26
|
from bullish.database.schemas import (
|
|
26
27
|
AnalysisORM,
|
|
27
28
|
JobTrackerORM,
|
|
@@ -29,6 +30,7 @@ from bullish.database.schemas import (
|
|
|
29
30
|
IndustryViewORM,
|
|
30
31
|
SignalSeriesORM,
|
|
31
32
|
BacktestResultORM,
|
|
33
|
+
OpenAINewsORM,
|
|
32
34
|
)
|
|
33
35
|
from bullish.database.scripts.upgrade import upgrade
|
|
34
36
|
from bullish.exceptions import DatabaseFileNotFoundError
|
|
@@ -38,6 +40,7 @@ from tickermood.database.scripts.upgrade import upgrade as tickermood_upgrade #
|
|
|
38
40
|
|
|
39
41
|
if TYPE_CHECKING:
|
|
40
42
|
from bullish.analysis.backtest import BacktestResult, BacktestResultQuery
|
|
43
|
+
from bullish.analysis.openai import OpenAINews
|
|
41
44
|
|
|
42
45
|
logger = logging.getLogger(__name__)
|
|
43
46
|
|
|
@@ -71,7 +74,11 @@ class BullishDb(BearishDb, BullishDbBase): # type: ignore
|
|
|
71
74
|
logger.info(
|
|
72
75
|
"Running tickermood upgrade to create the subject table in the database."
|
|
73
76
|
)
|
|
74
|
-
|
|
77
|
+
try:
|
|
78
|
+
tickermood_upgrade(database_url=database_url, no_migration=True)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.error(f"failed to update database: {e}")
|
|
81
|
+
print(f"failed to update database: {e}")
|
|
75
82
|
return engine
|
|
76
83
|
|
|
77
84
|
def model_post_init(self, __context: Any) -> None:
|
|
@@ -358,11 +365,53 @@ class BullishDb(BearishDb, BullishDbBase): # type: ignore
|
|
|
358
365
|
LIMIT 1
|
|
359
366
|
"""
|
|
360
367
|
)
|
|
368
|
+
sql_oai = text(
|
|
369
|
+
"""
|
|
370
|
+
SELECT *
|
|
371
|
+
FROM openai
|
|
372
|
+
WHERE symbol = :symbol
|
|
373
|
+
ORDER BY news_date DESC
|
|
374
|
+
LIMIT 1
|
|
375
|
+
"""
|
|
376
|
+
)
|
|
361
377
|
|
|
362
378
|
with Session(self._engine) as session:
|
|
363
379
|
row = session.execute(sql, {"symbol": symbol}).mappings().one_or_none()
|
|
380
|
+
row_oai = (
|
|
381
|
+
session.execute(sql_oai, {"symbol": symbol}).mappings().one_or_none()
|
|
382
|
+
)
|
|
383
|
+
row_dict = {}
|
|
364
384
|
if row:
|
|
365
385
|
row_dict = dict(row)
|
|
366
386
|
row_dict = row_dict | {"news_date": row_dict["date"]}
|
|
367
|
-
|
|
368
|
-
|
|
387
|
+
if row_oai:
|
|
388
|
+
row_dict_oai = dict(row_oai)
|
|
389
|
+
row_dict = row_dict | {
|
|
390
|
+
"oai_news_date": row_dict_oai.get("news_date"),
|
|
391
|
+
"oai_recent_news": row_dict_oai.get("recent_news"),
|
|
392
|
+
"oai_recommendation": row_dict_oai.get("recommendation"),
|
|
393
|
+
"oai_explanation": row_dict_oai.get("explanation"),
|
|
394
|
+
"oai_high_price_target": row_dict_oai.get("high_price_target"),
|
|
395
|
+
"oai_low_price_target": row_dict_oai.get("low_price_target"),
|
|
396
|
+
"oai_moat": row_dict_oai.get("moat"),
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return SubjectAnalysis.model_validate(row_dict)
|
|
400
|
+
|
|
401
|
+
def write_many_openai_news(self, openai_news: List["OpenAINews"]) -> None:
|
|
402
|
+
with Session(self._engine) as session:
|
|
403
|
+
stmt = (
|
|
404
|
+
insert(OpenAINewsORM)
|
|
405
|
+
.prefix_with("OR REPLACE")
|
|
406
|
+
.values([a.model_dump() for a in openai_news])
|
|
407
|
+
)
|
|
408
|
+
session.exec(stmt) # type: ignore
|
|
409
|
+
session.commit()
|
|
410
|
+
|
|
411
|
+
def update_analysis(self, symbol: str, fields: Dict[str, Any]) -> None:
|
|
412
|
+
with Session(self._engine) as session:
|
|
413
|
+
stmt = (
|
|
414
|
+
update(AnalysisORM).where(AnalysisORM.symbol == symbol).values(**fields) # type: ignore
|
|
415
|
+
)
|
|
416
|
+
session.exec(stmt) # type: ignore
|
|
417
|
+
session.commit()
|
bullish/database/schemas.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from datetime import date
|
|
1
2
|
from typing import Dict, Any, List, Optional
|
|
2
3
|
|
|
3
4
|
from sqlmodel import Field, SQLModel
|
|
@@ -7,6 +8,7 @@ from bullish.analysis.backtest import BacktestResult
|
|
|
7
8
|
from bullish.analysis.filter import FilteredResults
|
|
8
9
|
from bullish.analysis.indicators import SignalSeries
|
|
9
10
|
from bullish.analysis.industry_views import IndustryView
|
|
11
|
+
from bullish.analysis.openai import OpenAINews
|
|
10
12
|
|
|
11
13
|
from bullish.jobs.models import JobTracker
|
|
12
14
|
from sqlalchemy import Index
|
|
@@ -22,6 +24,13 @@ dynamic_indexes = tuple(
|
|
|
22
24
|
)
|
|
23
25
|
|
|
24
26
|
|
|
27
|
+
class OpenAINewsORM(SQLModel, OpenAINews, table=True):
|
|
28
|
+
__tablename__ = "openai"
|
|
29
|
+
__table_args__ = {"extend_existing": True} # noqa:RUF012
|
|
30
|
+
symbol: str = Field(primary_key=True)
|
|
31
|
+
news_date: date = Field(primary_key=True)
|
|
32
|
+
|
|
33
|
+
|
|
25
34
|
class AnalysisORM(BaseTable, Analysis, table=True):
|
|
26
35
|
__tablename__ = "analysis"
|
|
27
36
|
__table_args__ = {"extend_existing": True} # noqa:RUF012
|
bullish/interface/interface.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import abc
|
|
2
2
|
import logging
|
|
3
3
|
from datetime import date
|
|
4
|
-
from typing import List, Optional
|
|
4
|
+
from typing import List, Optional, Dict, Any
|
|
5
5
|
|
|
6
6
|
import pandas as pd
|
|
7
7
|
from bearish.interface.interface import BearishDbBase # type: ignore
|
|
@@ -15,6 +15,7 @@ from bullish.analysis.constants import Industry, Sector, IndustryGroup, Country
|
|
|
15
15
|
from bullish.analysis.filter import FilterQuery, FilteredResults
|
|
16
16
|
from bullish.analysis.indicators import SignalSeries
|
|
17
17
|
from bullish.analysis.industry_views import Type, IndustryView
|
|
18
|
+
from bullish.analysis.openai import OpenAINews
|
|
18
19
|
from bullish.jobs.models import JobTracker, JobTrackerStatus, add_icons
|
|
19
20
|
|
|
20
21
|
logger = logging.getLogger(__name__)
|
|
@@ -155,3 +156,8 @@ class BullishDbBase(BearishDbBase): # type: ignore
|
|
|
155
156
|
|
|
156
157
|
@abc.abstractmethod
|
|
157
158
|
def read_subject(self, symbol: str) -> Optional[SubjectAnalysis]: ...
|
|
159
|
+
@abc.abstractmethod
|
|
160
|
+
def write_many_openai_news(self, openai_news: List[OpenAINews]) -> None: ...
|
|
161
|
+
|
|
162
|
+
@abc.abstractmethod
|
|
163
|
+
def update_analysis(self, symbol: str, fields: Dict[str, Any]) -> None: ...
|
bullish/jobs/tasks.py
CHANGED
|
@@ -4,6 +4,7 @@ from typing import Optional, Any, Callable, List
|
|
|
4
4
|
|
|
5
5
|
import pandas as pd
|
|
6
6
|
from bearish.main import Bearish # type: ignore
|
|
7
|
+
from bearish.models.sec.sec import Secs # type: ignore
|
|
7
8
|
from tickermood.main import get_news # type: ignore
|
|
8
9
|
from tickermood.types import DatabaseConfig # type: ignore
|
|
9
10
|
|
|
@@ -15,6 +16,7 @@ from .models import JobTrackerStatus, JobTracker, JobType
|
|
|
15
16
|
from ..analysis.analysis import run_analysis, run_signal_series_analysis
|
|
16
17
|
from ..analysis.backtest import run_many_tests, BackTestConfig
|
|
17
18
|
from ..analysis.industry_views import compute_industry_view
|
|
19
|
+
from ..analysis.openai import get_open_ai_news
|
|
18
20
|
from ..analysis.predefined_filters import predefined_filters, load_custom_filters
|
|
19
21
|
from ..database.crud import BullishDb
|
|
20
22
|
from bullish.analysis.filter import FilterUpdate
|
|
@@ -83,6 +85,9 @@ def _base_update(
|
|
|
83
85
|
series_length=update_query.window_size,
|
|
84
86
|
delay=update_query.data_age_in_days,
|
|
85
87
|
)
|
|
88
|
+
bearish.get_prices_index(series_length=update_query.window_size)
|
|
89
|
+
Secs.upload(bearish._bearish_db)
|
|
90
|
+
Secs.update_values(bearish._bearish_db)
|
|
86
91
|
if update_query.update_financials:
|
|
87
92
|
bearish.update_financials()
|
|
88
93
|
bullish_db = BullishDb(database_path=database_path)
|
|
@@ -186,16 +191,31 @@ def news(
|
|
|
186
191
|
headless: bool = True,
|
|
187
192
|
task: Optional[Task] = None,
|
|
188
193
|
) -> None:
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
194
|
+
bullish_db = BullishDb(database_path=database_path)
|
|
195
|
+
if get_open_ai_news(bullish_db, symbols):
|
|
196
|
+
for symbol in symbols:
|
|
197
|
+
subject = bullish_db.read_subject(symbol)
|
|
198
|
+
if subject:
|
|
199
|
+
logger.debug(
|
|
200
|
+
f"extracting news for {symbol} subject: {subject.model_dump()}"
|
|
201
|
+
)
|
|
202
|
+
try:
|
|
203
|
+
bullish_db.update_analysis(
|
|
204
|
+
symbol,
|
|
205
|
+
subject.model_dump(
|
|
206
|
+
exclude_none=True,
|
|
207
|
+
exclude_unset=True,
|
|
208
|
+
exclude_defaults=True,
|
|
209
|
+
exclude={"symbol"},
|
|
210
|
+
),
|
|
211
|
+
)
|
|
212
|
+
except Exception as e:
|
|
213
|
+
logger.error(f"failed to extract news for {symbol}: {e}")
|
|
214
|
+
print(f"failed to extract news for {symbol}: {e}")
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@huey.periodic_task(crontab(minute="0", hour="8"), context=True) # type: ignore
|
|
199
219
|
def cron_news(
|
|
200
220
|
task: Optional[Task] = None,
|
|
201
221
|
) -> None:
|
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: bullishpy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.76.0
|
|
4
4
|
Summary:
|
|
5
|
+
License-File: LICENSE
|
|
5
6
|
Author: aan
|
|
6
7
|
Author-email: andoludovic.andriamamonjy@gmail.com
|
|
7
8
|
Requires-Python: >=3.12,<3.13
|
|
8
9
|
Classifier: Programming Language :: Python :: 3
|
|
9
10
|
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
-
Requires-Dist: bearishpy (>=0.
|
|
11
|
+
Requires-Dist: bearishpy (>=0.35.0,<0.36.0)
|
|
11
12
|
Requires-Dist: click (>=7.0,<=8.1)
|
|
12
13
|
Requires-Dist: huey (>=2.5.3,<3.0.0)
|
|
13
14
|
Requires-Dist: joblib (>=1.5.1,<2.0.0)
|
|
14
|
-
Requires-Dist:
|
|
15
|
+
Requires-Dist: mysec (>=0.3.0,<0.4.0)
|
|
16
|
+
Requires-Dist: pandas-ta (>=0.4.71b0,<0.5.0)
|
|
15
17
|
Requires-Dist: plotly (>=4.12.0,<6.0.0)
|
|
16
18
|
Requires-Dist: streamlit (>=1.45.1,<2.0.0)
|
|
17
19
|
Requires-Dist: streamlit-file-browser (>=3.2.22,<4.0.0)
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
bullish/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
bullish/analysis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
bullish/analysis/analysis.py,sha256=
|
|
3
|
+
bullish/analysis/analysis.py,sha256=3lZSLpTe4lFBGNrCpXmYBZhwD7o0rXSbQEkLpxJLylA,25130
|
|
4
4
|
bullish/analysis/backtest.py,sha256=x91ek5kOzJHvYq0TmJh1Q8wBDDduIaieE0zDaoZFXew,14325
|
|
5
5
|
bullish/analysis/constants.py,sha256=j3vQwjGhY-4dEEV-TkeKMDUTo2GM7M97Hcpi19LDcFQ,11458
|
|
6
6
|
bullish/analysis/filter.py,sha256=VvQALnYNyYylXkorYR3oGhsF4L_sAUSE7-aop4Trp9o,9326
|
|
7
|
-
bullish/analysis/functions.py,sha256=
|
|
8
|
-
bullish/analysis/indicators.py,sha256=
|
|
7
|
+
bullish/analysis/functions.py,sha256=jw1Tc-YtoyobYhC6AWJH-xXgaczwDZMTfQIES6Y_8qM,15780
|
|
8
|
+
bullish/analysis/indicators.py,sha256=CcDu8mu1jOOS5-3gNHYA9qDA3Ua-6PGUyoio2bDIe48,28435
|
|
9
9
|
bullish/analysis/industry_views.py,sha256=-B4CCAYz2arGQtWTXLLMpox0loO_MGdVQd2ycCRMOQQ,6799
|
|
10
|
-
bullish/analysis/
|
|
10
|
+
bullish/analysis/openai.py,sha256=Fw7A8lFMgSEQFA48Q9GjVpEC3oiBgSHUFi7YO5rzhAc,3444
|
|
11
|
+
bullish/analysis/predefined_filters.py,sha256=E65qrTSaDFuUxoaeZ8D72K5AobumobpQdpcTIF308D4,14053
|
|
11
12
|
bullish/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
bullish/app/app.py,sha256=
|
|
13
|
+
bullish/app/app.py,sha256=FLWwhjGwMVXYfA9EI5RUeQRQGf9Qu7up0ypJgS4FTFE,17367
|
|
13
14
|
bullish/cli.py,sha256=yYqiEQAvOIQ-pTn77RPuE449gwaEGBeQwNHHAJ5yQDM,2739
|
|
14
15
|
bullish/database/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
16
|
bullish/database/alembic/README,sha256=heMzebYwlGhnE8_4CWJ4LS74WoEZjBy-S-mIJRxAEKI,39
|
|
@@ -27,12 +28,16 @@ bullish/database/alembic/versions/49c83f9eb5ac_.py,sha256=kCBItp7KmqpJ03roy5ikQj
|
|
|
27
28
|
bullish/database/alembic/versions/4b0a2f40b7d3_.py,sha256=G0K7w7pOPYjPZkXTB8LWhxoxuWBPcPwOfnubTBtdeEY,1827
|
|
28
29
|
bullish/database/alembic/versions/4ee82b171449_.py,sha256=QtPy5VyZPyZxS7MVkk_wGi3C44PVDoHyJ-9m9fWdqqc,1047
|
|
29
30
|
bullish/database/alembic/versions/5b10ee7604c1_.py,sha256=YlqaagPasR3RKASv7acME1jPS8p26VoTE2BvpOwdCpY,1463
|
|
31
|
+
bullish/database/alembic/versions/65662e214031_.py,sha256=Yq3lOW6liYTYiBaPRcFqVjn3k5z1mWIUXT17bv9ZroY,1596
|
|
32
|
+
bullish/database/alembic/versions/660897c02c00_.py,sha256=Sc_4uJAGheebijw3WzFNHclcWz0YF8vaZKEmVBwglDc,1033
|
|
30
33
|
bullish/database/alembic/versions/6d252e23f543_.py,sha256=izF-ejdXk733INkAokGqjA2U_M0_c1f_ruihZ-cgP7s,1525
|
|
31
34
|
bullish/database/alembic/versions/73564b60fe24_.py,sha256=MTlDRDNHj3E9gK7IMeAzv2UxxxYtWiu3gI_9xTLE-wg,1008
|
|
32
35
|
bullish/database/alembic/versions/79bc71ec6f9e_.py,sha256=4nShut2NEd1F3piSckIIBtke0GEsFAxYw5TZl5YYRzc,1140
|
|
33
36
|
bullish/database/alembic/versions/ae444f338124_.py,sha256=u8RphcniLCQce-HvN666QgCJpLsv6A91-a4R-Nif4bU,3672
|
|
37
|
+
bullish/database/alembic/versions/b36c310f49ec_.py,sha256=L0B3wyo9i0R14_H5fcDAxAm_5P1zIFsHUY888Do-pbI,1379
|
|
34
38
|
bullish/database/alembic/versions/b76079e9845f_.py,sha256=W8eeTABjI9tT1dp3hlK7g7tiKqDhmA8AoUX9Sw-ykLI,1165
|
|
35
39
|
bullish/database/alembic/versions/bf6b86dd5463_.py,sha256=fKB8knCprGmiL6AEyFdhybVmB7QX_W4MPFF9sPzUrSM,1094
|
|
40
|
+
bullish/database/alembic/versions/c828e29e1105_.py,sha256=rO9qwNay8HohSVHIJgYq7VWhtgn-jpF10h98WCu-wjU,3052
|
|
36
41
|
bullish/database/alembic/versions/cc28171c21a4_.py,sha256=ZsHFzqo6cfTXDodxaXRzkoKl0zK2TR15nD4SJeDlRi0,1401
|
|
37
42
|
bullish/database/alembic/versions/d0e58e050845_.py,sha256=x_LS3J27FNyy_WD99uvZzNehly-jpgn9abOYN-VjjZc,1164
|
|
38
43
|
bullish/database/alembic/versions/d663166c531d_.py,sha256=U92l6QXqPniAYrPeu2Bt77ReDbXveLj4aGXtgd806JY,1915
|
|
@@ -40,8 +45,8 @@ bullish/database/alembic/versions/ec25c8fa449f_.py,sha256=8Yts74KEjK4jg20zIo90_0
|
|
|
40
45
|
bullish/database/alembic/versions/ee5baabb35f8_.py,sha256=nBMEY-_C8AsSXVPyaDdUkwrFFo2gxShzJhmrjejDwtc,1632
|
|
41
46
|
bullish/database/alembic/versions/fc191121f522_.py,sha256=0sstF6TpAJ09-Mt-Vek9SdSWksvi4C58a5D92rBtuY8,1894
|
|
42
47
|
bullish/database/alembic/versions/ff0cc4ba40ec_.py,sha256=74lxga54ig_LoNZYK9toJL9iRwGbNRezh1zvO1YI40U,2719
|
|
43
|
-
bullish/database/crud.py,sha256
|
|
44
|
-
bullish/database/schemas.py,sha256=
|
|
48
|
+
bullish/database/crud.py,sha256=-pncRg_YA5y2wE2HELJHiGbeTzmaGF7LjMC8be10qwA,16123
|
|
49
|
+
bullish/database/schemas.py,sha256=HudFJ9lsIkVaEYjQUWammrsDnYSmEe4hOCbim3dN_4A,3946
|
|
45
50
|
bullish/database/scripts/create_revision.py,sha256=rggIf-3koPqJNth8FIg89EOfnIM7a9QrvL8X7UJsP0g,628
|
|
46
51
|
bullish/database/scripts/stamp.py,sha256=PWgVUEBumjNUMjTnGw46qmU3p221LeN-KspnW_gFuu4,839
|
|
47
52
|
bullish/database/scripts/upgrade.py,sha256=-Gz7aFNPEt9y9e1kltqXE76-j_8QeNtet_VlwY5AWjo,806
|
|
@@ -50,15 +55,15 @@ bullish/exceptions.py,sha256=4z_i-dD-CDz1bkGmZH9DOf1L_awlCPCgdUDPF7dhWAI,106
|
|
|
50
55
|
bullish/figures/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
51
56
|
bullish/figures/figures.py,sha256=aeMAZGr8HkcF6CIf8ed4cnxJ1YkOY2-euP5egwm0ELk,4750
|
|
52
57
|
bullish/interface/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
53
|
-
bullish/interface/interface.py,sha256=
|
|
58
|
+
bullish/interface/interface.py,sha256=6uZAY19WNtDRKdOitqzqMEo6JTep2M3HC8iFUKYntHA,5518
|
|
54
59
|
bullish/jobs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
55
60
|
bullish/jobs/app.py,sha256=5MJ5KXUo7JSNAvOPgkpIMasD11VTrjQvGzM7vmCY65E,77
|
|
56
61
|
bullish/jobs/models.py,sha256=rBXxtGFBpgZprrxq5_X2Df-bh8BLYEfw-VLMRucrqa8,784
|
|
57
|
-
bullish/jobs/tasks.py,sha256=
|
|
62
|
+
bullish/jobs/tasks.py,sha256=VuFQ2fmzlP_ayy5PhCzH9YroUSRRnvz2SPomuG3SMD0,7566
|
|
58
63
|
bullish/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
59
64
|
bullish/utils/checks.py,sha256=g-5QXNWNe1_BwHKrc2PtvPiLraL0tqGgxnzG7u-Wkgo,2189
|
|
60
|
-
bullishpy-0.
|
|
61
|
-
bullishpy-0.
|
|
62
|
-
bullishpy-0.
|
|
63
|
-
bullishpy-0.
|
|
64
|
-
bullishpy-0.
|
|
65
|
+
bullishpy-0.76.0.dist-info/METADATA,sha256=xUHUxktlK_C4Es0mHaQ2r6FSkAtcop456DSmZmpVX3g,3069
|
|
66
|
+
bullishpy-0.76.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
67
|
+
bullishpy-0.76.0.dist-info/entry_points.txt,sha256=eaPpmL6vmSBFo0FBtwibCXGqAW4LFJ83whJzT1VjD-0,43
|
|
68
|
+
bullishpy-0.76.0.dist-info/licenses/LICENSE,sha256=nYb7AJFegu6ndlQhbbk54MjT-GH-0x9RF6Ls-ggJ_g4,1075
|
|
69
|
+
bullishpy-0.76.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|