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.
@@ -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.high_price_target is not None:
546
+ if self.oai_high_price_target is not None:
533
547
  self.upside = (
534
- (float(self.high_price_target) - float(last_price))
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>{scrub(t.get('content').replace("\n",""))}</p>" # type: ignore
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
 
@@ -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 # type: ignore
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(series_a=series_a, series_b=series_b, above=above)
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 # type: ignore
471
+ return bbands
@@ -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 top_performers(self) -> "NamedFilterQuery":
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
- "weekly_growth": [1, 100],
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 fundamentals(self) -> "NamedFilterQuery":
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
- "growing_basic_eps",
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
- ["europe", "top_performers", "fundamentals"],
289
- ["us", "top_performers", "fundamentals"],
290
- ["europe", "earnings_date"],
291
- ["us", "earnings_date"],
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
- ["europe", "rsi_oversold_", "macd", "fundamentals"],
302
- ["us", "rsi_oversold_", "macd", "adx", "fundamentals"],
303
- ["europe", "rsi_neutral_", "macd", "adx", "fundamentals"],
304
- ["us", "rsi_neutral_", "macd", "adx", "fundamentals"],
305
- ["europe", "rsi_30", "macd", "adx", "fundamentals"],
306
- ["us", "rsi_30", "macd", "adx", "fundamentals"],
307
- ["europe", "top_performers", "cheap"],
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
- ["europe", "top_performers", "fundamentals"],
321
- ["us", "top_performers", "fundamentals"],
322
- ["europe", "earnings_date"],
323
- ["us", "earnings_date"],
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.news_date.date()}</h2>
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
- tickermood_upgrade(database_url=database_url, no_migration=True)
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
- return SubjectAnalysis.model_validate(row_dict)
368
- return None
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()
@@ -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
@@ -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
- base_news(
190
- database_path=database_path,
191
- job_type=job_type,
192
- symbols=symbols,
193
- headless=headless,
194
- task=task,
195
- )
196
-
197
-
198
- @huey.periodic_task(crontab(minute="0", hour="3"), context=True) # type: ignore
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.3
1
+ Metadata-Version: 2.4
2
2
  Name: bullishpy
3
- Version: 0.67.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.26.0,<0.27.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: pandas-ta (>=0.3.14b0,<0.4.0)
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=Bcupt-qROPddj1hGTNAY8vhu0pnFqNvXoDtUNhRXErY,24217
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=lrbPvTo3GLtylDCfeIKoXCKF5gaY5QFFToNqtuj7xhI,15794
8
- bullish/analysis/indicators.py,sha256=kdjDVhIFiDBhezJJg9ifGheMC6oCR0gC87d_FiW_tjI,28183
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/predefined_filters.py,sha256=skhTUIf_v0EKZBpMgBv2old_Yo78XfLYpJ4MVKljlSU,12116
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=7hWVVd2jBM-Es9S904ck1mtIMSadWgFqwns0bTwrKOU,16720
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=69dq-vvhPQI3aopGIwaBSowBW37EGUnN0f7olVbOmEM,14180
44
- bullish/database/schemas.py,sha256=fQ4RZeOjlFoIor7rjwpisbHRNDd7-zbyDdzNKaiNGQQ,3637
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=R2qVEMyBl9mBlPUO40zXp4vhfLKH7pgl_u2BmAVlD4w,5250
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=7_zKZaLpbmh7XxvjhfWcowdDAp9sEABULB2PSkasfbM,6509
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.67.0.dist-info/LICENSE,sha256=nYb7AJFegu6ndlQhbbk54MjT-GH-0x9RF6Ls-ggJ_g4,1075
61
- bullishpy-0.67.0.dist-info/METADATA,sha256=ld3mWit2JWMYH5qzPMSIq2v1R27fibJo38W4RFRYxtg,3009
62
- bullishpy-0.67.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
63
- bullishpy-0.67.0.dist-info/entry_points.txt,sha256=eaPpmL6vmSBFo0FBtwibCXGqAW4LFJ83whJzT1VjD-0,43
64
- bullishpy-0.67.0.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.3
2
+ Generator: poetry-core 2.2.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any