mirror of
https://github.com/tvishia29-alt/FXandRatesDashboard.git
synced 2026-07-27 16:07:44 +00:00
pleaseworknow
This commit is contained in:
Binary file not shown.
+23
@@ -719,6 +719,29 @@ def main():
|
||||
print(f" {info['label']}: {v}%{chg_str}")
|
||||
time.sleep(0.3) # Rate limit
|
||||
print(f" Fetched {len(output['gov_bonds'])} bonds")
|
||||
|
||||
# Backfill yields from EODHD bonds for the curve/spreads views
|
||||
bond_to_yield = {
|
||||
"us2y": ("US", "y2"), "us10y": ("US", "y10"), "us30y": ("US", "y30"),
|
||||
"uk2y": ("UK", "y2"), "uk10y": ("UK", "y10"), "uk30y": ("UK", "y30"),
|
||||
"de2y": ("Eurozone", "y2"), "de10y": ("Eurozone", "y10"), "de30y": ("Eurozone", "y30"),
|
||||
"jp2y": ("Japan", "y2"), "jp10y": ("Japan", "y10"), "jp30y": ("Japan", "y30"),
|
||||
"it2y": ("Italy", "y2"), "it10y": ("Italy", "y10"),
|
||||
"fr10y": ("France", "y10"), "es10y": ("Spain", "y10"),
|
||||
"ca2y": ("Canada", "y2"), "ca10y": ("Canada", "y10"),
|
||||
"au2y": ("Australia", "y2"), "au10y": ("Australia", "y10"),
|
||||
"nz2y": ("New Zealand", "y2"), "nz10y": ("New Zealand", "y10"),
|
||||
"ch10y": ("Switzerland", "y10"), "se10y": ("Sweden", "y10"), "no10y": ("Norway", "y10"),
|
||||
}
|
||||
for bond_key, (country, tenor) in bond_to_yield.items():
|
||||
bond = output["gov_bonds"].get(bond_key)
|
||||
if bond and bond.get("value") is not None:
|
||||
if country not in output["yields"]:
|
||||
output["yields"][country] = {}
|
||||
# Only fill if not already set from FRED/yfinance
|
||||
if output["yields"][country].get(tenor) is None:
|
||||
output["yields"][country][tenor] = bond["value"]
|
||||
print(" Backfilled yields from EODHD bonds")
|
||||
else:
|
||||
print(" EODHD API key not set — skipping government bonds")
|
||||
|
||||
|
||||
+25
-5
@@ -44,11 +44,11 @@ const Dot=({on})=><span style={{display:"inline-block",width:6,height:6,borderRa
|
||||
function Tabs({tabs,act,set}){return <div style={{display:"flex",gap:2,borderBottom:"1px solid "+C.bd,marginBottom:12,overflowX:"auto"}}>{tabs.map(t=><button key={t} onClick={()=>set(t)} style={{padding:"8px 16px",fontSize:12,fontWeight:600,background:act===t?C.ac+"20":"transparent",color:act===t?C.ac:C.txD,border:"none",borderBottom:act===t?"2px solid "+C.ac:"2px solid transparent",whiteSpace:"nowrap"}}>{t}</button>)}</div>}
|
||||
function Tbl({cols,rows,sm}){return <div style={{overflowX:"auto"}}><table style={{width:"100%",borderCollapse:"collapse",fontSize:sm?11:12}}><thead><tr>{cols.map(c=><th key={c.k} style={{padding:"6px 10px",textAlign:c.a||"right",color:C.txM,fontWeight:600,fontSize:10,letterSpacing:1,textTransform:"uppercase",borderBottom:"1px solid "+C.bd,whiteSpace:"nowrap",background:C.bgC}}>{c.l}</th>)}</tr></thead><tbody>{rows.map((r,i)=><tr key={i} style={{background:i%2?C.bg+"60":"transparent"}}>{cols.map(c=>{const v=c.rn?c.rn(r):r[c.k];return <td key={c.k} style={{padding:"5px 10px",textAlign:c.a||"right",fontFamily:c.nm?"inherit":Mn,fontSize:sm?11:12,color:c.cl?c.cl(r):C.tx,whiteSpace:"nowrap",borderBottom:"1px solid "+C.bd+"20"}}>{v!=null?v:"—"}</td>})}</tr>)}</tbody></table></div>}
|
||||
function TV({sym,h=280}){const [shouldLoad,setShouldLoad]=React.useState(false);React.useEffect(()=>{setShouldLoad(false)},[sym]);if(!sym)return <div style={{height:h,borderRadius:6,border:"1px solid "+C.bd,display:"flex",alignItems:"center",justifyContent:"center",color:C.txM,fontSize:11}}>No chart</div>;if(!shouldLoad)return <div style={{height:h,borderRadius:6,border:"1px solid "+C.bd,background:C.bg,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",gap:10,color:C.txM,fontSize:11}}><div style={{fontSize:12,color:C.txD}}>Chart ready to load</div><div style={{fontFamily:Mn,color:C.tx}}>{sym}</div><button onClick={()=>setShouldLoad(true)} style={{padding:"8px 14px",borderRadius:6,border:"1px solid "+C.ac+"50",background:C.ac+"18",color:C.ac,fontSize:12,fontWeight:700}}>Load Chart</button></div>;return <iframe loading="lazy" title={sym} src={"https://s.tradingview.com/widgetembed/?symbol="+encodeURIComponent(sym)+"&interval=D&range=3M&theme=dark&style=1&locale=en&hide_legend=1&hide_volume=1&hide_side_toolbar=1&allow_symbol_change=0&withdateranges=1&backgroundColor=%23111827&gridColor=%231e293b"} style={{width:"100%",height:h,borderRadius:6,border:"1px solid "+C.bd,background:C.bg}} frameBorder="0"/>}
|
||||
function RiskBar({d,live}){const vd=(d.VIX?.chg||0)<0,dd=(d.DXY?.chg||0)<0,on=vd&&dd,off=!vd&&!dd,bc=on?C.g:off?C.r:C.am;return <div style={{background:"linear-gradient(90deg,"+bc+"18 0%,"+C.bg+" 50%,"+bc+"18 100%)",borderBottom:"1px solid "+bc+"40",padding:"5px 16px",display:"flex",alignItems:"center",gap:16,overflowX:"auto",whiteSpace:"nowrap",fontSize:12}}><span style={{color:bc,fontWeight:700,fontSize:10,letterSpacing:2,padding:"2px 8px",border:"1px solid "+bc+"60",borderRadius:3}}>{on?"RISK ON":off?"RISK OFF":"MIXED"}</span><Dot on={live}/>{Object.entries(d).map(([k,v])=><span key={k} style={{display:"inline-flex",gap:4,alignItems:"center"}}><span style={{color:C.txM,fontWeight:600,fontSize:11}}>{k}</span><span style={{color:C.tx,fontFamily:Mn,fontWeight:600,fontSize:12}}>{v.level!=null?Number(v.level).toLocaleString(undefined,{maximumFractionDigits:2}):"—"}</span>{v.chg!=null&&<span style={{color:cc(v.chg),fontFamily:Mn,fontSize:10}}>{k.includes("10Y")?fb(v.chg):fc(v.chg,k==="VIX"?"":"%")}</span>}</span>)}</div>}
|
||||
function RiskBar({d,live}){const vd=(d.VIX?.chg||0)<0,dd=(d.DXY?.chg||0)<0,on=vd&&dd,off=!vd&&!dd,bc=on?C.g:off?C.r:C.am;const fmtLvl=(k,v)=>{if(v==null)return"—";if(k==="2s10s")return v.toFixed(1)+"bp";if(k.includes("Y"))return v.toFixed(2)+"%";return Number(v).toLocaleString(undefined,{maximumFractionDigits:2})};return <div style={{background:"linear-gradient(90deg,"+bc+"18 0%,"+C.bg+" 50%,"+bc+"18 100%)",borderBottom:"1px solid "+bc+"40",padding:"5px 16px",display:"flex",alignItems:"center",gap:16,overflowX:"auto",whiteSpace:"nowrap",fontSize:12}}><span style={{color:bc,fontWeight:700,fontSize:10,letterSpacing:2,padding:"2px 8px",border:"1px solid "+bc+"60",borderRadius:3}}>{on?"RISK ON":off?"RISK OFF":"MIXED"}</span><Dot on={live}/>{Object.entries(d).map(([k,v])=><span key={k} style={{display:"inline-flex",gap:4,alignItems:"center"}}><span style={{color:C.txM,fontWeight:600,fontSize:11}}>{k}</span><span style={{color:C.tx,fontFamily:Mn,fontWeight:600,fontSize:12}}>{fmtLvl(k,v.level)}</span>{v.chg!=null&&<span style={{color:cc(v.chg),fontFamily:Mn,fontSize:10}}>{k.includes("10Y")?fb(v.chg):fc(v.chg,k==="VIX"?"":"%")}</span>}</span>)}</div>}
|
||||
|
||||
/* ═══ PAGES ═══════════════════════════════════════════════════════ */
|
||||
function FxTbl({data,live}){return <div><div style={{display:"flex",alignItems:"center",gap:8,marginBottom:8}}><Dot on={live}/><span style={{color:C.txM,fontSize:10}}>{live?"Live via Frankfurter":"Loading..."}</span></div><Tbl cols={[{k:"pair",l:"Pair",a:"left",nm:1,cl:()=>C.b},{k:"spot",l:"Spot",rn:r=>r.spot!=null?Number(r.spot).toFixed(r.spot>100?2:4):"—"},{k:"d1",l:"1D %",rn:r=>fc(r.d1),cl:r=>cc(r.d1)},{k:"w1",l:"1W %",rn:r=>fc(r.w1),cl:r=>cc(r.w1)},{k:"m1",l:"1M %",rn:r=>fc(r.m1),cl:r=>cc(r.m1)},{k:"ytd",l:"YTD %",rn:r=>fc(r.ytd),cl:r=>cc(r.ytd)}]} rows={data}/></div>}
|
||||
function YldTbl({yld,live,policyRates}){return <div><div style={{display:"flex",alignItems:"center",gap:8,marginBottom:8}}><Dot on={live}/><span style={{color:C.txM,fontSize:10}}>{live?"US via FRED · EU via ECB":"No yield data"}</span></div><Tbl cols={[{k:"c",l:"Country",a:"left",nm:1,rn:r=>r.f+" "+r.c},{k:"cb",l:"CB Rate",rn:r=>r.cb!=null?r.cb.toFixed(2)+"%":"—",cl:r=>r.cb!=null?C.tx:C.txM},{k:"y2",l:"2Y",rn:r=>r.y2!=null?r.y2.toFixed(2):"—",cl:r=>r.y2?C.tx:C.txM},{k:"y10",l:"10Y",rn:r=>r.y10!=null?r.y10.toFixed(2):"—",cl:r=>r.y10?C.tx:C.txM},{k:"s",l:"2s10s",rn:r=>r.y2&&r.y10?fb((r.y10-r.y2)*100):"—",cl:r=>r.y2&&r.y10?cc(r.y10-r.y2):C.txM}]} rows={RT.map(r=>({...r,cb:policyRates?.[r.c]??null,...(yld[r.c]||{})}))} sm/></div>}
|
||||
function YldTbl({yld,live,policyRates}){return <div><div style={{display:"flex",alignItems:"center",gap:8,marginBottom:8}}><Dot on={live}/><span style={{color:C.txM,fontSize:10}}>{live?"Live rates loaded":"No rate data"}</span></div><Tbl cols={[{k:"c",l:"Country",a:"left",nm:1,rn:r=>r.f+" "+r.c},{k:"cb",l:"Policy",rn:r=>r.cb!=null?r.cb.toFixed(2)+"%":"\u2014",cl:r=>r.cb!=null?C.tx:C.txM},{k:"y2",l:"2Y",rn:r=>r.y2!=null?r.y2.toFixed(2)+"%":"\u2014"},{k:"y5",l:"5Y",rn:r=>r.y5!=null?r.y5.toFixed(2)+"%":"\u2014"},{k:"y10",l:"10Y",rn:r=>r.y10!=null?r.y10.toFixed(2)+"%":"\u2014"},{k:"y30",l:"30Y",rn:r=>r.y30!=null?r.y30.toFixed(2)+"%":"\u2014"},{k:"s2s10",l:"2s10s",rn:r=>(r.y2!=null&&r.y10!=null)?fb((r.y10-r.y2)*100):"\u2014",cl:r=>(r.y2!=null&&r.y10!=null)?cc(r.y10-r.y2):C.txM},{k:"s10s30",l:"10s30s",rn:r=>(r.y10!=null&&r.y30!=null)?fb((r.y30-r.y10)*100):"\u2014",cl:r=>(r.y10!=null&&r.y30!=null)?cc(r.y30-r.y10):C.txM},{k:"term",l:"10Y-Policy",rn:r=>(r.cb!=null&&r.y10!=null)?fb((r.y10-r.cb)*100):"\u2014",cl:r=>(r.cb!=null&&r.y10!=null)?cc(r.y10-r.cb):C.txM}]} rows={RT.map(r=>({...r,cb:policyRates?.[r.c]??null,...(yld[r.c]||{})}))} sm/></div>}
|
||||
function Cmdt({d}){const items=[{k:"gold",l:"Gold XAU",s:"TVC:GOLD"},{k:"wti",l:"WTI Crude",s:"TVC:USOIL"},{k:"brent",l:"Brent",s:"TVC:UKOIL"}];return <div><div className="g3" style={{display:"grid",gridTemplateColumns:"repeat(3,1fr)",gap:12,marginBottom:16}}>{items.map(it=>{const v=d[it.k];return <div key={it.k} style={{background:C.bg,border:"1px solid "+C.bd,borderRadius:6,padding:14}}><div style={{color:C.txM,fontSize:10,fontWeight:600,textTransform:"uppercase",letterSpacing:1,marginBottom:4}}>{it.l}</div><div style={{color:C.tx,fontSize:24,fontWeight:700,fontFamily:Mn}}>{v?.level!=null?"$"+Number(v.level).toLocaleString(undefined,{maximumFractionDigits:2}):"—"}</div>{v?.chg!=null&&<div style={{color:cc(v.chg),fontSize:12,fontFamily:Mn,marginTop:2}}>{fc(v.chg)}</div>}</div>})}</div><div className="g3" style={{display:"grid",gridTemplateColumns:"repeat(3,1fr)",gap:12}}>{items.map(it=><div key={it.k}><TV sym={it.s} h={220}/></div>)}</div></div>}
|
||||
function XCry({yld}){const g=RT.filter(r=>["US","Eurozone","UK","Japan","Switzerland","Australia","New Zealand","Canada","Sweden","Norway"].includes(r.c));const gY=c=>yld[c]?.y2??null;const pairs=[];for(let i=0;i<g.length;i++)for(let j=i+1;j<g.length;j++){const a=g[i],b=g[j],ya=gY(a.c),yb=gY(b.c);if(ya==null||yb==null)continue;const d=Math.abs(ya-yb),lc=ya>yb?a:b,sc=ya>yb?b:a;pairs.push({lc:lc.ccy,sc:sc.ccy,lf:lc.f,sf:sc.f,ly:Math.max(ya,yb),sy:Math.min(ya,yb),carry:d})}if(!pairs.length)return <div style={{padding:20,textAlign:"center",color:C.txM,fontSize:12}}>Carry data requires live yield feeds (FRED / ECB)</div>;return <Tbl cols={[{k:"rk",l:"#",a:"center",cl:r=>r.rk<=5?C.g:C.txD},{k:"t",l:"Trade",a:"left",nm:1,rn:r=>r.lf+" Long "+r.lc+" / "+r.sf+" Short "+r.sc},{k:"ly",l:"Long Yld",rn:r=>r.ly.toFixed(2)+"%"},{k:"sy",l:"Short Yld",rn:r=>r.sy.toFixed(2)+"%"},{k:"carry",l:"Carry",rn:r=>"+"+r.carry.toFixed(2)+"%",cl:r=>r.carry>3?C.g:r.carry>1?C.tl:C.txD},{k:"bp",l:"bps",rn:r=>"+"+(r.carry*100).toFixed(0),cl:r=>r.carry>3?C.g:r.carry>1?C.tl:C.txD}]} rows={pairs.sort((a,b)=>b.carry-a.carry).map((r,i)=>({...r,rk:i+1}))} sm/>}
|
||||
function UCry({yld}){const uY=yld.US?.y2;if(uY==null)return <div style={{padding:20,textAlign:"center",color:C.txM,fontSize:12}}>Carry data requires live US yield feed (FRED)</div>;const rows=RT.filter(r=>r.c!=="US").map(r=>{const ry=yld[r.c]?.y2;return {...r,carry:ry!=null?+((ry-uY).toFixed(2)):null}}).filter(r=>r.carry!=null).sort((a,b)=>b.carry-a.carry).map((r,i)=>({...r,rk:i+1}));if(!rows.length)return <div style={{padding:20,textAlign:"center",color:C.txM,fontSize:12}}>No foreign yield data available yet</div>;return <Tbl cols={[{k:"rk",l:"#",a:"center",cl:r=>r.rk<=3?C.g:r.rk>=13?C.r:C.txD},{k:"c",l:"Currency",a:"left",nm:1,rn:r=>r.f+" "+r.ccy},{k:"y",l:"2Y Yield",rn:r=>(yld[r.c]?.y2!=null?yld[r.c].y2.toFixed(2)+"%":"—")},{k:"carry",l:"vs USD",rn:r=>fc(r.carry),cl:r=>cc(r.carry)},{k:"dir",l:"Direction",a:"left",nm:1,rn:r=>r.carry>0.3?"Long "+r.ccy:"Short "+r.ccy,cl:r=>r.carry>0.3?C.g:C.r}]} rows={rows} sm/>}
|
||||
@@ -221,12 +221,23 @@ function Scenarios({risk,yld,g10,em,cmd}){
|
||||
|
||||
function CorrMx(){return <div><div style={{marginBottom:12}}><span style={{color:C.ac,fontSize:13,fontWeight:700}}>30-Day Correlation Matrix</span></div><div style={{padding:40,textAlign:"center",color:C.txM,fontSize:12,border:"1px dashed "+C.bd,borderRadius:6}}>—<br/><span style={{fontSize:11,marginTop:8,display:"inline-block"}}>Correlation data not available — requires a live feed</span></div></div>}
|
||||
|
||||
/* ═══ CURVES ══════════════════════════════════════════════════════ */
|
||||
function Curves({yld,policyRates}){const majors=["US","UK","Eurozone","Japan","Canada","Australia","New Zealand","Switzerland","Sweden","Norway"];const rows=majors.map(c=>{const y=yld[c]||{};const cb=policyRates?.[c]??null;return{c,f:RT.find(r=>r.c===c)?.f||"",cb,y2:y.y2??null,y5:y.y5??null,y10:y.y10??null,y30:y.y30??null,s2s10:(y.y2!=null&&y.y10!=null)?y.y10-y.y2:null,s10s30:(y.y10!=null&&y.y30!=null)?y.y30-y.y10:null,term:(cb!=null&&y.y10!=null)?y.y10-cb:null}});return <div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:16}}><div style={{color:C.tx,fontSize:15,fontWeight:700,marginBottom:12}}>Curve Monitor</div><Tbl cols={[{k:"c",l:"Country",a:"left",nm:1,rn:r=>r.f+" "+r.c},{k:"cb",l:"Policy",rn:r=>r.cb!=null?r.cb.toFixed(2)+"%":"\u2014"},{k:"y2",l:"2Y",rn:r=>r.y2!=null?r.y2.toFixed(2)+"%":"\u2014"},{k:"y5",l:"5Y",rn:r=>r.y5!=null?r.y5.toFixed(2)+"%":"\u2014"},{k:"y10",l:"10Y",rn:r=>r.y10!=null?r.y10.toFixed(2)+"%":"\u2014"},{k:"y30",l:"30Y",rn:r=>r.y30!=null?r.y30.toFixed(2)+"%":"\u2014"},{k:"s2s10",l:"2s10s",rn:r=>r.s2s10!=null?fb(r.s2s10*100):"\u2014",cl:r=>r.s2s10!=null?cc(r.s2s10):C.txM},{k:"s10s30",l:"10s30s",rn:r=>r.s10s30!=null?fb(r.s10s30*100):"\u2014",cl:r=>r.s10s30!=null?cc(r.s10s30):C.txM},{k:"term",l:"10Y-Policy",rn:r=>r.term!=null?fb(r.term*100):"\u2014",cl:r=>r.term!=null?cc(r.term):C.txM}]} rows={rows} sm/></div>}
|
||||
|
||||
/* ═══ SPREADS ═════════════════════════════════════════════════════ */
|
||||
function Spreads({yld}){const us10=yld.US?.y10??null;const de10=yld.Eurozone?.y10??yld.Germany?.y10??null;const jp10=yld.Japan?.y10??null;const uk10=yld.UK?.y10??null;const it10=yld.Italy?.y10??null;const fr10=yld.France?.y10??null;const es10=yld.Spain?.y10??null;const ca10=yld.Canada?.y10??null;const au10=yld.Australia?.y10??null;const rows=[{name:"UST 10Y \u2013 Bund 10Y",spread:(us10!=null&&de10!=null)?us10-de10:null},{name:"UST 10Y \u2013 JGB 10Y",spread:(us10!=null&&jp10!=null)?us10-jp10:null},{name:"Gilt 10Y \u2013 Bund 10Y",spread:(uk10!=null&&de10!=null)?uk10-de10:null},{name:"BTP 10Y \u2013 Bund 10Y",spread:(it10!=null&&de10!=null)?it10-de10:null},{name:"OAT 10Y \u2013 Bund 10Y",spread:(fr10!=null&&de10!=null)?fr10-de10:null},{name:"Spain 10Y \u2013 Bund 10Y",spread:(es10!=null&&de10!=null)?es10-de10:null},{name:"Canada 10Y \u2013 UST 10Y",spread:(ca10!=null&&us10!=null)?ca10-us10:null},{name:"Australia 10Y \u2013 UST 10Y",spread:(au10!=null&&us10!=null)?au10-us10:null}];return <div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:16}}><div style={{color:C.tx,fontSize:15,fontWeight:700,marginBottom:12}}>Cross-Market Spreads</div><Tbl cols={[{k:"name",l:"Spread",a:"left",nm:1},{k:"spread",l:"Value",rn:r=>r.spread!=null?fb(r.spread*100):"\u2014",cl:r=>r.spread!=null?cc(r.spread):C.txM}]} rows={rows} sm/></div>}
|
||||
|
||||
/* ═══ DEEP DIVE ═══════════════════════════════════════════════════ */
|
||||
function Deep({ctry,g10,em,yld,news,nL,risk,macro,policyRates,govBonds}){
|
||||
const ccy=cOf[ctry],fp=[...g10,...em].find(x=>x.pair===pOf[ctry]),y=yld[ctry]||{},tpl=RT.find(x=>x.c===ctry),m=macro?.[ctry]||{},cbRate=policyRates?.[ctry]??null;
|
||||
const pfxList=bondPfx[ctry]||[];const cBonds=Object.entries(govBonds||{}).filter(([k])=>pfxList.some(p=>k.startsWith(p))).map(([k,v])=>({key:k,...v}));
|
||||
const kws=nKw[ctry]||[ccy.toLowerCase()];const cN=news.filter(n=>kws.some(kw=>(n.headline||"").toLowerCase().includes(kw)));const dN=cN.length>=2?cN:news;
|
||||
const ce=Object.entries(y).filter(([k,v])=>v!=null&&k.startsWith("y")).map(([k,v])=>[k.replace("y","").toUpperCase(),v]);
|
||||
const curve2s10s=(y.y2!=null&&y.y10!=null)?y.y10-y.y2:null;
|
||||
const curve10s30s=(y.y10!=null&&y.y30!=null)?y.y30-y.y10:null;
|
||||
const policyGap=(cbRate!=null&&y.y10!=null)?y.y10-cbRate:null;
|
||||
const us10=yld.US?.y10??null;
|
||||
const localSpreadVsUS=(us10!=null&&y.y10!=null&&ctry!=="US")?y.y10-us10:null;
|
||||
const [ai,setAi]=useState(null);
|
||||
const [aiLd,setAiLd]=useState(false);
|
||||
const [aiErr,setAiErr]=useState("");
|
||||
@@ -429,6 +440,13 @@ function Deep({ctry,g10,em,yld,news,nL,risk,macro,policyRates,govBonds}){
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{marginTop:12,paddingTop:10,borderTop:"1px solid "+C.bd}}>
|
||||
<div style={{display:"flex",justifyContent:"space-between",padding:"4px 0"}}><span style={{color:C.txD,fontSize:11}}>2s10s</span><span style={{color:curve2s10s!=null?cc(curve2s10s):C.txM,fontFamily:Mn,fontSize:12}}>{curve2s10s!=null?fb(curve2s10s*100):"\u2014"}</span></div>
|
||||
<div style={{display:"flex",justifyContent:"space-between",padding:"4px 0"}}><span style={{color:C.txD,fontSize:11}}>10s30s</span><span style={{color:curve10s30s!=null?cc(curve10s30s):C.txM,fontFamily:Mn,fontSize:12}}>{curve10s30s!=null?fb(curve10s30s*100):"\u2014"}</span></div>
|
||||
<div style={{display:"flex",justifyContent:"space-between",padding:"4px 0"}}><span style={{color:C.txD,fontSize:11}}>10Y - Policy</span><span style={{color:policyGap!=null?cc(policyGap):C.txM,fontFamily:Mn,fontSize:12}}>{policyGap!=null?fb(policyGap*100):"\u2014"}</span></div>
|
||||
{ctry!=="US"&&<div style={{display:"flex",justifyContent:"space-between",padding:"4px 0"}}><span style={{color:C.txD,fontSize:11}}>10Y vs US</span><span style={{color:localSpreadVsUS!=null?cc(localSpreadVsUS):C.txM,fontFamily:Mn,fontSize:12}}>{localSpreadVsUS!=null?fb(localSpreadVsUS*100):"\u2014"}</span></div>}
|
||||
</div>
|
||||
|
||||
{tvY[ctry]
|
||||
? <TV sym={tvY[ctry]} h={170}/>
|
||||
: <div style={{height:170,borderRadius:6,border:"1px solid "+C.bd,display:"flex",alignItems:"center",justifyContent:"center",color:C.txM,fontSize:11}}>
|
||||
@@ -496,7 +514,7 @@ function Deep({ctry,g10,em,yld,news,nL,risk,macro,policyRates,govBonds}){
|
||||
}
|
||||
|
||||
/* ═══ BRIEF ═══════════════════════════════════════════════════════ */
|
||||
function genBr(g10,em,risk,yld,cal,cmd){const L=[];const vix=risk.VIX,dxy=risk.DXY;const vD=(vix?.chg||0)<0,dD=(dxy?.chg||0)<0;if(vD&&dD)L.push("Risk on — VIX down "+Math.abs(vix?.chg||0).toFixed(1)+" at "+(vix?.level?.toFixed(1)||"—")+", USD softer at "+(dxy?.level?.toFixed(1)||"—")+" ("+fc(dxy?.chg)+"). Supportive for carry.");else if(!vD&&!dD)L.push("Defensive tone — VIX up at "+(vix?.level?.toFixed(1)||"—")+", USD bid at "+(dxy?.level?.toFixed(1)||"—")+" ("+fc(dxy?.chg)+"). JPY/CHF finding support.");else L.push("Mixed — VIX "+(vix?.level?.toFixed(1)||"—")+" ("+fc(vix?.chg,"")+"), DXY "+(dxy?.level?.toFixed(1)||"—")+" ("+fc(dxy?.chg)+"). Positioning ahead of events.");const mv=[...g10,...em].filter(x=>x.d1!=null).sort((a,b)=>Math.abs(b.d1)-Math.abs(a.d1)).slice(0,4);if(mv.length)L.push("Movers: "+mv.map(x=>x.pair+" "+(x.d1>0?"▲":"▼")+Math.abs(x.d1).toFixed(2)+"%").join(", ")+".");if(cmd.gold?.level)L.push("Gold $"+cmd.gold.level.toFixed(0)+(cmd.gold.chg!=null?" ("+fc(cmd.gold.chg)+")":"")+"."+(cmd.wti?.level?" WTI $"+cmd.wti.level.toFixed(2)+(cmd.wti.chg!=null?" ("+fc(cmd.wti.chg)+")":"")+".":" "));const u2=yld.US?.y2;if(u2!=null){const tc=RT.filter(r=>r.c!=="US"&&yld[r.c]?.y2!=null).map(r=>({ccy:r.ccy,c:+((yld[r.c].y2)-u2).toFixed(2)})).filter(x=>x.c>1).sort((a,b)=>b.c-a.c).slice(0,3);L.push("US 2Y "+u2.toFixed(2)+"%."+(tc.length?" Top carry: "+tc.map(x=>x.ccy+" +"+x.c+"%").join(", ")+".":" "))}const hi=cal.filter(e=>e.imp==="red").slice(0,4);if(hi.length)L.push("Watch: "+hi.map(e=>e.ctry+" "+e.ev+" ("+e.date+")").join(", ")+".");return L.join(" ")}
|
||||
function genBr(g10,em,risk,yld,cal,cmd){const L=[];const vix=risk.VIX,dxy=risk.DXY;const vD=(vix?.chg||0)<0,dD=(dxy?.chg||0)<0;if(vD&&dD)L.push("Risk on — VIX down "+Math.abs(vix?.chg||0).toFixed(1)+" at "+(vix?.level?.toFixed(1)||"—")+", USD softer at "+(dxy?.level?.toFixed(1)||"—")+" ("+fc(dxy?.chg)+"). Supportive for carry.");else if(!vD&&!dD)L.push("Defensive tone — VIX up at "+(vix?.level?.toFixed(1)||"—")+", USD bid at "+(dxy?.level?.toFixed(1)||"—")+" ("+fc(dxy?.chg)+"). JPY/CHF finding support.");else L.push("Mixed — VIX "+(vix?.level?.toFixed(1)||"—")+" ("+fc(vix?.chg,"")+"), DXY "+(dxy?.level?.toFixed(1)||"—")+" ("+fc(dxy?.chg)+"). Positioning ahead of events.");const mv=[...g10,...em].filter(x=>x.d1!=null).sort((a,b)=>Math.abs(b.d1)-Math.abs(a.d1)).slice(0,4);if(mv.length)L.push("Movers: "+mv.map(x=>x.pair+" "+(x.d1>0?"▲":"▼")+Math.abs(x.d1).toFixed(2)+"%").join(", ")+".");if(cmd.gold?.level)L.push("Gold $"+cmd.gold.level.toFixed(0)+(cmd.gold.chg!=null?" ("+fc(cmd.gold.chg)+")":"")+"."+(cmd.wti?.level?" WTI $"+cmd.wti.level.toFixed(2)+(cmd.wti.chg!=null?" ("+fc(cmd.wti.chg)+")":"")+".":" "));const u2=yld.US?.y2;if(u2!=null){const tc=RT.filter(r=>r.c!=="US"&&yld[r.c]?.y2!=null).map(r=>({ccy:r.ccy,c:+((yld[r.c].y2)-u2).toFixed(2)})).filter(x=>x.c>1).sort((a,b)=>b.c-a.c).slice(0,3);L.push("US 2Y "+u2.toFixed(2)+"%."+(tc.length?" Top carry: "+tc.map(x=>x.ccy+" +"+x.c+"%").join(", ")+".":" "))}const us10=yld.US?.y10;if(u2!=null&&us10!=null){const curve=us10-u2;L.push("US curve at "+((curve*100)>0?"+":"")+(curve*100).toFixed(1)+"bp (2s10s).")}const de10=yld.Eurozone?.y10;if(us10!=null&&de10!=null){L.push("UST-Bund spread "+(((us10-de10)*100).toFixed(0))+"bp.")}const hi=cal.filter(e=>e.imp==="red").slice(0,4);if(hi.length)L.push("Watch: "+hi.map(e=>e.ctry+" "+e.ev+" ("+e.date+")").join(", ")+".");return L.join(" ")}
|
||||
function Brief({g10,em,yld,risk,cal,cmd}){const [br,sBr]=useState(null);const [ts,sTs]=useState(null);const [ld,sLd]=useState(false);const gen=()=>{sLd(true);setTimeout(()=>{sBr(genBr(g10,em,risk,yld,cal,cmd));sTs(new Date().toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit"}));sLd(false)},600)};return <div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:20}}><div style={{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:16,flexWrap:"wrap",gap:8}}><div style={{display:"flex",alignItems:"center",gap:10}}><span style={{fontSize:20}}>🧠</span><div><div style={{color:C.tx,fontSize:16,fontWeight:700}}>AI Morning Brief</div><div style={{color:C.txM,fontSize:11}}>Live FX, yields, carry, commodities & calendar</div></div></div><div style={{display:"flex",alignItems:"center",gap:8}}>{ts&&<span style={{color:C.txM,fontSize:10}}>Generated {ts}</span>}<button onClick={gen} disabled={ld} style={{padding:"10px 24px",background:ld?"transparent":"linear-gradient(135deg,"+C.ac+","+C.acD+")",color:ld?C.ac:"#fff",border:ld?"1px solid "+C.ac+"40":"none",borderRadius:6,fontSize:12,fontWeight:700}}>{ld?<><span className="sp" style={{marginRight:4}}/>Analysing</>:br?"↻ Regen":"⚡ Generate Brief"}</button></div></div>{br?<div style={{padding:16,background:C.bg,borderRadius:8,border:"1px solid "+C.ac+"25"}}><div style={{color:C.tx,fontSize:13,fontStyle:"italic",lineHeight:1.7}}>{br}</div></div>:<div style={{padding:24,background:C.bg,borderRadius:8,border:"1px dashed "+C.bd,textAlign:"center",color:C.txM,fontSize:12}}>Click "Generate Brief"</div>}</div>}
|
||||
function Cal({evts,live}){const [fl,sF]=useState("All");const gF=["🇺🇸","🇪🇺","🇬🇧","🇯🇵","🇨🇭","🇦🇺","🇳🇿","🇨🇦","🇸🇪","🇳🇴"];return <div><div style={{display:"flex",gap:6,marginBottom:10,alignItems:"center"}}><Dot on={live}/>{["All","G10","EM","CB"].map(f=><button key={f} onClick={()=>sF(f)} style={{padding:"5px 14px",fontSize:11,fontWeight:600,borderRadius:4,background:fl===f?C.ac+"25":C.bg,color:fl===f?C.ac:C.txD,border:"1px solid "+(fl===f?C.ac+"50":C.bd)}}>{f}</button>)}</div><Tbl cols={[{k:"date",l:"Date",a:"left"},{k:"time",l:"Time",a:"left"},{k:"ctry",l:"",a:"center",nm:1},{k:"ev",l:"Event",a:"left",nm:1,cl:r=>r.imp==="red"?C.r:C.am},{k:"prev",l:"Prev"},{k:"fcast",l:"Fcst"},{k:"act",l:"Act",cl:r=>r.beat==="beat"?C.g:r.beat==="miss"?C.r:r.beat==="inline"?C.am:(r.act&&r.act!=="—")?C.g:C.txM},{k:"beat",l:"B/M",cl:r=>r.beat==="beat"?C.g:r.beat==="miss"?C.r:C.am,rn:r=>r.beat==="beat"?"▲ Beat":r.beat==="miss"?"▼ Miss":r.beat==="inline"?"● Inline":"—"}]} rows={evts.filter(e=>{if(fl==="G10")return gF.includes(e.ctry);if(fl==="EM")return!gF.includes(e.ctry);if(fl==="CB")return(e.ev||"").toLowerCase().match(/rate|fomc|boj|ecb|rba|rbnz|boc/);return true})}/></div>}
|
||||
function Sett({open,close,keys,save}){const [lk,sL]=useState(keys);useEffect(()=>sL(keys),[keys,open]);if(!open)return null;return <div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.7)",zIndex:1000,display:"flex",alignItems:"center",justifyContent:"center"}} onClick={close}><div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:10,padding:24,width:520,maxWidth:"92vw"}} onClick={e=>e.stopPropagation()}><div style={{display:"flex",justifyContent:"space-between",marginBottom:16}}><span style={{color:C.tx,fontSize:16,fontWeight:700}}>⚙ API Keys</span><button onClick={close} style={{background:"none",border:"none",color:C.txM,fontSize:20}}>✕</button></div>{[{k:"finnhub",l:"Finnhub — VIX, commodities, news, calendar",u:"https://finnhub.io/register"}].map(a=><div key={a.k} style={{marginBottom:14,padding:12,background:C.bg,borderRadius:6,border:"1px solid "+C.bd}}><div style={{display:"flex",justifyContent:"space-between",marginBottom:4}}><span style={{color:C.tx,fontSize:12,fontWeight:600}}>{a.l}</span><a href={a.u} target="_blank" rel="noopener" style={{color:C.ac,fontSize:10,textDecoration:"none"}}>Get key →</a></div><input type="password" value={lk[a.k]||""} onChange={e=>sL({...lk,[a.k]:e.target.value})} style={{width:"100%",padding:"8px 12px",background:C.bgC,border:"1px solid "+C.bd,borderRadius:4,color:C.tx,fontSize:12,fontFamily:Mn,boxSizing:"border-box"}}/></div>)}<button onClick={()=>{save(lk);close()}} style={{width:"100%",padding:"12px",background:"linear-gradient(135deg,"+C.ac+","+C.acD+")",color:"#fff",border:"none",borderRadius:6,fontSize:13,fontWeight:700}}>Save</button></div></div>}
|
||||
@@ -527,7 +545,7 @@ function App(){
|
||||
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}});
|
||||
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 2Y":{level:usY.y2??null,chg:null},"US 10Y":{level:usY.y10??null,chg:null},"DE 10Y":{level:mappedYields.Eurozone?.y10??null,chg:null},"JP 10Y":{level:mappedYields.Japan?.y10??null,chg:null},"2s10s":{level:(usY.y2!=null&&usY.y10!=null)?+((usY.y10-usY.y2)*100).toFixed(1):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);
|
||||
@@ -539,13 +557,15 @@ function App(){
|
||||
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"}}>
|
||||
<div style={{display:"flex",alignItems:"center",gap:6,overflowX:"auto"}}><span style={{fontWeight:800,fontSize:14,color:C.ac,padding:"12px 0"}}>FX</span><span style={{fontWeight:800,fontSize:14,color:C.tx}}>RADAR</span><div style={{width:1,height:20,background:C.bd,margin:"0 8px"}}/>{[{id:"brief",l:"🧠 Brief"},{id:"sentiment",l:"🧭 Sentiment"},{id:"markets",l:"📊 Markets"},{id:"commod",l:"🛢 Commodities"},{id:"scenarios",l:"🎯 Scenarios"},{id:"corr",l:"🔗 Correlations"},{id:"deep",l:"🌍 Deep Dive"},{id:"cal",l:"📅 Calendar"}].map(s=><button key={s.id} onClick={()=>setSec(s.id)} style={{padding:"12px 10px",fontSize:12,fontWeight:600,background:"transparent",border:"none",color:sec===s.id?C.tx:C.txM,borderBottom:sec===s.id?"2px solid "+C.ac:"2px solid transparent",whiteSpace:"nowrap"}}>{s.l}</button>)}</div>
|
||||
<div style={{display:"flex",alignItems:"center",gap:6,overflowX:"auto"}}><span style={{fontWeight:800,fontSize:14,color:C.ac,padding:"12px 0"}}>FX</span><span style={{fontWeight:800,fontSize:14,color:C.tx}}>RADAR</span><div style={{width:1,height:20,background:C.bd,margin:"0 8px"}}/>{[{id:"brief",l:"🧠 Brief"},{id:"sentiment",l:"🧭 Sentiment"},{id:"markets",l:"📊 Markets"},{id:"curves",l:"📈 Curves"},{id:"spreads",l:"↔ Spreads"},{id:"commod",l:"🛢 Commodities"},{id:"scenarios",l:"🎯 Scenarios"},{id:"corr",l:"🔗 Correlations"},{id:"deep",l:"🌍 Deep Dive"},{id:"cal",l:"📅 Calendar"}].map(s=><button key={s.id} onClick={()=>setSec(s.id)} style={{padding:"12px 10px",fontSize:12,fontWeight:600,background:"transparent",border:"none",color:sec===s.id?C.tx:C.txM,borderBottom:sec===s.id?"2px solid "+C.ac:"2px solid transparent",whiteSpace:"nowrap"}}>{s.l}</button>)}</div>
|
||||
<div style={{display:"flex",alignItems:"center",gap:8,padding:"8px 0"}}><button onClick={load} disabled={ld} style={{background:C.tl+"18",border:"1px solid "+C.tl+"40",borderRadius:4,color:C.tl,padding:"4px 12px",fontSize:11,fontWeight:600}}>{ld?"...":"↻ Refresh"}</button>{lr&&<span style={{color:C.txM,fontSize:9}}>⏱{lr.toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit",second:"2-digit"})}</span>}<Clock/><button onClick={()=>setSO(true)} style={{background:C.g+"18",border:"1px solid "+C.g+"50",borderRadius:4,color:C.g,padding:"4px 8px",fontSize:11,fontWeight:600}}>✓</button></div>
|
||||
</div>
|
||||
<div style={{padding:"16px 20px",maxWidth:1600,margin:"0 auto"}}>
|
||||
{sec==="brief"&&<Brief g10={g10D} em={emD} yld={yld} risk={risk} cal={cal} cmd={cmd}/>}
|
||||
{sec==="sentiment"&&<Sentiment risk={risk} g10={g10D} em={emD} cmd={cmd}/>}
|
||||
{sec==="markets"&&<div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:16}}><Markets g10={g10D} em={emD} fxL={fxL} yld={yld} yL={yL} etfs={etfs} vol={vol} bondEtfs={bondEtfs} policyRates={policyRates}/></div>}
|
||||
{sec==="curves"&&<Curves yld={yld} policyRates={policyRates}/>}
|
||||
{sec==="spreads"&&<Spreads yld={yld}/>}
|
||||
{sec==="commod"&&<div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:16}}><div style={{color:C.tx,fontSize:15,fontWeight:700,marginBottom:12}}>Commodities</div><Cmdt d={cmd}/></div>}
|
||||
{sec==="scenarios"&&<Scenarios risk={risk} yld={yld} g10={g10D} em={emD} cmd={cmd}/>}
|
||||
{sec==="corr"&&<div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:20}}><CorrMx/></div>}
|
||||
|
||||
Reference in New Issue
Block a user