bullishpy 0.37.0__tar.gz → 0.39.0__tar.gz

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.

Files changed (59) hide show
  1. {bullishpy-0.37.0 → bullishpy-0.39.0}/PKG-INFO +1 -1
  2. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/analysis/analysis.py +51 -2
  3. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/app/app.py +82 -0
  4. bullishpy-0.39.0/bullish/database/alembic/versions/ae444f338124_.py +96 -0
  5. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/crud.py +24 -6
  6. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/schemas.py +5 -1
  7. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/interface/interface.py +4 -1
  8. {bullishpy-0.37.0 → bullishpy-0.39.0}/pyproject.toml +1 -1
  9. {bullishpy-0.37.0 → bullishpy-0.39.0}/README.md +0 -0
  10. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/__init__.py +0 -0
  11. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/analysis/__init__.py +0 -0
  12. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/analysis/backtest.py +0 -0
  13. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/analysis/constants.py +0 -0
  14. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/analysis/filter.py +0 -0
  15. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/analysis/functions.py +0 -0
  16. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/analysis/indicators.py +0 -0
  17. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/analysis/industry_views.py +0 -0
  18. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/analysis/predefined_filters.py +0 -0
  19. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/app/__init__.py +0 -0
  20. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/cli.py +0 -0
  21. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/__init__.py +0 -0
  22. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/alembic/README +0 -0
  23. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/alembic/alembic.ini +0 -0
  24. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/alembic/env.py +0 -0
  25. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/alembic/script.py.mako +0 -0
  26. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/alembic/versions/037dbd721317_.py +0 -0
  27. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/alembic/versions/040b15fba458_.py +0 -0
  28. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/alembic/versions/08ac1116e055_.py +0 -0
  29. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/alembic/versions/11d35a452b40_.py +0 -0
  30. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/alembic/versions/12889a2cbd7d_.py +0 -0
  31. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/alembic/versions/17e51420e7ad_.py +0 -0
  32. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/alembic/versions/49c83f9eb5ac_.py +0 -0
  33. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/alembic/versions/4b0a2f40b7d3_.py +0 -0
  34. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/alembic/versions/5b10ee7604c1_.py +0 -0
  35. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/alembic/versions/6d252e23f543_.py +0 -0
  36. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/alembic/versions/73564b60fe24_.py +0 -0
  37. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/alembic/versions/79bc71ec6f9e_.py +0 -0
  38. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/alembic/versions/b76079e9845f_.py +0 -0
  39. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/alembic/versions/bf6b86dd5463_.py +0 -0
  40. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/alembic/versions/d0e58e050845_.py +0 -0
  41. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/alembic/versions/d663166c531d_.py +0 -0
  42. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/alembic/versions/ec25c8fa449f_.py +0 -0
  43. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/alembic/versions/ee5baabb35f8_.py +0 -0
  44. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/alembic/versions/fc191121f522_.py +0 -0
  45. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/alembic/versions/ff0cc4ba40ec_.py +0 -0
  46. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/scripts/create_revision.py +0 -0
  47. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/scripts/stamp.py +0 -0
  48. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/scripts/upgrade.py +0 -0
  49. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/database/settings.py +0 -0
  50. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/exceptions.py +0 -0
  51. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/figures/__init__.py +0 -0
  52. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/figures/figures.py +0 -0
  53. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/interface/__init__.py +0 -0
  54. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/jobs/__init__.py +0 -0
  55. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/jobs/app.py +0 -0
  56. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/jobs/models.py +0 -0
  57. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/jobs/tasks.py +0 -0
  58. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/utils/__init__.py +0 -0
  59. {bullishpy-0.37.0 → bullishpy-0.39.0}/bullish/utils/checks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: bullishpy
3
- Version: 0.37.0
3
+ Version: 0.39.0
4
4
  Summary:
5
5
  Author: aan
6
6
  Author-email: andoludovic.andriamamonjy@gmail.com
@@ -1,6 +1,8 @@
1
+ import json
1
2
  import logging
3
+ import re
2
4
  import time
3
- from datetime import date
5
+ from datetime import date, datetime
4
6
  from itertools import batched, chain
5
7
  from pathlib import Path
6
8
  from typing import (
@@ -13,6 +15,7 @@ from typing import (
13
15
  get_args,
14
16
  TYPE_CHECKING,
15
17
  ClassVar,
18
+ Dict,
16
19
  )
17
20
 
18
21
  import pandas as pd
@@ -488,12 +491,54 @@ class AnalysisView(BaseModel):
488
491
  default=None,
489
492
  ),
490
493
  ]
494
+ consensus: Optional[str] = None
495
+ recommendation: Optional[str] = None
491
496
  yearly_growth: Optional[float] = None
492
497
  weekly_growth: Optional[float] = None
493
498
  monthly_growth: Optional[float] = None
494
499
 
495
500
 
496
- class Analysis(AnalysisEarningsDate, AnalysisView, BaseEquity, TechnicalAnalysis, FundamentalAnalysis): # type: ignore
501
+ def json_loads(value: Any) -> Any:
502
+ if isinstance(value, str):
503
+ try:
504
+ return json.loads(value)
505
+ except Exception as e:
506
+ logger.debug(e)
507
+ return None
508
+ return value
509
+
510
+
511
+ def scrub(text: str) -> str:
512
+ strip_markup = re.compile(r"[\\`*_{}\[\]()>#+\-.!|~:$;\"\'<>&]").sub
513
+ return strip_markup("", text)
514
+
515
+
516
+ class SubjectAnalysis(BaseModel):
517
+ high_price_target: Optional[float] = None
518
+ low_price_target: Optional[float] = None
519
+ consensus: Optional[str] = None
520
+ reason: Optional[str] = None
521
+ recommendation: Optional[str] = None
522
+ explanation: Optional[str] = None
523
+ news_date: Optional[datetime] = None
524
+ news_summary: Annotated[
525
+ Optional[List[Dict[str, Any]]], BeforeValidator(json_loads)
526
+ ] = None
527
+ summary: Annotated[Optional[Dict[str, Any]], BeforeValidator(json_loads)] = None
528
+
529
+ def to_news(self) -> Optional[str]:
530
+ if not self.news_summary:
531
+ return None
532
+ return "".join(
533
+ [
534
+ f"<p>{scrub(t.get('content').replace("\n",""))}</p>" # type: ignore
535
+ for t in self.news_summary
536
+ if t.get("content")
537
+ ]
538
+ )
539
+
540
+
541
+ class Analysis(SubjectAnalysis, AnalysisEarningsDate, AnalysisView, BaseEquity, TechnicalAnalysis, FundamentalAnalysis): # type: ignore
497
542
 
498
543
  @classmethod
499
544
  def from_ticker(cls, bearish_db: "BullishDb", ticker: Ticker) -> "Analysis":
@@ -503,16 +548,20 @@ class Analysis(AnalysisEarningsDate, AnalysisView, BaseEquity, TechnicalAnalysis
503
548
  excluded_sources=get_args(TickerOnlySources),
504
549
  )
505
550
  )
551
+
506
552
  equity = asset.get_one_equity()
507
553
  financials = Financials.from_ticker(bearish_db, ticker)
508
554
  fundamental_analysis = FundamentalAnalysis.from_financials(financials, ticker)
509
555
  prices = Prices.from_ticker(bearish_db, ticker)
510
556
  technical_analysis = TechnicalAnalysis.from_data(prices.to_dataframe(), ticker)
511
557
  next_earnings_date = bearish_db.read_next_earnings_date(ticker.symbol)
558
+ subject = bearish_db.read_subject(ticker.symbol)
559
+
512
560
  return cls.model_validate(
513
561
  equity.model_dump()
514
562
  | fundamental_analysis.model_dump()
515
563
  | technical_analysis.model_dump()
564
+ | (subject.model_dump() if subject else {})
516
565
  | {
517
566
  "next_earnings_date": next_earnings_date,
518
567
  "price_per_earning_ratio": (
@@ -91,11 +91,13 @@ def on_table_select() -> None:
91
91
  prices = db.read_series(query, months=24)
92
92
  data = Prices(prices=prices).to_dataframe()
93
93
  dates = db.read_dates(symbol)
94
+ subject = db.read_subject(symbol)
94
95
  industry_data = get_industry_comparison_data(db, data, "Mean", industry, country)
95
96
 
96
97
  fig = plot(data, symbol, dates=dates, industry_data=industry_data)
97
98
 
98
99
  st.session_state.ticker_figure = fig
100
+ st.session_state.ticker_news = subject
99
101
 
100
102
 
101
103
  @st.dialog("🔑 Provide database file to continue")
@@ -278,6 +280,7 @@ def filter() -> None:
278
280
  st.session_state.database_path
279
281
  ).read_filter_query(query)
280
282
  st.session_state.ticker_figure = None
283
+ st.session_state.ticker_news = None
281
284
  st.session_state.filter_query = {}
282
285
  st.session_state.query = query
283
286
  st.rerun()
@@ -297,8 +300,87 @@ def dialog_plot_figure() -> None:
297
300
  unsafe_allow_html=True,
298
301
  )
299
302
  st.html("<span class='big-dialog'></span>")
303
+ if st.session_state.ticker_news:
304
+ st.markdown(
305
+ f"""
306
+ <div class="news-hover" >
307
+ 📰 <span class="label">News</span>
308
+ <div class="tooltip">
309
+ <h2>Date: {st.session_state.ticker_news.news_date.date()}</h2>
310
+ <h2>Price targets</h2>
311
+ <p>High price target: {st.session_state.ticker_news.high_price_target}</p>
312
+ <p>Low price target: {st.session_state.ticker_news.low_price_target}</p>
313
+ <h2>Recommendation: {st.session_state.ticker_news.recommendation}</h2>
314
+ <h2>Consensus: {st.session_state.ticker_news.consensus}</h2>
315
+ <h2>Explanation & reasons</h2>
316
+ <p>{st.session_state.ticker_news.explanation}</p>
317
+ <p>{st.session_state.ticker_news.reason}</p>
318
+ <h2>News summaries</h2>
319
+ {st.session_state.ticker_news.to_news()}
320
+ </div>
321
+ </div>
322
+ <style>
323
+ /* Hover target (fixed top-left) */
324
+ .news-hover {{
325
+ position: absolute;
326
+ left: 1rem;
327
+ display: inline-flex;
328
+ align-items: center;
329
+ gap: .4rem;
330
+ font-size: 1.7rem; /* big label */
331
+ font-weight: 600;
332
+ color: #333;
333
+ cursor: pointer;
334
+ user-select: none;
335
+ z-index: 1100;
336
+ }}
337
+ /* Tooltip bubble */
338
+ .news-hover .tooltip {{
339
+ position: absolute;
340
+ top: 110%; /* below the label */
341
+ left: 0;
342
+ width: 840px;
343
+ max-height: 620px;
344
+ overflow-y: auto;
345
+ background: #222;
346
+ color: #fff;
347
+ padding: 1.2rem;
348
+ border-radius: 10px;
349
+ font-size: .95rem;
350
+ line-height: 1.45;
351
+ box-shadow: 0 8px 20px rgba(0,0,0,.4);
352
+ opacity: 0;
353
+ visibility: hidden;
354
+ transition: opacity .25s ease;
355
+ }}
356
+ .news-hover .tooltip hr {{
357
+ border: none;
358
+ border-top: 1px solid #444;
359
+ margin: 1rem 0;
360
+ }}
361
+ /* Show tooltip on hover or keyboard focus */
362
+ .news-hover:hover .tooltip,
363
+ .news-hover:focus-within .tooltip {{
364
+ opacity: 1;
365
+ visibility: visible;
366
+ }}
367
+ /* Little arrow under the bubble */
368
+ .news-hover .tooltip::after {{
369
+ content: "";
370
+ position: absolute;
371
+ top: -10px;
372
+ left: 20px;
373
+ border: 10px solid transparent;
374
+ border-top-color: #222;
375
+ }}
376
+ </style>
377
+ """,
378
+ unsafe_allow_html=True,
379
+ )
380
+
300
381
  st.plotly_chart(st.session_state.ticker_figure, use_container_width=True)
301
382
  st.session_state.ticker_figure = None
383
+ st.session_state.ticker_news = None
302
384
 
303
385
 
304
386
  @st.dialog("⭐ Save filtered results")
@@ -0,0 +1,96 @@
1
+ """
2
+
3
+ Revision ID: ae444f338124
4
+ Revises: d0e58e050845
5
+ Create Date: 2025-08-07 11:07:48.169941
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 = "ae444f338124"
18
+ down_revision: Union[str, None] = "d0e58e050845"
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
+ with op.batch_alter_table("analysis", schema=None) as batch_op:
26
+ batch_op.add_column(
27
+ sa.Column("consensus", sqlmodel.sql.sqltypes.AutoString(), nullable=True)
28
+ )
29
+ batch_op.add_column(
30
+ sa.Column(
31
+ "recommendation", sqlmodel.sql.sqltypes.AutoString(), nullable=True
32
+ )
33
+ )
34
+ batch_op.add_column(sa.Column("high_price_target", sa.Float(), nullable=True))
35
+ batch_op.add_column(sa.Column("low_price_target", sa.Float(), nullable=True))
36
+ batch_op.add_column(
37
+ sa.Column("reason", sqlmodel.sql.sqltypes.AutoString(), nullable=True)
38
+ )
39
+ batch_op.add_column(
40
+ sa.Column("explanation", sqlmodel.sql.sqltypes.AutoString(), nullable=True)
41
+ )
42
+ batch_op.add_column(sa.Column("news_date", sa.DateTime(), nullable=True))
43
+ batch_op.add_column(sa.Column("news_summary", sa.JSON(), nullable=True))
44
+ batch_op.alter_column(
45
+ "summary",
46
+ existing_type=sa.VARCHAR(),
47
+ type_=sa.JSON(),
48
+ existing_nullable=True,
49
+ )
50
+ batch_op.create_index("ix_analysis_consensus", ["consensus"], unique=False)
51
+ batch_op.create_index("ix_analysis_explanation", ["explanation"], unique=False)
52
+ batch_op.create_index(
53
+ "ix_analysis_high_price_target", ["high_price_target"], unique=False
54
+ )
55
+ batch_op.create_index(
56
+ "ix_analysis_low_price_target", ["low_price_target"], unique=False
57
+ )
58
+ batch_op.create_index("ix_analysis_news_date", ["news_date"], unique=False)
59
+ batch_op.create_index(
60
+ "ix_analysis_news_summary", ["news_summary"], unique=False
61
+ )
62
+ batch_op.create_index("ix_analysis_reason", ["reason"], unique=False)
63
+ batch_op.create_index(
64
+ "ix_analysis_recommendation", ["recommendation"], unique=False
65
+ )
66
+
67
+ # ### end Alembic commands ###
68
+
69
+
70
+ def downgrade() -> None:
71
+ # ### commands auto generated by Alembic - please adjust! ###
72
+ with op.batch_alter_table("analysis", schema=None) as batch_op:
73
+ batch_op.drop_index("ix_analysis_recommendation")
74
+ batch_op.drop_index("ix_analysis_reason")
75
+ batch_op.drop_index("ix_analysis_news_summary")
76
+ batch_op.drop_index("ix_analysis_news_date")
77
+ batch_op.drop_index("ix_analysis_low_price_target")
78
+ batch_op.drop_index("ix_analysis_high_price_target")
79
+ batch_op.drop_index("ix_analysis_explanation")
80
+ batch_op.drop_index("ix_analysis_consensus")
81
+ batch_op.alter_column(
82
+ "summary",
83
+ existing_type=sa.JSON(),
84
+ type_=sa.VARCHAR(),
85
+ existing_nullable=True,
86
+ )
87
+ batch_op.drop_column("news_summary")
88
+ batch_op.drop_column("news_date")
89
+ batch_op.drop_column("explanation")
90
+ batch_op.drop_column("reason")
91
+ batch_op.drop_column("low_price_target")
92
+ batch_op.drop_column("high_price_target")
93
+ batch_op.drop_column("recommendation")
94
+ batch_op.drop_column("consensus")
95
+
96
+ # ### end Alembic commands ###
@@ -7,21 +7,21 @@ from typing import TYPE_CHECKING, Any, List, Optional
7
7
 
8
8
  import pandas as pd
9
9
  from bearish.database.crud import BearishDb # type: ignore
10
- from bearish.models.base import Ticker # type: ignore
11
10
  from bearish.database.schemas import EarningsDateORM, EquityORM, PriceORM # type: ignore
12
- from bearish.types import Sources # type: ignore
11
+ from bearish.models.base import Ticker # type: ignore
13
12
  from bearish.models.price.price import Price # type: ignore
14
13
  from bearish.models.price.prices import Prices # type: ignore
14
+ from bearish.types import Sources # type: ignore
15
15
  from pydantic import ConfigDict
16
16
  from sqlalchemy import Engine, create_engine, insert, delete, update
17
+ from sqlalchemy import text
17
18
  from sqlmodel import Session, select
18
19
 
19
- from bullish.analysis.analysis import Analysis
20
-
20
+ from bullish.analysis.analysis import Analysis, SubjectAnalysis
21
21
  from bullish.analysis.constants import Industry, IndustryGroup, Sector, Country
22
+ from bullish.analysis.filter import FilteredResults
22
23
  from bullish.analysis.indicators import SignalSeries
23
24
  from bullish.analysis.industry_views import Type, IndustryView
24
-
25
25
  from bullish.database.schemas import (
26
26
  AnalysisORM,
27
27
  JobTrackerORM,
@@ -32,7 +32,6 @@ from bullish.database.schemas import (
32
32
  )
33
33
  from bullish.database.scripts.upgrade import upgrade
34
34
  from bullish.exceptions import DatabaseFileNotFoundError
35
- from bullish.analysis.filter import FilteredResults
36
35
  from bullish.interface.interface import BullishDbBase
37
36
  from bullish.jobs.models import JobTracker, JobTrackerStatus
38
37
 
@@ -341,3 +340,22 @@ class BullishDb(BearishDb, BullishDbBase): # type: ignore
341
340
  if result:
342
341
  return result.date() # type: ignore
343
342
  return None
343
+
344
+ def read_subject(self, symbol: str) -> Optional[SubjectAnalysis]:
345
+ sql = text(
346
+ """
347
+ SELECT *
348
+ FROM subject
349
+ WHERE symbol = :symbol
350
+ ORDER BY date DESC
351
+ LIMIT 1
352
+ """
353
+ )
354
+
355
+ with Session(self._engine) as session:
356
+ row = session.execute(sql, {"symbol": symbol}).mappings().one_or_none()
357
+ if row:
358
+ row_dict = dict(row)
359
+ row_dict = row_dict | {"news_date": row_dict["date"]}
360
+ return SubjectAnalysis.model_validate(row_dict)
361
+ return None
@@ -1,4 +1,4 @@
1
- from typing import Dict, Any
1
+ from typing import Dict, Any, List, Optional
2
2
 
3
3
  from sqlmodel import Field, SQLModel
4
4
  from sqlalchemy import Column, JSON
@@ -25,6 +25,10 @@ dynamic_indexes = tuple(
25
25
  class AnalysisORM(BaseTable, Analysis, table=True):
26
26
  __tablename__ = "analysis"
27
27
  __table_args__ = {"extend_existing": True} # noqa:RUF012
28
+ news_summary: Optional[List[Dict[str, Any]]] = Field(
29
+ default=None, sa_column=Column(JSON)
30
+ )
31
+ summary: Optional[Dict[str, Any]] = Field(default=None, sa_column=Column(JSON))
28
32
 
29
33
 
30
34
  AnalysisORM.__table_args__ = tuple( # type: ignore # noqa: RUF005
@@ -9,7 +9,7 @@ from bearish.models.base import Ticker # type: ignore
9
9
  from bearish.types import Sources # type: ignore
10
10
 
11
11
 
12
- from bullish.analysis.analysis import Analysis, AnalysisView
12
+ from bullish.analysis.analysis import Analysis, AnalysisView, SubjectAnalysis
13
13
  from bullish.analysis.backtest import BacktestResult, BacktestResultQuery
14
14
  from bullish.analysis.constants import Industry, Sector, IndustryGroup, Country
15
15
  from bullish.analysis.filter import FilterQuery, FilteredResults
@@ -152,3 +152,6 @@ class BullishDbBase(BearishDbBase): # type: ignore
152
152
 
153
153
  @abc.abstractmethod
154
154
  def read_next_earnings_date(self, symbol: str) -> Optional[date]: ...
155
+
156
+ @abc.abstractmethod
157
+ def read_subject(self, symbol: str) -> Optional[SubjectAnalysis]: ...
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "bullishpy"
3
- version = "0.37.0"
3
+ version = "0.39.0"
4
4
  description = ""
5
5
  authors = ["aan <andoludovic.andriamamonjy@gmail.com>"]
6
6
  readme = "README.md"
File without changes
File without changes