This commit is contained in:
Tvishi Agarwal
2026-04-09 20:15:17 +00:00
parent b83f55c069
commit 3e4a3ed03f
2 changed files with 105 additions and 36 deletions
+88 -22
View File
@@ -84,14 +84,14 @@ BOND_ETFS = {
# ─── COMMODITY TICKERS ───────────────────────────────────────────────────────
COMMODITIES = {
"GC=F": {"name": "Gold", "unit": "$/oz"},
"SI=F": {"name": "Silver", "unit": "$/oz"},
"CL=F": {"name": "WTI Crude", "unit": "$/bbl"},
"BZ=F": {"name": "Brent Crude", "unit": "$/bbl"},
"NG=F": {"name": "Natural Gas", "unit": "$/MMBtu"},
"HG=F": {"name": "Copper", "unit": "$/lb"},
"ZW=F": {"name": "Wheat", "unit": "¢/bu"},
"ZC=F": {"name": "Corn", "unit": "¢/bu"},
"GC=F": {"key": "gold", "name": "Gold", "unit": "$/oz"},
"SI=F": {"key": "silver", "name": "Silver", "unit": "$/oz"},
"CL=F": {"key": "wti", "name": "WTI Crude", "unit": "$/bbl"},
"BZ=F": {"key": "brent", "name": "Brent Crude", "unit": "$/bbl"},
"NG=F": {"key": "natgas", "name": "Natural Gas", "unit": "$/MMBtu"},
"HG=F": {"key": "copper", "name": "Copper", "unit": "$/lb"},
"ZW=F": {"key": "wheat", "name": "Wheat", "unit": "¢/bu"},
"ZC=F": {"key": "corn", "name": "Corn", "unit": "¢/bu"},
}
# ─── KEY ETFs ────────────────────────────────────────────────────────────────
@@ -278,12 +278,12 @@ def fetch_finnhub_calendar():
{
"date": e.get("time", "")[:10],
"time": e.get("time", "")[11:16],
"country": e.get("country", ""),
"event": e.get("event", ""),
"prev": e.get("prev"),
"estimate": e.get("estimate"),
"actual": e.get("actual"),
"impact": e.get("impact", ""),
"ctry": e.get("country", ""),
"ev": e.get("event", ""),
"prev": e.get("prev") if e.get("prev") is not None else "\u2014",
"fcast": e.get("estimate") if e.get("estimate") is not None else "\u2014",
"act": e.get("actual") if e.get("actual") is not None else "\u2014",
"imp": "red" if e.get("impact") == "high" else "orange",
}
for e in events[:40]
]
@@ -309,12 +309,16 @@ def fetch_rss_news():
title = item.findtext("title", "")
link = item.findtext("link", "")
pub = item.findtext("pubDate", "")
try:
pub_ts = int(datetime.strptime(pub, "%a, %d %b %Y %H:%M:%S %Z").timestamp())
except Exception:
pub_ts = 0
if title:
articles.append({
"headline": title,
"source": source,
"url": link,
"datetime": pub,
"datetime": pub_ts,
"category": "general",
})
except Exception as e:
@@ -322,6 +326,28 @@ def fetch_rss_news():
return articles
def fetch_single_ticker_history(ticker, name, period="3mo"):
"""Fallback: fetch a single ticker individually if batch download fails."""
try:
data = yf.download(ticker, period=period, progress=False, threads=False)
closes = data["Close"].dropna()
if len(closes) < 2:
return None
last = float(closes.iloc[-1])
d1 = float(closes.iloc[-2]) if len(closes) >= 2 else None
w1 = float(closes.iloc[-6]) if len(closes) >= 6 else None
m1 = float(closes.iloc[-22]) if len(closes) >= 22 else None
return {
"price": round(last, 4),
"d1_pct": round((last / d1 - 1) * 100, 2) if d1 else None,
"w1_pct": round((last / w1 - 1) * 100, 2) if w1 else None,
"m1_pct": round((last / m1 - 1) * 100, 2) if m1 else None,
}
except Exception as e:
print(f" Single ticker history error ({ticker}): {e}")
return None
def main():
print("=" * 60)
print(f"FX RADAR Data Generation — {datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')}")
@@ -348,7 +374,19 @@ def main():
us_yields[tenor] = val
print(f" US {tenor}: {val}%")
time.sleep(0.2) # Rate limit
output["yields"]["US"] = us_yields
output["yields"]["US"] = {
"y1m": us_yields.get("1M"),
"y3m": us_yields.get("3M"),
"y6m": us_yields.get("6M"),
"y1": us_yields.get("1Y"),
"y2": us_yields.get("2Y"),
"y3": us_yields.get("3Y"),
"y5": us_yields.get("5Y"),
"y7": us_yields.get("7Y"),
"y10": us_yields.get("10Y"),
"y20": us_yields.get("20Y"),
"y30": us_yields.get("30Y"),
}
# 2. Global yields from FRED (limited but free)
print("\n[2/8] Fetching global yields from FRED...")
@@ -361,7 +399,11 @@ def main():
print(f" {country} {tenor}: {val}%")
time.sleep(0.2)
if country_yields:
output["yields"][country] = country_yields
output["yields"][country] = {
"y10": country_yields.get("10Y"),
"y2": country_yields.get("2Y"),
"y5": country_yields.get("5Y"),
}
# 3. Yield proxies from Yahoo Finance (US treasuries)
print("\n[3/8] Fetching yield data from Yahoo Finance...")
@@ -370,15 +412,39 @@ def main():
})
for name, data in yf_yields.items():
print(f" {name}: {data['price']}")
# Fill missing US yields from Yahoo proxies if FRED missing
us_out = output["yields"].get("US", {})
if us_out.get("y3m") is None and yf_yields.get("US_3M"):
us_out["y3m"] = yf_yields["US_3M"]["price"]
if us_out.get("y5") is None and yf_yields.get("US_5Y"):
us_out["y5"] = yf_yields["US_5Y"]["price"]
if us_out.get("y10") is None and yf_yields.get("US_10Y"):
us_out["y10"] = yf_yields["US_10Y"]["price"]
if us_out.get("y30") is None and yf_yields.get("US_30Y"):
us_out["y30"] = yf_yields["US_30Y"]["price"]
output["yields"]["US"] = us_out
output["yields"]["US_yf"] = yf_yields
# 4. Commodities
print("\n[4/8] Fetching commodities...")
commod_data = fetch_yf_history(COMMODITIES)
for name, data in commod_data.items():
info = next((v for k, v in COMMODITIES.items() if v["name"] == name), {})
data["unit"] = info.get("unit", "")
print(f" {name}: ${data['price']} ({data.get('d1_pct', '?')}%)")
commod_data_raw = fetch_yf_history({k: v["name"] for k, v in COMMODITIES.items()})
commod_data = {}
print(" Commodity raw keys:", list(commod_data_raw.keys()))
for ticker, info in COMMODITIES.items():
name = info["name"]
key = info["key"]
row = commod_data_raw.get(name)
if row:
row["unit"] = info.get("unit", "")
commod_data[key] = row
print(f" {name}: ${row['price']} ({row.get('d1_pct', '?')}%)")
else:
# Fallback: try single ticker download
single = fetch_single_ticker_history(ticker, name)
if single:
single["unit"] = info.get("unit", "")
commod_data[key] = single
print(f" {name} (fallback): ${single['price']} ({single.get('d1_pct', '?')}%)")
output["commodities"] = commod_data
# 5. Key ETFs
+17 -14
View File
@@ -20,6 +20,7 @@ async function fFr(d,sym){try{return await jf("https://api.frankfurter.dev/v1/"+
async function fFD(){try{const r=await fetch("/.netlify/functions/fred-yields");if(!r.ok)throw new Error("fred-yields failed");return await r.json()}catch{return null}}
async function fECB(){try{const r=await fetch("/.netlify/functions/ecb-yields");if(!r.ok)throw new Error("ecb-yields failed");return await r.json()}catch(err){console.error("ECB yields fetch failed",err);return null}}
async function fFh(ep,p,k){if(!k)return null;try{const q=new URLSearchParams({...p,token:k}).toString();return await jf("https://finnhub.io/api/v1/"+ep+"?"+q)}catch{return null}}
async function fData(){const r=await fetch("/data.json?_="+Date.now());if(!r.ok)throw new Error("Failed to load data.json");return await r.json()}
const ago=n=>{const d=new Date();d.setDate(d.getDate()-n);return d.toISOString().slice(0,10)};const fwd=n=>{const d=new Date();d.setDate(d.getDate()+n);return d.toISOString().slice(0,10)};const jan2=()=>new Date().getFullYear()+"-01-02";
const G10=[{p:"EURUSD",q:"EUR",i:1},{p:"GBPUSD",q:"GBP",i:1},{p:"USDJPY",q:"JPY",i:0},{p:"USDCHF",q:"CHF",i:0},{p:"AUDUSD",q:"AUD",i:1},{p:"NZDUSD",q:"NZD",i:1},{p:"USDCAD",q:"CAD",i:0},{p:"USDSEK",q:"SEK",i:0},{p:"USDNOK",q:"NOK",i:0}];
@@ -344,21 +345,23 @@ function App(){
const svK=useCallback(nk=>{setKeys(nk);Object.entries(nk).forEach(([k,v])=>sk(k,v))},[]);
const load=useCallback(async()=>{
sLd(true);
try{const [now,d1,w1,m1,ytd]=await Promise.all([fFr("latest",SYM),fFr(ago(1),SYM),fFr(ago(7),SYM),fFr(ago(30),SYM),fFr(jan2(),SYM)]);if(now?.rates){const bld=defs=>defs.map(d=>{const s=now.rates[d.q];if(!s)return{pair:d.p,spot:null,d1:null,w1:null,m1:null,ytd:null};const sp=d.i?1/s:s;const ch=h=>{const v=h?.rates?.[d.q];if(!v)return null;return d.i?pc(1/s,1/v):pc(s,v)};return{pair:d.p,spot:sp,d1:ch(d1),w1:ch(w1),m1:ch(m1),ytd:ch(ytd)}});sG10(bld(G10));sEM(bld(EMP));sFxL(true);const eur=now.rates.EUR,eurY=d1?.rates?.EUR;if(eur)sRisk(p=>({...p,DXY:{level:+(1/eur*104.5).toFixed(2),chg:eurY?+pc(1/eur,1/eurY):null}}))}}catch{}
try{const usYields=await fFD();if(usYields){sYld(p=>({...p,US:usYields}));sYL(true);if(usYields.y10!=null)sRisk(p=>({...p,"US 10Y":{level:usYields.y10,chg:null}}))}}catch{}
try{const euYields=await fECB();if(euYields){sYld(p=>({...p,Eurozone:euYields}))}}catch(err){console.error("Eurozone yields load failed",err)}
if(keys.finnhub){
const [vR,gR,wR,bR]=await Promise.allSettled([fFh("quote",{symbol:"CBOE:VIX"},keys.finnhub),fFh("forex/rates",{base:"USD"},keys.finnhub),fFh("quote",{symbol:"NYMEX:CL1!"},keys.finnhub),fFh("quote",{symbol:"NYMEX:BZ1!"},keys.finnhub)]);
const v=vR.status==="fulfilled"?vR.value:null;if(v?.c)sRisk(p=>({...p,VIX:{level:v.c,chg:+(v.c-v.pc).toFixed(2)}}));
const g=gR.status==="fulfilled"?gR.value:null;if(g?.quote?.XAU){const gp=1/g.quote.XAU;sCmd(p=>({...p,gold:{level:gp,chg:null}}));sRisk(p=>({...p,Gold:{level:gp,chg:null}}));}
const w=wR.status==="fulfilled"?wR.value:null;if(w?.c){sCmd(p=>({...p,wti:{level:w.c,chg:w.pc?+pc(w.c,w.pc):null}}));sRisk(p=>({...p,WTI:{level:w.c,chg:w.pc?+pc(w.c,w.pc):null}}));}
const b=bR.status==="fulfilled"?bR.value:null;if(b?.c){sCmd(p=>({...p,brent:{level:b.c,chg:b.pc?+pc(b.c,b.pc):null}}));sRisk(p=>({...p,Brent:{level:b.c,chg:b.pc?+pc(b.c,b.pc):null}}));}
sRL(true)}
sLd(false);sLR(new Date());
},[keys]);
try{
const data=await fData();
const mapFx=defs=>defs.map(d=>{const x=data.fx?.[d.p];return{pair:d.p,spot:x?.price??null,d1:x?.d1_pct??null,w1:x?.w1_pct??null,m1:x?.m1_pct??null,ytd:null}});
sG10(mapFx(G10));sEM(mapFx(EMP));sFxL(true);
const mappedYields={};
if(data.yields){Object.entries(data.yields).forEach(([k,v])=>{if(k!=="US_yf")mappedYields[k]=v})}
sYld(mappedYields);sYL(true);
const dxy=data.fx?.DXY;const vix=data.volatility?.["VIX (S&P 500 Vol)"];const move=data.volatility?.["MOVE (Bond Vol)"];const usY=mappedYields.US||{};
sRisk({DXY:{level:dxy?.price??null,chg:dxy?.d1_pct??null},VIX:{level:vix?.price??null,chg:vix?.change_pct??null},Gold:{level:data.commodities?.gold?.price??null,chg:data.commodities?.gold?.d1_pct??null},WTI:{level:data.commodities?.wti?.price??null,chg:data.commodities?.wti?.d1_pct??null},Brent:{level:data.commodities?.brent?.price??null,chg:data.commodities?.brent?.d1_pct??null},"US 10Y":{level:usY.y10??null,chg:null},MOVE:{level:move?.price??null,chg:move?.change_pct??null}});
sRL(true);
sCmd({gold:{level:data.commodities?.gold?.price??null,chg:data.commodities?.gold?.d1_pct??null},wti:{level:data.commodities?.wti?.price??null,chg:data.commodities?.wti?.d1_pct??null},brent:{level:data.commodities?.brent?.price??null,chg:data.commodities?.brent?.d1_pct??null}});
const parsedNews=(data.news||[]).map(n=>{const dt=typeof n.datetime==="number"?n.datetime:Math.floor(Date.parse(n.datetime)/1000)||0;return{...n,datetime:dt}});sNews(parsedNews);sNL(true);
const parsedCal=(data.calendar||[]).map(e=>({date:e.date||"",time:e.time||"",ctry:e.ctry||"",ev:e.ev||"",prev:e.prev??"—",fcast:e.fcast??"—",act:e.act??"—",imp:e.imp||"orange"}));sCal(parsedCal);sCL(true);
sLR(new Date());
}catch(err){console.error("Failed to load dashboard data",err)}finally{sLd(false)}
},[]);
useEffect(()=>{load()},[load]);
useEffect(()=>{if((sec==="deep"||sec==="brief")&&keys.finnhub&&!nL){(async()=>{try{const nd=await fFh("news",{category:"forex"},keys.finnhub);if(Array.isArray(nd)){sNews(nd.slice(0,30));sNL(true)}}catch{}})()}},[sec,keys.finnhub,nL]);
useEffect(()=>{if(sec==="cal"&&keys.finnhub&&!cL){(async()=>{try{const cd=await fFh("calendar/economic",{from:ago(0),to:fwd(7)},keys.finnhub);if(cd?.economicCalendar?.length){const fl={"US":"🇺🇸","EU":"🇪🇺","GB":"🇬🇧","JP":"🇯🇵","CH":"🇨🇭","AU":"🇦🇺","NZ":"🇳🇿","CA":"🇨🇦","SE":"🇸🇪","NO":"🇳🇴","BR":"🇧🇷","MX":"🇲🇽","ZA":"🇿🇦","IN":"🇮🇳","KR":"🇰🇷","TR":"🇹🇷","PL":"🇵🇱","CN":"🇨🇳","DE":"🇩🇪"};sCal(cd.economicCalendar.slice(0,30).map(e=>({date:e.time?.slice(0,10)||"",time:e.time?.slice(11,16)||"",ctry:fl[e.country]||e.country||"",ev:e.event||"",prev:e.prev!=null?String(e.prev):"—",fcast:e.estimate!=null?String(e.estimate):"—",act:e.actual!=null?String(e.actual):"—",imp:e.impact==="high"?"red":"orange"})));sCL(true)}}catch{}})()}},[sec,keys.finnhub,cL]);
return <div style={{minHeight:"100vh",background:C.bg}}>
<RiskBar d={risk} live={rL||fxL}/>
<div style={{position:"sticky",top:0,zIndex:100,background:C.bgC,borderBottom:"1px solid "+C.bd,padding:"0 16px",display:"flex",alignItems:"center",justifyContent:"space-between",flexWrap:"wrap"}}>