bullishpy 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of bullishpy might be problematic. Click here for more details.

bullish/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,608 @@
1
+ import logging
2
+ from datetime import date
3
+ from typing import (
4
+ Annotated,
5
+ Any,
6
+ List,
7
+ Optional,
8
+ Sequence,
9
+ Type,
10
+ cast,
11
+ get_args,
12
+ TYPE_CHECKING,
13
+ )
14
+
15
+ import pandas as pd
16
+ import pandas_ta as ta # type: ignore
17
+ from bearish.interface.interface import BearishDbBase # type: ignore
18
+ from bearish.models.assets.equity import BaseEquity # type: ignore
19
+ from bearish.models.base import ( # type: ignore
20
+ DataSourceBase,
21
+ Ticker,
22
+ PriceTracker,
23
+ TrackerQuery,
24
+ FinancialsTracker,
25
+ )
26
+ from bearish.models.financials.balance_sheet import ( # type: ignore
27
+ BalanceSheet,
28
+ QuarterlyBalanceSheet,
29
+ )
30
+ from bearish.models.financials.base import Financials # type: ignore
31
+ from bearish.models.financials.cash_flow import ( # type: ignore
32
+ CashFlow,
33
+ QuarterlyCashFlow,
34
+ )
35
+ from bearish.models.financials.metrics import ( # type: ignore
36
+ FinancialMetrics,
37
+ QuarterlyFinancialMetrics,
38
+ )
39
+ from bearish.models.price.prices import Prices # type: ignore
40
+ from bearish.models.query.query import AssetQuery, Symbols # type: ignore
41
+ from bearish.types import TickerOnlySources # type: ignore
42
+ from pydantic import BaseModel, BeforeValidator, Field, create_model
43
+
44
+ if TYPE_CHECKING:
45
+ from bullish.database.crud import BullishDb
46
+
47
+ QUARTERLY = "quarterly"
48
+ logger = logging.getLogger(__name__)
49
+
50
+
51
+ def to_float(value: Any) -> Optional[float]:
52
+ if value == "None":
53
+ return None
54
+ if value is None:
55
+ return None
56
+ if isinstance(value, str):
57
+ try:
58
+ return float(value)
59
+ except ValueError:
60
+ return None
61
+ return float(value)
62
+
63
+
64
+ def price_growth(prices: pd.DataFrame, days: int, max: bool = False) -> Optional[float]:
65
+ prices_ = prices.copy()
66
+ last_index = prices_.last_valid_index()
67
+ delta = pd.Timedelta(days=days)
68
+ start_index = last_index - delta # type: ignore
69
+
70
+ try:
71
+ closest_index = prices_.index.unique().asof(start_index) # type: ignore
72
+ price = (
73
+ prices_.loc[closest_index].close
74
+ if not max
75
+ else prices_[closest_index:].close.max()
76
+ )
77
+ except Exception as e:
78
+ logger.warning(
79
+ f"""Failing to calculate price growth: {e}.""",
80
+ exc_info=True,
81
+ )
82
+ return None
83
+ return ( # type: ignore
84
+ (prices_.loc[last_index].close - price) * 100 / prices_.loc[last_index].close
85
+ )
86
+
87
+
88
+ def buy_opportunity(
89
+ series_a: pd.Series, series_b: pd.Series # type: ignore
90
+ ) -> Optional[date]:
91
+ sell = ta.cross(series_a=series_a, series_b=series_b)
92
+ buy = ta.cross(series_a=series_b, series_b=series_a)
93
+ if not buy[buy == 1].index.empty and not sell[sell == 1].index.empty:
94
+ last_buy_signal = pd.Timestamp(buy[buy == 1].index[-1])
95
+ last_sell_signal = pd.Timestamp(sell[sell == 1].index[-1])
96
+ if last_buy_signal > last_sell_signal:
97
+ return last_buy_signal
98
+ return None
99
+
100
+
101
+ def perc(data: pd.Series) -> float: # type: ignore
102
+ return cast(float, ((data.iloc[-1] - data.iloc[0]) / data.iloc[0]) * 100)
103
+
104
+
105
+ def yoy(prices: pd.DataFrame) -> pd.Series: # type: ignore
106
+ return prices.close.resample("YE").apply(perc) # type: ignore
107
+
108
+
109
+ def mom(prices: pd.DataFrame) -> pd.Series: # type: ignore
110
+ return prices.close.resample("ME").apply(perc) # type: ignore
111
+
112
+
113
+ def wow(prices: pd.DataFrame) -> pd.Series: # type: ignore
114
+ return prices.close.resample("W").apply(perc) # type: ignore
115
+
116
+
117
+ def _load_data(
118
+ data: Sequence[DataSourceBase], symbol: str, class_: Type[DataSourceBase]
119
+ ) -> pd.DataFrame:
120
+ try:
121
+ records = pd.DataFrame.from_records(
122
+ [f.model_dump() for f in data if f.symbol == symbol]
123
+ )
124
+ return records.set_index("date").sort_index()
125
+ except Exception as e:
126
+ logger.warning(f"Failed to load data from {symbol}: {e}")
127
+ columns = list(class_.model_fields)
128
+ return pd.DataFrame(columns=columns).sort_index()
129
+
130
+
131
+ def _compute_growth(series: pd.Series) -> bool: # type: ignore
132
+ if series.empty:
133
+ return False
134
+ return all(series.pct_change(fill_method=None).dropna() > 0)
135
+
136
+
137
+ def _all_positive(series: pd.Series) -> bool: # type: ignore
138
+ if series.empty:
139
+ return False
140
+ return all(series.dropna() > 0)
141
+
142
+
143
+ def _get_last(data: pd.Series) -> Optional[float]: # type: ignore
144
+ return data.iloc[-1] if not data.empty else None
145
+
146
+
147
+ def _abs(data: pd.Series) -> pd.Series: # type: ignore
148
+ try:
149
+ return abs(data)
150
+ except Exception as e:
151
+ logger.warning(f"Failed to compute absolute value: {e}")
152
+ return data
153
+
154
+
155
+ class TechnicalAnalysis(BaseModel):
156
+ rsi_last_value: Optional[float] = None
157
+ macd_12_26_9_buy_date: Optional[date] = None
158
+ ma_50_200_buy_date: Optional[date] = None
159
+ slope_7: Optional[float] = None
160
+ slope_14: Optional[float] = None
161
+ slope_30: Optional[float] = None
162
+ slope_60: Optional[float] = None
163
+ last_adx: Optional[float] = None
164
+ last_dmp: Optional[float] = None
165
+ last_dmn: Optional[float] = None
166
+ last_price: Annotated[
167
+ Optional[float],
168
+ BeforeValidator(to_float),
169
+ Field(
170
+ default=None,
171
+ ),
172
+ ]
173
+ last_price_date: Annotated[
174
+ Optional[date],
175
+ Field(
176
+ default=None,
177
+ ),
178
+ ]
179
+ year_to_date_growth: Annotated[
180
+ Optional[float],
181
+ Field(
182
+ default=None,
183
+ ),
184
+ ]
185
+ last_52_weeks_growth: Annotated[
186
+ Optional[float],
187
+ Field(
188
+ default=None,
189
+ ),
190
+ ]
191
+ last_week_growth: Annotated[
192
+ Optional[float],
193
+ Field(
194
+ default=None,
195
+ ),
196
+ ]
197
+ last_month_growth: Annotated[
198
+ Optional[float],
199
+ Field(
200
+ default=None,
201
+ ),
202
+ ]
203
+ last_year_growth: Annotated[
204
+ Optional[float],
205
+ Field(
206
+ default=None,
207
+ ),
208
+ ]
209
+ year_to_date_max_growth: Annotated[
210
+ Optional[float],
211
+ Field(
212
+ default=None,
213
+ ),
214
+ ]
215
+ last_week_max_growth: Annotated[
216
+ Optional[float],
217
+ Field(
218
+ default=None,
219
+ ),
220
+ ]
221
+ last_month_max_growth: Annotated[
222
+ Optional[float],
223
+ Field(
224
+ default=None,
225
+ ),
226
+ ]
227
+ last_year_max_growth: Annotated[
228
+ Optional[float],
229
+ Field(
230
+ default=None,
231
+ ),
232
+ ]
233
+ macd_12_26_9_buy: Annotated[
234
+ Optional[float],
235
+ Field(
236
+ default=None,
237
+ ),
238
+ ]
239
+ star_yoy: Annotated[
240
+ Optional[float],
241
+ Field(
242
+ default=None,
243
+ ),
244
+ ]
245
+ star_wow: Annotated[
246
+ Optional[float],
247
+ Field(
248
+ default=None,
249
+ ),
250
+ ]
251
+ star_mom: Annotated[
252
+ Optional[float],
253
+ Field(
254
+ default=None,
255
+ ),
256
+ ]
257
+
258
+ @classmethod
259
+ def from_data(cls, prices: pd.DataFrame) -> "TechnicalAnalysis":
260
+ try:
261
+ last_index = prices.last_valid_index()
262
+ year_to_date_days = (
263
+ last_index
264
+ - pd.Timestamp(year=last_index.year, month=1, day=1, tz="UTC") # type: ignore
265
+ ).days
266
+ year_to_date_growth = price_growth(prices, year_to_date_days)
267
+ last_52_weeks_growth = price_growth(prices=prices, days=399)
268
+ last_week_growth = price_growth(prices=prices, days=7)
269
+ last_month_growth = price_growth(prices=prices, days=31)
270
+ last_year_growth = price_growth(prices=prices, days=365)
271
+ year_to_date_max_growth = price_growth(prices, year_to_date_days, max=True)
272
+ last_week_max_growth = price_growth(prices=prices, days=7, max=True)
273
+ last_month_max_growth = price_growth(prices=prices, days=31, max=True)
274
+ last_year_max_growth = price_growth(prices=prices, days=365, max=True)
275
+ prices.ta.sma(50, append=True)
276
+ prices.ta.sma(200, append=True)
277
+ prices.ta.adx(append=True)
278
+ prices["SLOPE_14"] = ta.linreg(prices.close, slope=True, length=14)
279
+ prices["SLOPE_7"] = ta.linreg(prices.close, slope=True, length=7)
280
+ prices["SLOPE_30"] = ta.linreg(prices.close, slope=True, length=30)
281
+ prices["SLOPE_60"] = ta.linreg(prices.close, slope=True, length=60)
282
+ prices.ta.macd(append=True)
283
+ prices.ta.rsi(append=True)
284
+
285
+ rsi_last_value = prices.RSI_14.iloc[-1]
286
+ macd_12_26_9_buy_date = buy_opportunity(
287
+ prices.MACDs_12_26_9, prices.MACD_12_26_9
288
+ )
289
+ star_yoy = yoy(prices).median()
290
+ star_mom = mom(prices).median()
291
+ star_wow = wow(prices).median()
292
+ try:
293
+ macd_12_26_9_buy = (
294
+ prices.MACD_12_26_9.iloc[-1] > prices.MACDs_12_26_9.iloc[-1]
295
+ )
296
+ except Exception as e:
297
+ logger.warning(
298
+ f"Failing to calculate MACD buy date: {e}", exc_info=True
299
+ )
300
+ macd_12_26_9_buy = None
301
+ ma_50_200_buy_date = buy_opportunity(prices.SMA_200, prices.SMA_50)
302
+ return cls(
303
+ rsi_last_value=rsi_last_value,
304
+ macd_12_26_9_buy_date=macd_12_26_9_buy_date,
305
+ macd_12_26_9_buy=macd_12_26_9_buy,
306
+ ma_50_200_buy_date=ma_50_200_buy_date,
307
+ last_price=prices.close.iloc[-1],
308
+ last_price_date=prices.index[-1],
309
+ last_adx=prices.ADX_14.iloc[-1],
310
+ last_dmp=prices.DMP_14.iloc[-1],
311
+ last_dmn=prices.DMN_14.iloc[-1],
312
+ slope_7=prices.SLOPE_7.iloc[-1],
313
+ slope_14=prices.SLOPE_14.iloc[-1],
314
+ slope_30=prices.SLOPE_30.iloc[-1],
315
+ slope_60=prices.SLOPE_60.iloc[-1],
316
+ year_to_date_growth=year_to_date_growth,
317
+ last_52_weeks_growth=last_52_weeks_growth,
318
+ last_week_growth=last_week_growth,
319
+ last_month_growth=last_month_growth,
320
+ last_year_growth=last_year_growth,
321
+ year_to_date_max_growth=year_to_date_max_growth,
322
+ last_week_max_growth=last_week_max_growth,
323
+ last_month_max_growth=last_month_max_growth,
324
+ last_year_max_growth=last_year_max_growth,
325
+ star_yoy=star_yoy,
326
+ star_mom=star_mom,
327
+ star_wow=star_wow,
328
+ )
329
+ except Exception as e:
330
+ logger.error(f"Failing to calculate technical analysis: {e}", exc_info=True)
331
+ return cls() # type: ignore
332
+
333
+
334
+ class BaseFundamentalAnalysis(BaseModel):
335
+ positive_free_cash_flow: Optional[float] = None
336
+ growing_operating_cash_flow: Optional[float] = None
337
+ operating_cash_flow_is_higher_than_net_income: Optional[float] = None
338
+ mean_capex_ratio: Optional[float] = None
339
+ max_capex_ratio: Optional[float] = None
340
+ min_capex_ratio: Optional[float] = None
341
+ mean_dividend_payout_ratio: Optional[float] = None
342
+ max_dividend_payout_ratio: Optional[float] = None
343
+ min_dividend_payout_ratio: Optional[float] = None
344
+ positive_net_income: Optional[float] = None
345
+ positive_operating_income: Optional[float] = None
346
+ growing_net_income: Optional[float] = None
347
+ growing_operating_income: Optional[float] = None
348
+ positive_diluted_eps: Optional[float] = None
349
+ positive_basic_eps: Optional[float] = None
350
+ growing_basic_eps: Optional[float] = None
351
+ growing_diluted_eps: Optional[float] = None
352
+ positive_debt_to_equity: Optional[float] = None
353
+ positive_return_on_assets: Optional[float] = None
354
+ positive_return_on_equity: Optional[float] = None
355
+ earning_per_share: Optional[float] = None
356
+
357
+ def is_empty(self) -> bool:
358
+ return all(getattr(self, field) is None for field in self.model_fields)
359
+
360
+ @classmethod
361
+ def from_financials(
362
+ cls, financials: "Financials", ticker: Ticker
363
+ ) -> "BaseFundamentalAnalysis":
364
+ return cls._from_financials(
365
+ balance_sheets=financials.balance_sheets,
366
+ financial_metrics=financials.financial_metrics,
367
+ cash_flows=financials.cash_flows,
368
+ ticker=ticker,
369
+ )
370
+
371
+ @classmethod
372
+ def _from_financials(
373
+ cls,
374
+ balance_sheets: List[BalanceSheet] | List[QuarterlyBalanceSheet],
375
+ financial_metrics: List[FinancialMetrics] | List[QuarterlyFinancialMetrics],
376
+ cash_flows: List[CashFlow] | List[QuarterlyCashFlow],
377
+ ticker: Ticker,
378
+ ) -> "BaseFundamentalAnalysis":
379
+ try:
380
+ symbol = ticker.symbol
381
+
382
+ balance_sheet = _load_data(balance_sheets, symbol, BalanceSheet)
383
+ financial = _load_data(financial_metrics, symbol, FinancialMetrics)
384
+ cash_flow = _load_data(cash_flows, symbol, CashFlow)
385
+
386
+ # Debt-to-equity
387
+ debt_to_equity = (
388
+ balance_sheet.total_liabilities / balance_sheet.total_shareholder_equity
389
+ ).dropna()
390
+ positive_debt_to_equity = _all_positive(debt_to_equity)
391
+
392
+ # Add relevant balance sheet data to financials
393
+ financial["total_shareholder_equity"] = balance_sheet[
394
+ "total_shareholder_equity"
395
+ ]
396
+ financial["common_stock_shares_outstanding"] = balance_sheet[
397
+ "common_stock_shares_outstanding"
398
+ ]
399
+
400
+ # EPS and income checks
401
+ earning_per_share = _get_last(
402
+ (
403
+ financial.net_income / financial.common_stock_shares_outstanding
404
+ ).dropna()
405
+ )
406
+ positive_net_income = _all_positive(financial.net_income)
407
+ positive_operating_income = _all_positive(financial.operating_income)
408
+ growing_net_income = _compute_growth(financial.net_income)
409
+ growing_operating_income = _compute_growth(financial.operating_income)
410
+ positive_diluted_eps = _all_positive(financial.diluted_eps)
411
+ positive_basic_eps = _all_positive(financial.basic_eps)
412
+ growing_basic_eps = _compute_growth(financial.basic_eps)
413
+ growing_diluted_eps = _compute_growth(financial.diluted_eps)
414
+
415
+ # Profitability ratios
416
+ return_on_equity = (
417
+ financial.net_income * 100 / financial.total_shareholder_equity
418
+ ).dropna()
419
+ return_on_assets = (
420
+ financial.net_income * 100 / balance_sheet.total_assets
421
+ ).dropna()
422
+ positive_return_on_assets = _all_positive(return_on_assets)
423
+ positive_return_on_equity = _all_positive(return_on_equity)
424
+ # Cash flow analysis
425
+ cash_flow["net_income"] = financial["net_income"]
426
+ free_cash_flow = (
427
+ cash_flow["operating_cash_flow"] - cash_flow["capital_expenditure"]
428
+ )
429
+ positive_free_cash_flow = _all_positive(free_cash_flow)
430
+ growing_operating_cash_flow = _compute_growth(
431
+ cash_flow["operating_cash_flow"]
432
+ )
433
+ operating_income_net_income = cash_flow[
434
+ ["operating_cash_flow", "net_income"]
435
+ ].dropna()
436
+ operating_cash_flow_is_higher_than_net_income = all(
437
+ operating_income_net_income["operating_cash_flow"]
438
+ >= operating_income_net_income["net_income"]
439
+ )
440
+ cash_flow["capex_ratio"] = (
441
+ cash_flow["capital_expenditure"] / cash_flow["operating_cash_flow"]
442
+ ).dropna()
443
+ mean_capex_ratio = cash_flow["capex_ratio"].mean()
444
+ max_capex_ratio = cash_flow["capex_ratio"].max()
445
+ min_capex_ratio = cash_flow["capex_ratio"].min()
446
+ dividend_payout_ratio = (
447
+ _abs(cash_flow["cash_dividends_paid"]) / free_cash_flow
448
+ ).dropna()
449
+ mean_dividend_payout_ratio = dividend_payout_ratio.mean()
450
+ max_dividend_payout_ratio = dividend_payout_ratio.max()
451
+ min_dividend_payout_ratio = dividend_payout_ratio.min()
452
+
453
+ return cls(
454
+ earning_per_share=earning_per_share,
455
+ positive_debt_to_equity=positive_debt_to_equity,
456
+ positive_return_on_assets=positive_return_on_assets,
457
+ positive_return_on_equity=positive_return_on_equity,
458
+ growing_net_income=growing_net_income,
459
+ growing_operating_income=growing_operating_income,
460
+ positive_diluted_eps=positive_diluted_eps,
461
+ positive_basic_eps=positive_basic_eps,
462
+ growing_basic_eps=growing_basic_eps,
463
+ growing_diluted_eps=growing_diluted_eps,
464
+ positive_net_income=positive_net_income,
465
+ positive_operating_income=positive_operating_income,
466
+ positive_free_cash_flow=positive_free_cash_flow,
467
+ growing_operating_cash_flow=growing_operating_cash_flow,
468
+ operating_cash_flow_is_higher_than_net_income=operating_cash_flow_is_higher_than_net_income,
469
+ mean_capex_ratio=mean_capex_ratio,
470
+ max_capex_ratio=max_capex_ratio,
471
+ min_capex_ratio=min_capex_ratio,
472
+ mean_dividend_payout_ratio=mean_dividend_payout_ratio,
473
+ max_dividend_payout_ratio=max_dividend_payout_ratio,
474
+ min_dividend_payout_ratio=min_dividend_payout_ratio,
475
+ )
476
+ except Exception as e:
477
+ logger.error(
478
+ f"Failed to compute fundamental analysis for {ticker}: {e}",
479
+ exc_info=True,
480
+ )
481
+ return cls()
482
+
483
+
484
+ class YearlyFundamentalAnalysis(BaseFundamentalAnalysis):
485
+ ...
486
+
487
+
488
+ fields_with_prefix = {
489
+ f"{QUARTERLY}_{name}": (Optional[float], Field(default=None))
490
+ for name in BaseFundamentalAnalysis.model_fields
491
+ }
492
+
493
+ # Create the new model
494
+ BaseQuarterlyFundamentalAnalysis = create_model( # type: ignore
495
+ "BaseQuarterlyFundamentalAnalysis", **fields_with_prefix
496
+ )
497
+
498
+
499
+ class QuarterlyFundamentalAnalysis(BaseQuarterlyFundamentalAnalysis): # type: ignore
500
+ @classmethod
501
+ def from_quarterly_financials(
502
+ cls, financials: "Financials", ticker: Ticker
503
+ ) -> "QuarterlyFundamentalAnalysis":
504
+ base_financial_analisys = BaseFundamentalAnalysis._from_financials(
505
+ balance_sheets=financials.quarterly_balance_sheets,
506
+ financial_metrics=financials.quarterly_financial_metrics,
507
+ cash_flows=financials.quarterly_cash_flows,
508
+ ticker=ticker,
509
+ )
510
+ return cls.model_validate({f"{QUARTERLY}_{k}": v for k, v in base_financial_analisys.model_dump().items()}) # type: ignore # noqa: E501
511
+
512
+
513
+ class FundamentalAnalysis(YearlyFundamentalAnalysis, QuarterlyFundamentalAnalysis):
514
+ @classmethod
515
+ def from_financials(
516
+ cls, financials: Financials, ticker: Ticker
517
+ ) -> "FundamentalAnalysis":
518
+ yearly_analysis = YearlyFundamentalAnalysis.from_financials(
519
+ financials=financials, ticker=ticker
520
+ )
521
+ quarterly_analysis = QuarterlyFundamentalAnalysis.from_quarterly_financials(
522
+ financials=financials, ticker=ticker
523
+ )
524
+ return FundamentalAnalysis.model_validate(
525
+ yearly_analysis.model_dump() | quarterly_analysis.model_dump()
526
+ )
527
+
528
+
529
+ class AnalysisView(BaseModel):
530
+ sector: Annotated[
531
+ Optional[str],
532
+ Field(
533
+ None,
534
+ description="Broad sector to which the company belongs, "
535
+ "such as 'Real Estate' or 'Technology'",
536
+ ),
537
+ ]
538
+ industry: Annotated[
539
+ Optional[str],
540
+ Field(
541
+ None,
542
+ description="Detailed industry categorization for the company, "
543
+ "like 'Real Estate Management & Development'",
544
+ ),
545
+ ]
546
+ market_capitalization: Annotated[
547
+ Optional[float],
548
+ BeforeValidator(to_float),
549
+ Field(
550
+ default=None,
551
+ description="Market capitalization value",
552
+ ),
553
+ ]
554
+ country: Annotated[
555
+ Optional[str],
556
+ Field(None, description="Country where the company's headquarters is located"),
557
+ ]
558
+ symbol: str = Field(
559
+ description="Unique ticker symbol identifying the company on the stock exchange"
560
+ )
561
+ name: Annotated[
562
+ Optional[str],
563
+ Field(None, description="Full name of the company"),
564
+ ]
565
+
566
+
567
+ class Analysis(AnalysisView, BaseEquity, TechnicalAnalysis, FundamentalAnalysis): # type: ignore
568
+ price_per_earning_ratio: Optional[float] = None
569
+
570
+ @classmethod
571
+ def from_ticker(cls, bearish_db: BearishDbBase, ticker: Ticker) -> "Analysis":
572
+ asset = bearish_db.read_assets(
573
+ AssetQuery(
574
+ symbols=Symbols(equities=[ticker]),
575
+ excluded_sources=get_args(TickerOnlySources),
576
+ )
577
+ )
578
+ equity = asset.get_one_equity()
579
+ financials = Financials.from_ticker(bearish_db, ticker)
580
+ fundamental_analysis = FundamentalAnalysis.from_financials(financials, ticker)
581
+ prices = Prices.from_ticker(bearish_db, ticker)
582
+ technical_analysis = TechnicalAnalysis.from_data(prices.to_dataframe())
583
+ return cls.model_validate(
584
+ equity.model_dump()
585
+ | fundamental_analysis.model_dump()
586
+ | technical_analysis.model_dump()
587
+ | {
588
+ "price_per_earning_ratio": (
589
+ (
590
+ technical_analysis.last_price
591
+ / fundamental_analysis.earning_per_share
592
+ )
593
+ if technical_analysis.last_price is not None
594
+ and fundamental_analysis.earning_per_share != 0
595
+ and fundamental_analysis.earning_per_share is not None
596
+ else None
597
+ )
598
+ }
599
+ )
600
+
601
+
602
+ def run_analysis(bullish_db: "BullishDb") -> None:
603
+ price_trackers = set(bullish_db._read_tracker(TrackerQuery(), PriceTracker))
604
+ finance_trackers = set(bullish_db._read_tracker(TrackerQuery(), FinancialsTracker))
605
+ tickers = list(price_trackers.intersection(finance_trackers))
606
+ for ticker in tickers:
607
+ analysis = Analysis.from_ticker(bullish_db, ticker)
608
+ bullish_db.write_analysis(analysis)