Premchan369 commited on
Commit
250e0ee
·
verified ·
1 Parent(s): 34072a0

Add fundamentals overlay — valuation, quality, growth metrics from yfinance info

Browse files
Files changed (1) hide show
  1. fundamentals_overlay.py +290 -0
fundamentals_overlay.py ADDED
@@ -0,0 +1,290 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fundamentals Overlay v1.0 — Valuation, Quality & Growth Metrics
2
+ Extracts PE, PEG, ROE, debt/equity, FCF yield, growth estimates from yfinance.
3
+ Maps raw metrics to 0-100 scoring for the multi-factor engine.
4
+ """
5
+ import yfinance as yf
6
+ import numpy as np
7
+ from typing import Dict, Optional
8
+ from datetime import datetime
9
+
10
+ # Sector median PE ratios (US market, approximate)
11
+ SECTOR_MEDIAN_PE = {
12
+ 'Technology': 25.0,
13
+ 'Healthcare': 22.0,
14
+ 'Financial Services': 15.0,
15
+ 'Industrials': 18.0,
16
+ 'Consumer Discretionary': 20.0,
17
+ 'Consumer Staples': 20.0,
18
+ 'Energy': 12.0,
19
+ 'Utilities': 18.0,
20
+ 'Real Estate': 18.0,
21
+ 'Basic Materials': 14.0,
22
+ 'Communication Services': 18.0,
23
+ }
24
+
25
+ # Default when sector unknown
26
+ DEFAULT_SECTOR_PE = 18.0
27
+
28
+
29
+ class FundamentalsOverlay:
30
+ """Pull fundamentals from yfinance info and score for multi-factor engine."""
31
+
32
+ def __init__(self):
33
+ self._cache = {} # ticker -> (info_dict, timestamp)
34
+ self._cache_ttl = 3600 # 1 hour
35
+
36
+ def fetch_info(self, ticker: str) -> Optional[Dict]:
37
+ """Fetch yfinance info with caching."""
38
+ now = datetime.now()
39
+ if ticker in self._cache:
40
+ info, ts = self._cache[ticker]
41
+ if (now - ts).total_seconds() < self._cache_ttl:
42
+ return info
43
+
44
+ try:
45
+ info = yf.Ticker(ticker).info
46
+ if not info or info.get('trailingPE') is None and info.get('forwardPE') is None:
47
+ return None
48
+ self._cache[ticker] = (info, now)
49
+ return info
50
+ except Exception:
51
+ return None
52
+
53
+ def extract_metrics(self, ticker: str) -> Dict:
54
+ """Extract all relevant fundamentals."""
55
+ info = self.fetch_info(ticker)
56
+ if not info:
57
+ return self._default_metrics(ticker)
58
+
59
+ sector = info.get('sector', 'Unknown')
60
+ industry = info.get('industry', 'Unknown')
61
+ sector_pe = SECTOR_MEDIAN_PE.get(sector, DEFAULT_SECTOR_PE)
62
+
63
+ # Valuation
64
+ pe_trailing = info.get('trailingPE')
65
+ pe_forward = info.get('forwardPE')
66
+ peg_ratio = info.get('pegRatio')
67
+ ps_ratio = info.get('priceToSalesTrailing12Months')
68
+ pb_ratio = info.get('priceToBook')
69
+ ev_ebitda = info.get('enterpriseToEbitda')
70
+
71
+ # Quality
72
+ roe = info.get('returnOnEquity')
73
+ roa = info.get('returnOnAssets')
74
+ debt_equity = info.get('debtToEquity')
75
+ current_ratio = info.get('currentRatio')
76
+ gross_margin = info.get('grossMargins')
77
+ operating_margin = info.get('operatingMargins')
78
+ profit_margin = info.get('profitMargins')
79
+
80
+ # Growth
81
+ revenue_growth = info.get('revenueGrowth')
82
+ earnings_growth = info.get('earningsGrowth')
83
+ earnings_qtr_growth = info.get('earningsQuarterlyGrowth')
84
+ est_growth = info.get('earningsGrowth') # Forward estimate
85
+ book_value_growth = None # Not directly available
86
+
87
+ # Cash Flow
88
+ fcf = info.get('freeCashflow')
89
+ market_cap = info.get('marketCap')
90
+ fcf_yield = (fcf / market_cap) if fcf and market_cap else None
91
+
92
+ # Dividend
93
+ div_yield = info.get('dividendYield')
94
+ payout_ratio = info.get('payoutRatio')
95
+
96
+ # Price & Performance
97
+ price = info.get('currentPrice') or info.get('regularMarketPrice')
98
+ fifty_two_high = info.get('fiftyTwoWeekHigh')
99
+ fifty_two_low = info.get('fiftyTwoWeekLow')
100
+ beta = info.get('beta')
101
+ eps = info.get('trailingEps')
102
+
103
+ # Insider / Institutional
104
+ held_insiders = info.get('heldPercentInsiders')
105
+ held_institutions = info.get('heldPercentInstitutions')
106
+ short_ratio = info.get('shortRatio')
107
+ short_pct = info.get('shortPercentOfFloat')
108
+
109
+ return {
110
+ 'ticker': ticker,
111
+ 'sector': sector,
112
+ 'industry': industry,
113
+ 'sector_median_pe': sector_pe,
114
+ # Valuation
115
+ 'pe_trailing': pe_trailing,
116
+ 'pe_forward': pe_forward,
117
+ 'peg_ratio': peg_ratio,
118
+ 'ps_ratio': ps_ratio,
119
+ 'pb_ratio': pb_ratio,
120
+ 'ev_ebitda': ev_ebitda,
121
+ # Quality
122
+ 'roe': roe,
123
+ 'roa': roa,
124
+ 'debt_equity': debt_equity,
125
+ 'current_ratio': current_ratio,
126
+ 'gross_margin': gross_margin,
127
+ 'operating_margin': operating_margin,
128
+ 'profit_margin': profit_margin,
129
+ # Growth
130
+ 'revenue_growth': revenue_growth,
131
+ 'earnings_growth': earnings_growth,
132
+ 'earnings_qtr_growth': earnings_qtr_growth,
133
+ 'est_growth': est_growth,
134
+ 'fcf_yield': fcf_yield,
135
+ # Dividend
136
+ 'dividend_yield': div_yield,
137
+ 'payout_ratio': payout_ratio,
138
+ # Risk
139
+ 'beta': beta,
140
+ 'price': price,
141
+ 'fifty_two_week_high': fifty_two_high,
142
+ 'fifty_two_week_low': fifty_two_low,
143
+ 'eps': eps,
144
+ # Ownership
145
+ 'held_insiders': held_insiders,
146
+ 'held_institutions': held_institutions,
147
+ 'short_ratio': short_ratio,
148
+ 'short_pct': short_pct,
149
+ }
150
+
151
+ def _default_metrics(self, ticker: str) -> Dict:
152
+ """Default when yfinance info unavailable."""
153
+ return {
154
+ 'ticker': ticker,
155
+ 'sector': 'Unknown',
156
+ 'pe_trailing': None, 'pe_forward': None,
157
+ 'peg_ratio': None, 'ps_ratio': None, 'pb_ratio': None,
158
+ 'roe': None, 'debt_equity': None,
159
+ 'revenue_growth': None, 'est_growth': None,
160
+ 'fcf_yield': None, 'beta': None,
161
+ }
162
+
163
+ def score_fundamentals(self, metrics: Dict) -> Dict:
164
+ """Score fundamentals 0-100 for multi-factor engine."""
165
+ score = 50.0
166
+ sector_pe = metrics.get('sector_median_pe', DEFAULT_SECTOR_PE)
167
+
168
+ # Valuation (30 points)
169
+ pe = metrics.get('pe_forward') or metrics.get('pe_trailing')
170
+ if pe:
171
+ if pe < 10: score += 30
172
+ elif pe < sector_pe * 0.6: score += 25
173
+ elif pe < sector_pe * 0.8: score += 15
174
+ elif pe < sector_pe: score += 8
175
+ elif pe < sector_pe * 1.2: score += 0
176
+ elif pe < sector_pe * 1.5: score -= 15
177
+ else: score -= 25
178
+
179
+ peg = metrics.get('peg_ratio')
180
+ if peg and peg > 0:
181
+ if peg < 0.8: score += 20
182
+ elif peg < 1.0: score += 15
183
+ elif peg < 1.5: score += 5
184
+ elif peg > 2.5: score -= 20
185
+ elif peg > 2.0: score -= 10
186
+
187
+ pb = metrics.get('pb_ratio')
188
+ if pb:
189
+ if pb < 1.0: score += 10
190
+ elif pb < 2.0: score += 5
191
+ elif pb > 10: score -= 15
192
+ elif pb > 5: score -= 10
193
+
194
+ # Quality (30 points)
195
+ roe = metrics.get('roe')
196
+ if roe:
197
+ if roe > 0.25: score += 30
198
+ elif roe > 0.20: score += 25
199
+ elif roe > 0.15: score += 20
200
+ elif roe > 0.10: score += 10
201
+ elif roe < 0.05: score -= 15
202
+
203
+ de = metrics.get('debt_equity')
204
+ if de is not None:
205
+ if de < 0.5: score += 15
206
+ elif de < 1.0: score += 10
207
+ elif de > 3.0: score -= 20
208
+ elif de > 2.0: score -= 15
209
+ elif de > 1.5: score -= 10
210
+
211
+ gm = metrics.get('gross_margin')
212
+ if gm:
213
+ if gm > 0.50: score += 10
214
+ elif gm > 0.30: score += 5
215
+
216
+ # Growth (25 points)
217
+ rev_g = metrics.get('revenue_growth')
218
+ if rev_g:
219
+ if rev_g > 0.30: score += 25
220
+ elif rev_g > 0.20: score += 20
221
+ elif rev_g > 0.10: score += 15
222
+ elif rev_g > 0.05: score += 8
223
+ elif rev_g < 0: score -= 15
224
+
225
+ earn_g = metrics.get('est_growth') or metrics.get('earnings_growth')
226
+ if earn_g:
227
+ if earn_g > 0.25: score += 20
228
+ elif earn_g > 0.15: score += 15
229
+ elif earn_g > 0.05: score += 5
230
+ elif earn_g < -0.05: score -= 15
231
+
232
+ # Cash Flow (15 points)
233
+ fcf_y = metrics.get('fcf_yield')
234
+ if fcf_y:
235
+ if fcf_y > 0.08: score += 15
236
+ elif fcf_y > 0.05: score += 10
237
+ elif fcf_y > 0.02: score += 5
238
+ elif fcf_y < 0: score -= 15
239
+
240
+ # Risk adjustment (beta penalty)
241
+ beta = metrics.get('beta')
242
+ if beta:
243
+ if beta > 2.0: score -= 10
244
+ elif beta > 1.5: score -= 5
245
+
246
+ return {
247
+ 'fundamental_score': max(0, min(100, round(score, 1))),
248
+ 'metrics': metrics,
249
+ 'category_scores': {
250
+ 'valuation_raw': pe if pe else 0,
251
+ 'peg_raw': peg if peg else 0,
252
+ 'roe_raw': roe if roe else 0,
253
+ 'growth_raw': earn_g if earn_g else 0,
254
+ 'fcf_yield_raw': fcf_y if fcf_y else 0,
255
+ }
256
+ }
257
+
258
+ def full_analysis(self, ticker: str) -> Dict:
259
+ """Complete fundamentals pipeline for a ticker."""
260
+ metrics = self.extract_metrics(ticker)
261
+ scored = self.score_fundamentals(metrics)
262
+
263
+ # Interpretation
264
+ score = scored['fundamental_score']
265
+ if score > 80:
266
+ interpretation = 'Excellent fundamentals — strong valuation + quality + growth'
267
+ elif score > 65:
268
+ interpretation = 'Good fundamentals — attractive on at least two dimensions'
269
+ elif score > 50:
270
+ interpretation = 'Average fundamentals — fairly priced, no edge'
271
+ elif score > 35:
272
+ interpretation = 'Weak fundamentals — overvalued or declining quality'
273
+ else:
274
+ interpretation = 'Poor fundamentals — avoid or short candidate'
275
+
276
+ scored['interpretation'] = interpretation
277
+ scored['ticker'] = ticker
278
+ scored['timestamp'] = datetime.now().isoformat()
279
+ return scored
280
+
281
+
282
+ if __name__ == '__main__':
283
+ fo = FundamentalsOverlay()
284
+ result = fo.full_analysis('AAPL')
285
+ print(f"Fundamental Score: {result['fundamental_score']}/100")
286
+ print(f"Interpretation: {result['interpretation']}")
287
+ print(f"Sector: {result['metrics'].get('sector', 'N/A')}")
288
+ print(f"PE: {result['metrics'].get('pe_forward', result['metrics'].get('pe_trailing', 'N/A'))}")
289
+ print(f"ROE: {result['metrics'].get('roe', 'N/A')}")
290
+ print(f"Growth: {result['metrics'].get('est_growth', result['metrics'].get('earnings_growth', 'N/A'))}")