eshan6704 commited on
Commit
af90ca9
·
verified ·
1 Parent(s): a2eb59d

Create yahooinfo.py

Browse files
Files changed (1) hide show
  1. app/yahooinfo.py +308 -0
app/yahooinfo.py ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ==============================
2
+ # Imports
3
+ # ==============================
4
+ import yfinance as yf
5
+ import pandas as pd
6
+ import traceback
7
+ from datetime import datetime, timezone
8
+
9
+ from .persist import exists, load, save
10
+
11
+ # ==============================
12
+ # Icons
13
+ # ==============================
14
+ MAIN_ICONS = {
15
+ "Price / Volume": "📈",
16
+ "Fundamentals": "📊",
17
+ "Trend": "📈",
18
+ "Signals": "🧠",
19
+ "Company Profile": "🏢",
20
+ "Management": "👔"
21
+ }
22
+
23
+ # ==============================
24
+ # Short names
25
+ # ==============================
26
+ SHORT_NAMES = {
27
+ "regularMarketPrice": "Price",
28
+ "regularMarketChange": "Chg",
29
+ "regularMarketChangePercent": "Chg%",
30
+ "regularMarketPreviousClose": "Prev",
31
+ "regularMarketOpen": "Open",
32
+ "regularMarketDayHigh": "High",
33
+ "regularMarketDayLow": "Low",
34
+ "regularMarketVolume": "Vol",
35
+ "averageDailyVolume10Day": "Avg Vol 10D",
36
+ "averageDailyVolume3Month": "Avg Vol 3M",
37
+ "fiftyDayAverage": "50DMA",
38
+ "twoHundredDayAverage": "200DMA",
39
+ "fiftyTwoWeekLow": "52W Low",
40
+ "fiftyTwoWeekHigh": "52W High",
41
+ "mostRecentQuarter":"Recent Q",
42
+ "lastFiscalYearEnd":"FY End"
43
+ }
44
+
45
+ # ==============================
46
+ # Price / Volume Sub-Groups
47
+ # ==============================
48
+ PRICE_VOLUME_GROUPS = {
49
+ "Market Price": ["Price","Chg","Chg%","Prev","Open"],
50
+ "Intraday Range": ["High","Low"],
51
+ "Volume & Liquidity": ["Vol","Avg Vol 10D","Avg Vol 3M"],
52
+ "Moving Averages": ["50DMA","200DMA"],
53
+ "52W Range": ["52W Low","52W High"]
54
+ }
55
+
56
+ PIN_PRICE = ["Price","Chg","Chg%","Prev","Open"]
57
+ PIN_FUND = ["MCap","PE","PB","EPS","ROE","ROA","Margin","D/E","Recent Q","FY End"]
58
+
59
+ # ==============================
60
+ # Noise keys
61
+ # ==============================
62
+ NOISE_KEYS = {
63
+ "maxAge","priceHint","triggerable",
64
+ "customPriceAlertConfidence",
65
+ "sourceInterval","exchangeDataDelayedBy",
66
+ "esgPopulated"
67
+ }
68
+
69
+ # ==============================
70
+ # Low-level Yahoo fetch
71
+ # ==============================
72
+ def yfinfo(symbol):
73
+ try:
74
+ t = yf.Ticker(symbol + ".NS")
75
+ info = t.info
76
+ return info if isinstance(info, dict) else {}
77
+ except Exception as e:
78
+ return {"__error__": str(e)}
79
+
80
+ # ==============================
81
+ # Formatting
82
+ # ==============================
83
+ def human_number(n):
84
+ try:
85
+ n = float(n)
86
+ if abs(n) >= 1e7: return f"{n/1e7:.2f}Cr"
87
+ if abs(n) >= 1e5: return f"{n/1e5:.2f}L"
88
+ if abs(n) >= 1e3: return f"{n/1e3:.2f}K"
89
+ return f"{n:,.2f}"
90
+ except:
91
+ return str(n)
92
+
93
+ DATE_KEYWORDS = ("date", "time", "timestamp", "fiscal", "quarter","earnings","dividend")
94
+ def looks_like_unix_ts(v):
95
+ try:
96
+ v = int(v)
97
+ return (946684800 <= v <= 4102444800 or 946684800000 <= v <= 4102444800000)
98
+ except:
99
+ return False
100
+ def unix_to_dt(v):
101
+ v = int(v)
102
+ if v > 10**12: v //= 1000
103
+ return datetime.fromtimestamp(v, tz=timezone.utc)
104
+ def fy_quarter_label(dt):
105
+ y, m = dt.year, dt.month
106
+ if m >= 4:
107
+ fy = y + 1
108
+ q = (m - 1)//3
109
+ else:
110
+ fy = y
111
+ q = (m + 8)//3
112
+ return f"Q{q} FY{str(fy)[-2:]}"
113
+ def format_value(k, v):
114
+ lk = k.lower()
115
+ # Date / Quarter
116
+ if isinstance(v,(int,float)) and looks_like_unix_ts(v):
117
+ if any(x in lk for x in DATE_KEYWORDS):
118
+ dt = unix_to_dt(v)
119
+ if "quarter" in lk:
120
+ return fy_quarter_label(dt)
121
+ return dt.strftime("%d %b %Y")
122
+ # Numbers
123
+ if isinstance(v,(int,float)):
124
+ cls = "pos" if v>0 else "neg" if v<0 else ""
125
+ if "percent" in lk:
126
+ return f'<span class="{cls}">{v:.2f}%</span>'
127
+ if any(x in lk for x in ["marketcap","revenue","income","value","cap","enterprise"]):
128
+ return f'<span class="{cls}">₹{human_number(v)}</span>'
129
+ return f'<span class="{cls}">{human_number(v)}</span>'
130
+ return v
131
+
132
+ # ==============================
133
+ # HTML Helpers
134
+ # ==============================
135
+ def column_layout(html):
136
+ return f"""
137
+ <style>
138
+ .grid{{display:grid;gap:10px;grid-template-columns:repeat(3,1fr);}}
139
+ @media(max-width:1024px){{.grid{{grid-template-columns:repeat(2,1fr);}}}}
140
+ @media(max-width:640px){{.grid{{grid-template-columns:1fr;}}}}
141
+ .pos{{color:#0a7d32;font-weight:600;}}
142
+ .neg{{color:#b00020;font-weight:600;}}
143
+ </style>
144
+ <div class="grid">{html}</div>
145
+ """
146
+ def html_card(title,body,mini=False):
147
+ font = "12px" if mini else "14px"
148
+ pad = "6px" if mini else "10px"
149
+ return f"""
150
+ <div style="background:#e6f0fa;border:1px solid #a3c0e0;border-radius:8px;padding:{pad};
151
+ font-size:{font};margin-bottom:6px;">
152
+ <div style="font-weight:600;margin-bottom:6px;">{title}</div>{body}
153
+ </div>
154
+ """
155
+ def make_table(df):
156
+ return "".join(
157
+ f"""<div style="display:flex;justify-content:space-between;border-bottom:1px dashed #bcd0ea;padding:2px 0;">
158
+ <span>{r.Field}</span><span>{r.Value}</span></div>"""
159
+ for r in df.itertuples()
160
+ )
161
+ def collapsible(title,body):
162
+ return f"""
163
+ <details open>
164
+ <summary style="cursor:pointer;font-weight:600;font-size:15px;padding:6px 0;">
165
+ {title}
166
+ </summary>{body}
167
+ </details>
168
+ """
169
+
170
+ # ==============================
171
+ # Data Helpers
172
+ # ==============================
173
+ def build_df_from_dict(data):
174
+ rows = [(SHORT_NAMES.get(k,k[:16]), format_value(k,v)) for k,v in data.items() if k not in NOISE_KEYS]
175
+ return pd.DataFrame(rows,columns=["Field","Value"])
176
+
177
+ def resolve_duplicates(data):
178
+ DUP = {
179
+ "price":["regularMarketPrice","currentPrice"],
180
+ "prev":["regularMarketPreviousClose","previousClose"],
181
+ "open":["regularMarketOpen","open"],
182
+ "high":["regularMarketDayHigh","dayHigh"],
183
+ "low":["regularMarketDayLow","dayLow"],
184
+ "volume":["regularMarketVolume","volume"]
185
+ }
186
+ resolved, used = {}, set()
187
+ for keys in DUP.values():
188
+ for k in keys:
189
+ if k in data:
190
+ resolved[k] = data[k]
191
+ used.update(keys)
192
+ break
193
+ for k,v in data.items():
194
+ if k not in used:
195
+ resolved[k] = v
196
+ return resolved
197
+
198
+ def classify(k,v):
199
+ lk = k.lower()
200
+ if k=="companyOfficers": return "management"
201
+ if any(x in lk for x in ["pe","pb","roe","roa","margin","debt","revenue","profit","eps","cap"]):
202
+ return "fundamental"
203
+ if isinstance(v,(int,float)): return "price_volume"
204
+ if isinstance(v,str) and len(v)>80: return "long_text"
205
+ return "profile"
206
+
207
+ def group_info(info):
208
+ g = {"price_volume":{}, "fundamental":{}, "profile":{}, "management":{}, "long_text":{}}
209
+ for k,v in info.items():
210
+ if k in NOISE_KEYS or v in [None,"",[],{}]: continue
211
+ g[classify(k,v)][k] = v
212
+ return g
213
+
214
+ def split_df(df):
215
+ n = len(df)
216
+ cols = 1 if n<=6 else 2 if n<=14 else 3
217
+ size = (n+cols-1)//cols
218
+ return [df.iloc[i:i+size] for i in range(0,n,size)]
219
+
220
+ # ==============================
221
+ # Derived Metrics
222
+ # ==============================
223
+ def build_price_volume_derived(info):
224
+ out={}
225
+ price=info.get("regularMarketPrice")
226
+ dma50=info.get("fiftyDayAverage")
227
+ dma200=info.get("twoHundredDayAverage")
228
+ low52=info.get("fiftyTwoWeekLow")
229
+ high52=info.get("fiftyTwoWeekHigh")
230
+ if price and dma50: out["vs 50DMA"]="Above ↑" if price>dma50 else "Below ↓"
231
+ if price and dma200: out["vs 200DMA"]="Above ↑" if price>dma200 else "Below ↓"
232
+ if price and low52 and high52 and high52!=low52: out["52W Pos"]=f"{(price-low52)/(high52-low52)*100:.1f}%"
233
+ return out
234
+
235
+ def build_smart_signals(info):
236
+ rows=[]
237
+ pe=info.get("trailingPE")
238
+ roe=info.get("returnOnEquity")
239
+ debt=info.get("debtToEquity")
240
+ price=info.get("regularMarketPrice")
241
+ dma50=info.get("fiftyDayAverage")
242
+ dma200=info.get("twoHundredDayAverage")
243
+ if pe: rows.append(("Valuation","Expensive" if pe>35 else "Cheap" if pe<15 else "Fair"))
244
+ if roe: rows.append(("Quality","High" if roe>0.18 else "Average"))
245
+ if debt: rows.append(("Balance Sheet","Weak" if debt>1 else "Healthy"))
246
+ if price and dma50 and dma200:
247
+ trend = "Bullish" if price>dma50>dma200 else "Bearish" if price<dma50<dma200 else "Neutral"
248
+ rows.append(("Momentum",trend))
249
+ return pd.DataFrame(rows,columns=["Field","Value"])
250
+
251
+ # ==============================
252
+ # Build Price/Volume Section
253
+ # ==============================
254
+ def build_price_volume_section(info,pv_data):
255
+ df=build_df_from_dict(pv_data)
256
+ derived=build_price_volume_derived(info)
257
+ if derived: df=pd.concat([df,pd.DataFrame(derived.items(),columns=["Field","Value"])],ignore_index=True)
258
+ cards=""
259
+ for title,fields in PRICE_VOLUME_GROUPS.items():
260
+ sub=df[df["Field"].isin(fields)]
261
+ if not sub.empty: cards+=html_card(title,make_table(sub),mini=True)
262
+ trend_df=df[df["Field"].isin(["vs 50DMA","vs 200DMA","52W Pos"])]
263
+ if not trend_df.empty: cards+=html_card("Trend & Momentum",make_table(trend_df),mini=True)
264
+ signals=build_smart_signals(info)
265
+ if not signals.empty: cards+=html_card("Smart Signals",make_table(signals),mini=True)
266
+ return column_layout(cards)
267
+
268
+ # ==============================
269
+ # Main Function
270
+ # ==============================
271
+ def fetch_info(symbol):
272
+ key=f"info_{symbol}"
273
+ if exists(key,"html"):
274
+ cached=load(key,"html")
275
+ if cached: return cached
276
+ try:
277
+ info=yfinfo(symbol)
278
+ if "__error__" in info: return "No data"
279
+ groups=group_info(info)
280
+ html=""
281
+ # Price / Volume
282
+ pv=resolve_duplicates(groups["price_volume"])
283
+ if pv: html+=html_card(f"{MAIN_ICONS['Price / Volume']} Price / Volume",build_price_volume_section(info,pv),shade=0)
284
+ # Fundamentals
285
+ if groups["fundamental"]:
286
+ df=build_df_from_dict(groups["fundamental"])
287
+ html+=html_card(f"{MAIN_ICONS['Fundamentals']} Fundamentals",
288
+ column_layout("".join(html_card("Fundamentals",make_table(c),mini=True) for c in split_df(df))),
289
+ shade=1)
290
+ # Company Profile
291
+ if groups["profile"]:
292
+ df=build_df_from_dict(groups["profile"])
293
+ html+=html_card(f"{MAIN_ICONS['Company Profile']} Company Profile",
294
+ column_layout("".join(html_card("Profile",make_table(c),mini=True) for c in split_df(df))),
295
+ shade=2)
296
+ # Management
297
+ if groups["management"].get("companyOfficers"):
298
+ cards=""
299
+ for o in groups["management"]["companyOfficers"]:
300
+ cards+=html_card(o.get("name",""),o.get("title",""),mini=True)
301
+ html+=html_card(f"{MAIN_ICONS['Management']} Management",column_layout(cards),shade=2)
302
+ # Long Text
303
+ for k,v in groups["long_text"].items():
304
+ html+=html_card(SHORT_NAMES.get(k,k[:16]),v)
305
+ save(key,html,"html")
306
+ return html
307
+ except Exception:
308
+ return f"<pre>{traceback.format_exc()}</pre>"