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.

@@ -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 Europe, Us
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=7),
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 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":
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
- "weekly_growth": [1, 100],
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 short_term_profitability(self) -> "NamedFilterQuery":
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("Short-term profitability", properties)
205
+ return self._custom_variant("Quarterly Fundamentals", properties)
185
206
 
186
- def long_term_profitability(self) -> "NamedFilterQuery":
207
+ def growing_quarterly_fundamentals(self) -> "NamedFilterQuery":
187
208
  properties = {
188
209
  "income": [
189
- "growing_net_income",
190
- "growing_operating_income",
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
- "growing_operating_cash_flow",
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
- "growing_basic_eps",
200
- "growing_diluted_eps",
201
- "quarterly_growing_basic_eps",
202
- "quarterly_growing_diluted_eps",
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("Long-term profitability", properties)
206
-
207
- def variants(self) -> List["NamedFilterQuery"]:
208
- variants_ = [
209
- self.country_variant("Europe", list(get_args(Europe))),
210
- self.country_variant("Us", list(get_args(Us))),
211
- self.country_variant("Europe", list(get_args(Europe))).top_performers(),
212
- self.country_variant("Us", list(get_args(Us))).top_performers(),
213
- self.country_variant("Europe", list(get_args(Europe))).poor_performers(),
214
- self.country_variant("Us", list(get_args(Us))).poor_performers(),
215
- self.country_variant("Europe", list(get_args(Europe)))
216
- .update_indicator_filter("RSI 30", "rsi_bullish_crossover_30")
217
- .update_indicator_filter("MACD", "macd_12_26_9_bullish_crossover"),
218
- self.country_variant("Europe", list(get_args(Europe)))
219
- .update_indicator_filter("RSI 40", "rsi_bullish_crossover_40")
220
- .update_indicator_filter("MACD", "macd_12_26_9_bullish_crossover"),
221
- self.country_variant("Europe", list(get_args(Europe)))
222
- .update_indicator_filter("RSI Neutral", "rsi_neutral")
223
- .update_indicator_filter("MACD", "macd_12_26_9_bullish_crossover"),
224
- self.country_variant("Us", list(get_args(Us)))
225
- .update_indicator_filter("RSI 30", "rsi_bullish_crossover_30")
226
- .update_indicator_filter("MACD", "macd_12_26_9_bullish_crossover"),
227
- self.country_variant("Us", list(get_args(Us)))
228
- .update_indicator_filter("RSI 40", "rsi_bullish_crossover_40")
229
- .update_indicator_filter("MACD", "macd_12_26_9_bullish_crossover"),
230
- self.country_variant("Us", list(get_args(Us)))
231
- .update_indicator_filter("RSI Neutral", "rsi_neutral")
232
- .update_indicator_filter("MACD", "macd_12_26_9_bullish_crossover"),
233
- ]
234
- variants_short_term_profitability = [
235
- v.short_term_profitability() for v in variants_
236
- ]
237
- variants_long_term_profitability = [
238
- v.long_term_profitability() for v in variants_
239
- ]
240
- return [
241
- *variants_,
242
- *variants_short_term_profitability,
243
- *variants_long_term_profitability,
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 read_custom_filters(Path(custom_filters_path))
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 caps",
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-caps",
367
+ name="Mid Cap",
285
368
  order_by_desc="market_capitalization",
286
369
  market_capitalization=[5e8, 1e10],
287
- ).variants()
288
-
289
- NEXT_EARNINGS_DATE = NamedFilterQuery(
290
- name="Next Earnings date",
291
- order_by_desc="market_capitalization",
292
- next_earnings_date=[
293
- datetime.date.today(),
294
- datetime.date.today() + timedelta(days=20),
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
- *SEMICONDUCTORS,
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.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>
@@ -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 ###