mirror of
https://github.com/tvishia29-alt/FXandRatesDashboard.git
synced 2026-07-27 16:07:44 +00:00
556 lines
58 KiB
HTML
556 lines
58 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||
<title>FX RADAR TEST v4</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet"/>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.23.9/babel.min.js"></script>
|
||
<style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body,#root{height:100%}body{background:#0a0e17;font-family:'IBM Plex Sans',system-ui,sans-serif;color:#e2e8f0;font-size:13px}::-webkit-scrollbar{width:5px;height:5px}::-webkit-scrollbar-track{background:#0a0e17}::-webkit-scrollbar-thumb{background:#334155;border-radius:3px}button{font-family:inherit;cursor:pointer}table{border-spacing:0}@keyframes spin{to{transform:rotate(360deg)}}.sp{width:12px;height:12px;border:2px solid #334155;border-top-color:#6366f1;border-radius:50%;animation:spin .8s linear infinite;display:inline-block;vertical-align:middle}@media(max-width:900px){.g2{grid-template-columns:1fr!important}.g3{grid-template-columns:1fr!important}.hmob{display:none!important}}</style>
|
||
</head><body><div id="root"></div>
|
||
<script type="text/babel">
|
||
const {useState,useEffect,useRef,useCallback}=React;
|
||
const C={bg:"#0a0e17",bgC:"#111827",bd:"#1e293b",tx:"#e2e8f0",txD:"#94a3b8",txM:"#64748b",g:"#22c55e",r:"#ef4444",b:"#3b82f6",am:"#f59e0b",ac:"#6366f1",acD:"#4338ca",tl:"#14b8a6"};
|
||
const cc=v=>v>0?C.g:v<0?C.r:C.txM;const fc=(v,s="%")=>v!=null?(v>0?"+":"")+Number(v).toFixed(2)+s:"—";const fb=v=>v!=null?(v>0?"+":"")+Number(v).toFixed(1)+"bp":"—";const pc=(a,b)=>b?+((a/b-1)*100).toFixed(2):null;const Mn="'JetBrains Mono',monospace";
|
||
const _d=s=>{try{return atob(s)}catch{return""}};const _K={finnhub:_d("ZDc5ZGg1cHIwMXFxcG1oZ2QwYjBkNzlkaDVwcjAxcXFwbWhnZDBiZw==")};
|
||
const gk=n=>{try{return localStorage.getItem("fxr_"+n)||""}catch{return""}};const sk=(n,v)=>{try{localStorage.setItem("fxr_"+n,v)}catch{}};
|
||
const jf=async u=>{const r=await fetch(u);if(!r.ok)throw new Error(r.status);return r.json()};
|
||
async function fFr(d,sym){try{return await jf("https://api.frankfurter.dev/v1/"+d+"?base=USD&symbols="+sym)}catch{return null}}
|
||
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}];
|
||
const EMP=[{p:"USDCNY",q:"CNY"},{p:"USDMXN",q:"MXN"},{p:"USDBRL",q:"BRL"},{p:"USDZAR",q:"ZAR"},{p:"USDINR",q:"INR"},{p:"USDKRW",q:"KRW"},{p:"USDTRY",q:"TRY"},{p:"USDPLN",q:"PLN"},{p:"USDHUF",q:"HUF"},{p:"USDCZK",q:"CZK"},{p:"USDSGD",q:"SGD"},{p:"USDIDR",q:"IDR"},{p:"USDTHB",q:"THB"}].map(x=>({...x,i:0}));
|
||
const SYM=[...new Set([...G10,...EMP].map(x=>x.q))].join(",");
|
||
|
||
const RT=[{c:"US",f:"🇺🇸",cb:null,ccy:"USD"},{c:"Eurozone",f:"🇪🇺",cb:null,ccy:"EUR"},{c:"UK",f:"🇬🇧",cb:null,ccy:"GBP"},{c:"Japan",f:"🇯🇵",cb:null,ccy:"JPY"},{c:"Switzerland",f:"🇨🇭",cb:null,ccy:"CHF"},{c:"Australia",f:"🇦🇺",cb:null,ccy:"AUD"},{c:"New Zealand",f:"🇳🇿",cb:null,ccy:"NZD"},{c:"Canada",f:"🇨🇦",cb:null,ccy:"CAD"},{c:"Sweden",f:"🇸🇪",cb:null,ccy:"SEK"},{c:"Norway",f:"🇳🇴",cb:null,ccy:"NOK"},{c:"Brazil",f:"🇧🇷",cb:null,ccy:"BRL"},{c:"Mexico",f:"🇲🇽",cb:null,ccy:"MXN"},{c:"South Africa",f:"🇿🇦",cb:null,ccy:"ZAR"},{c:"India",f:"🇮🇳",cb:null,ccy:"INR"},{c:"South Korea",f:"🇰🇷",cb:null,ccy:"KRW"},{c:"Turkey",f:"🇹🇷",cb:null,ccy:"TRY"},{c:"Poland",f:"🇵🇱",cb:null,ccy:"PLN"}];
|
||
const cOf={};RT.forEach(r=>cOf[r.c]=r.ccy);
|
||
const tvF={US:"TVC:DXY",Eurozone:"FX:EURUSD",UK:"FX:GBPUSD",Japan:"FX:USDJPY",Switzerland:"FX:USDCHF",Australia:"FX:AUDUSD","New Zealand":"FX:NZDUSD",Canada:"FX:USDCAD",Sweden:"FX:USDSEK",Norway:"FX:USDNOK",Brazil:"FX:USDBRL",Mexico:"FX:USDMXN","South Africa":"FX:USDZAR",India:"FX:USDINR","South Korea":"FX:USDKRW",Turkey:"FX:USDTRY",Poland:"FX:USDPLN"};
|
||
const tvY={US:"TVC:US10Y",Eurozone:"TVC:DE10Y",UK:"TVC:GB10Y",Japan:"TVC:JP10Y",Canada:"TVC:CA10Y",Australia:"TVC:AU10Y"};
|
||
const pOf={US:"DXY",Eurozone:"EURUSD",UK:"GBPUSD",Japan:"USDJPY",Switzerland:"USDCHF",Australia:"AUDUSD","New Zealand":"NZDUSD",Canada:"USDCAD",Sweden:"USDSEK",Norway:"USDNOK",Brazil:"USDBRL",Mexico:"USDMXN","South Africa":"USDZAR",India:"USDINR","South Korea":"USDKRW",Turkey:"USDTRY",Poland:"USDPLN"};
|
||
const nKw={US:["fed","fomc","treasury","dollar","powell","nonfarm"],Eurozone:["ecb","lagarde","eurozone","bund"],UK:["boe","bailey","gilt","sterling","uk gdp","uk cpi"],Japan:["boj","ueda","yen","jgb","japan"],Switzerland:["snb","swiss","franc"],Australia:["rba","aussie","australia"],Canada:["boc","canada","loonie"],"New Zealand":["rbnz","kiwi","new zealand"],Sweden:["riksbank","sweden"],Norway:["norges","norway"],Brazil:["bcb","brazil","real"],Mexico:["banxico","mexico","peso"],"South Africa":["sarb","south africa","rand"],India:["rbi","india","rupee"],"South Korea":["bok","korea","won"],Turkey:["cbrt","turkey","lira"],Poland:["nbp","poland","zloty"]};
|
||
|
||
const MD={};
|
||
|
||
|
||
|
||
/* ═══ UI ATOMS ════════════════════════════════════════════════════ */
|
||
const Dot=({on})=><span style={{display:"inline-block",width:6,height:6,borderRadius:"50%",background:on?C.g:C.am,marginRight:5}}/>;
|
||
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>}
|
||
|
||
/* ═══ 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}){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,...(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/>}
|
||
function CarryP({yld}){const [t,sT]=useState("Cross G10");return <div><Tabs tabs={["Cross G10","vs USD"]} act={t} set={sT}/>{t==="Cross G10"?<XCry yld={yld}/>:<UCry yld={yld}/>}</div>}
|
||
function EtfTbl({etfs}){const rows=Object.entries(etfs||{}).map(([name,v])=>({name,price:v?.price??null,d1:v?.d1_pct??null,w1:v?.w1_pct??null,m1:v?.m1_pct??null}));if(!rows.length)return <div style={{padding:20,textAlign:"center",color:C.txM,fontSize:12}}>No ETF data available</div>;return <Tbl cols={[{k:"name",l:"ETF",a:"left",nm:1,cl:()=>C.b},{k:"price",l:"Price",rn:r=>r.price!=null?Number(r.price).toFixed(2):"—"},{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)}]} rows={rows}/>}
|
||
function VolTbl({vol}){const rows=Object.entries(vol||{}).map(([name,v])=>({name,price:v?.price??null,prev:v?.prev_close??null,chg:v?.change_pct??null}));if(!rows.length)return <div style={{padding:20,textAlign:"center",color:C.txM,fontSize:12}}>No volatility data available</div>;return <Tbl cols={[{k:"name",l:"Instrument",a:"left",nm:1,cl:()=>C.b},{k:"price",l:"Level",rn:r=>r.price!=null?Number(r.price).toFixed(2):"—"},{k:"prev",l:"Prev",rn:r=>r.prev!=null?Number(r.prev).toFixed(2):"—"},{k:"chg",l:"Change",rn:r=>r.chg!=null?fc(r.chg,""):"—",cl:r=>cc(r.chg)}]} rows={rows}/>}
|
||
function BondEtfTbl({bondEtfs}){const rows=Object.entries(bondEtfs||{}).map(([name,v])=>({name,country:v?.country??"",duration:v?.duration??"",price:v?.price??null,d1:v?.d1_pct??null,w1:v?.w1_pct??null,m1:v?.m1_pct??null}));if(!rows.length)return <div style={{padding:20,textAlign:"center",color:C.txM,fontSize:12}}>No bond ETF data available</div>;return <Tbl cols={[{k:"name",l:"Bond ETF",a:"left",nm:1,cl:()=>C.b},{k:"country",l:"Country",a:"left"},{k:"duration",l:"Duration",a:"left"},{k:"price",l:"Price",rn:r=>r.price!=null?Number(r.price).toFixed(2):"—"},{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)}]} rows={rows} sm/>}
|
||
function Markets({g10,em,fxL,yld,yL,etfs,vol,bondEtfs}){const [t,sT]=useState("G10 FX");return <div><Tabs tabs={["G10 FX","EM FX","Rates","Carry","ETFs","Volatility","Bond ETFs"]} act={t} set={sT}/>{t==="G10 FX"&&<FxTbl data={g10} live={fxL}/>}{t==="EM FX"&&<FxTbl data={em} live={fxL}/>}{t==="Rates"&&<YldTbl yld={yld} live={yL}/>}{t==="Carry"&&<CarryP yld={yld}/>}{t==="ETFs"&&<EtfTbl etfs={etfs}/>}{t==="Volatility"&&<VolTbl vol={vol}/>}{t==="Bond ETFs"&&<BondEtfTbl bondEtfs={bondEtfs}/>}</div>}
|
||
|
||
/* ═══ SENTIMENT GAUGE ═════════════════════════════════════════════ */
|
||
function calcSentiment(risk,g10,em,cmd){
|
||
// Sub-score: Volatility (VIX level + VIX change + MOVE)
|
||
let vol=50;
|
||
const vix=risk.VIX?.level,vChg=risk.VIX?.chg||0;
|
||
if(vix!=null){if(vix<15)vol+=20;else if(vix<20)vol+=10;else if(vix<25)vol-=5;else if(vix<30)vol-=15;else vol-=25}
|
||
if(vChg<-3)vol+=10;else if(vChg<-1)vol+=5;else if(vChg>3)vol-=10;else if(vChg>1)vol-=5;
|
||
const mv=risk.MOVE;if(mv?.level!=null){if(mv.level<90)vol+=8;else if(mv.level>130)vol-=10;else if(mv.level>115)vol-=5}
|
||
vol=Math.max(0,Math.min(100,Math.round(vol)));
|
||
// Sub-score: Currency Flow (DXY direction + G10 breadth)
|
||
let fx=50;const dChg=risk.DXY?.chg||0;
|
||
if(dChg<-0.5)fx+=15;else if(dChg<-0.2)fx+=8;else if(dChg>0.5)fx-=15;else if(dChg>0.2)fx-=8;
|
||
const g10M=[...g10].filter(x=>x.d1!=null&&x.pair!=="DXY").map(x=>x.d1);
|
||
if(g10M.length){const pos=g10M.filter(x=>x>0).length;fx+=Math.round((pos/g10M.length-0.5)*30)}
|
||
fx=Math.max(0,Math.min(100,Math.round(fx)));
|
||
// Sub-score: Safe Haven (Gold + JPY + CHF)
|
||
let hv=50;const gChg=cmd.gold?.chg||0;
|
||
if(gChg>2)hv-=18;else if(gChg>0.8)hv-=10;else if(gChg<-1)hv+=12;else if(gChg<-0.3)hv+=5;
|
||
const jpy=g10.find(x=>x.pair==="USDJPY");if(jpy?.d1!=null){if(jpy.d1<-0.5)hv-=10;else if(jpy.d1>0.5)hv+=10}
|
||
const chf=g10.find(x=>x.pair==="USDCHF");if(chf?.d1!=null){if(chf.d1<-0.3)hv-=6;else if(chf.d1>0.3)hv+=6}
|
||
hv=Math.max(0,Math.min(100,Math.round(hv)));
|
||
// Sub-score: EM Risk
|
||
let emR=50;const emM=[...em].filter(x=>x.d1!=null).map(x=>x.d1);
|
||
if(emM.length){const avg=emM.reduce((a,b)=>a+b,0)/emM.length;
|
||
if(avg<-0.8)emR+=18;else if(avg<-0.3)emR+=10;else if(avg>0.8)emR-=18;else if(avg>0.3)emR-=10;
|
||
const stressed=emM.filter(x=>x>1).length;if(stressed>=3)emR-=10}
|
||
emR=Math.max(0,Math.min(100,Math.round(emR)));
|
||
// Weighted composite
|
||
const score=Math.max(0,Math.min(100,Math.round(vol*0.30+fx*0.25+hv*0.20+emR*0.25)));
|
||
return {score,subs:{vol,fx,haven:hv,em:emR}};
|
||
}
|
||
function sentLabel(s){if(s>=75)return{t:"Extreme Greed",c:C.g};if(s>=60)return{t:"Greed",c:"#4ade80"};if(s>=45)return{t:"Neutral",c:C.am};if(s>=30)return{t:"Fear",c:"#f97316"};return{t:"Extreme Fear",c:C.r}}
|
||
function sentNarrative(score,subs,risk,cmd,g10,em){
|
||
const p=[];const{vol,fx,haven,em:emS}=subs;const vix=risk.VIX?.level;
|
||
if(vol>=60)p.push("Vol regime is calm"+(vix?" with VIX at "+vix.toFixed(1):"")+" — supportive for carry and risk.");
|
||
else if(vol<=35)p.push("Elevated vol"+(vix?" (VIX "+vix.toFixed(1)+")":"")+" signals caution — favour defensives and short gamma.");
|
||
else p.push("Vol in neutral range"+(vix?" at VIX "+vix.toFixed(1):"")+".");
|
||
if(fx>=60)p.push("USD weakening is a tailwind for risk FX.");
|
||
else if(fx<=35)p.push("Strong USD is pressuring risk FX crosses.");
|
||
if(haven<=35){const jpyD=g10.find(x=>x.pair==="USDJPY");p.push("Safe haven bid in gold"+(jpyD?.d1<-0.3?" and JPY":"")+" confirms defensive positioning.")}
|
||
else if(haven>=60)p.push("Safe havens seeing outflows — risk appetite intact.");
|
||
if(emS<=35){const worst=[...em].filter(x=>x.d1!=null).sort((a,b)=>b.d1-a.d1).slice(0,2);
|
||
p.push("EM stress"+(worst.length?" led by "+worst.map(x=>x.pair.replace("USD","")+" "+fc(x.d1)).join(", "):"")+". Watch for contagion.")}
|
||
else if(emS>=60)p.push("EM FX resilient — constructive for EM carry.");
|
||
return p.join(" ");
|
||
}
|
||
function sentDrivers(risk,cmd,g10,em){
|
||
const d=[];const vix=risk.VIX,dxy=risk.DXY;
|
||
if(vix?.level!=null)d.push({label:"VIX",val:vix.level.toFixed(1),chg:vix.chg,weight:vix.level<20?"Bullish":vix.level<25?"Neutral":"Bearish",cl:vix.level<20?C.g:vix.level<25?C.am:C.r});
|
||
if(dxy?.chg!=null)d.push({label:"DXY Direction",val:fc(dxy.chg),chg:dxy.chg,weight:dxy.chg<0?"USD Weak (Risk On)":"USD Bid (Defensive)",cl:dxy.chg<0?C.g:C.r});
|
||
const gChg=cmd.gold?.chg;if(gChg!=null)d.push({label:"Gold",val:fc(gChg),chg:gChg,weight:gChg>1?"Safe Haven Bid":gChg<-1?"Risk Appetite":"Flat",cl:gChg>1?C.r:gChg<-1?C.g:C.am});
|
||
const jpyP=g10.find(x=>x.pair==="USDJPY");if(jpyP?.d1!=null)d.push({label:"USDJPY",val:fc(jpyP.d1),chg:jpyP.d1,weight:jpyP.d1<-0.3?"JPY Bid (Risk Off)":jpyP.d1>0.3?"JPY Weak (Risk On)":"Sideways",cl:jpyP.d1<-0.3?C.r:jpyP.d1>0.3?C.g:C.am});
|
||
const emMv=[...em].filter(x=>x.d1!=null).map(x=>x.d1);if(emMv.length){const a=+(emMv.reduce((s,v)=>s+v,0)/emMv.length).toFixed(2);d.push({label:"EM FX Avg",val:fc(a),chg:a,weight:a<-0.3?"EM under pressure":a>0.3?"EM strength":"Mixed",cl:a<-0.3?C.r:a>0.3?C.g:C.am})}
|
||
const mv=risk.MOVE;if(mv?.level!=null)d.push({label:"MOVE (Bond Vol)",val:mv.level.toFixed(1),chg:mv.chg,weight:mv.level<100?"Calm":mv.level<120?"Elevated":"Stressed",cl:mv.level<100?C.g:mv.level<120?C.am:C.r});
|
||
const wChg=cmd.wti?.chg;if(wChg!=null)d.push({label:"WTI Crude",val:fc(wChg),chg:wChg,weight:wChg>2?"Supply Concern":wChg<-2?"Demand Worry":"Stable",cl:wChg>2?C.am:wChg<-2?C.r:C.g});
|
||
return d;
|
||
}
|
||
function SubBar({label,icon,value}){
|
||
const{t:lb,c:lc}=sentLabel(value);
|
||
return <div style={{display:"flex",alignItems:"center",gap:10,padding:"6px 0"}}>
|
||
<span style={{fontSize:14,width:22,textAlign:"center"}}>{icon}</span>
|
||
<span style={{color:C.txD,fontSize:11,fontWeight:600,width:100}}>{label}</span>
|
||
<div style={{flex:1,height:8,background:C.bd,borderRadius:4,position:"relative",overflow:"hidden"}}>
|
||
<div style={{position:"absolute",left:0,top:0,bottom:0,width:value+"%",background:"linear-gradient(90deg,"+C.r+","+C.am+","+C.g+")",borderRadius:4,transition:"width 0.5s ease"}}/>
|
||
</div>
|
||
<span style={{fontFamily:Mn,fontSize:12,fontWeight:700,color:lc,width:28,textAlign:"right"}}>{value}</span>
|
||
<span style={{fontSize:10,color:lc,width:90}}>{lb}</span>
|
||
</div>;
|
||
}
|
||
function Sentiment({risk,g10,em,cmd}){
|
||
const{score,subs}=calcSentiment(risk,g10,em,cmd);const{t:label,c:lc}=sentLabel(score);
|
||
const drivers=sentDrivers(risk,cmd,g10,em);
|
||
const narrative=sentNarrative(score,subs,risk,cmd,g10,em);
|
||
const ang=Math.PI-(score/100)*Math.PI;
|
||
return <div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:20}}>
|
||
<div style={{display:"flex",alignItems:"center",gap:10,marginBottom:16}}><span style={{fontSize:20}}>🧭</span><div><div style={{color:C.tx,fontSize:16,fontWeight:700}}>FX Sentiment Gauge</div><div style={{color:C.txM,fontSize:11}}>Composite score from volatility, currency flows, safe havens & EM risk</div></div></div>
|
||
<div style={{display:"flex",alignItems:"center",justifyContent:"center",gap:40,marginBottom:20,flexWrap:"wrap"}}>
|
||
<div style={{position:"relative",width:240,height:140}}>
|
||
<svg viewBox="0 0 240 140" style={{width:240,height:140}}>
|
||
<defs><linearGradient id="sg" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stopColor={C.r}/><stop offset="30%" stopColor="#f97316"/><stop offset="50%" stopColor={C.am}/><stop offset="70%" stopColor="#4ade80"/><stop offset="100%" stopColor={C.g}/></linearGradient></defs>
|
||
<path d="M20 120 A100 100 0 0 1 220 120" fill="none" stroke={C.bd} strokeWidth="18" strokeLinecap="round"/>
|
||
<path d="M20 120 A100 100 0 0 1 220 120" fill="none" stroke="url(#sg)" strokeWidth="18" strokeLinecap="round" strokeDasharray="314" strokeDashoffset={314-(score/100)*314}/>
|
||
<line x1="120" y1="120" x2={120+78*Math.cos(ang)} y2={120-78*Math.sin(ang)} stroke={lc} strokeWidth="3" strokeLinecap="round"/>
|
||
<circle cx="120" cy="120" r="6" fill={lc}/>
|
||
<text x="22" y="138" fill={C.txM} fontSize="9" fontFamily="JetBrains Mono,monospace">0</text>
|
||
<text x="120" y="14" fill={C.txM} fontSize="9" fontFamily="JetBrains Mono,monospace" textAnchor="middle">50</text>
|
||
<text x="218" y="138" fill={C.txM} fontSize="9" fontFamily="JetBrains Mono,monospace" textAnchor="end">100</text>
|
||
</svg>
|
||
<div style={{position:"absolute",bottom:0,left:0,right:0,textAlign:"center"}}>
|
||
<div style={{color:lc,fontSize:32,fontWeight:800,fontFamily:Mn}}>{score}</div>
|
||
</div>
|
||
</div>
|
||
<div style={{textAlign:"center"}}>
|
||
<div style={{color:lc,fontSize:24,fontWeight:800,letterSpacing:1}}>{label}</div>
|
||
<div style={{color:C.txM,fontSize:11,marginTop:4}}>0 = Extreme Fear · 100 = Extreme Greed</div>
|
||
</div>
|
||
</div>
|
||
{narrative&&<div style={{padding:14,background:C.bg,borderRadius:8,border:"1px solid "+lc+"25",marginBottom:20}}>
|
||
<div style={{color:C.tx,fontSize:12,lineHeight:1.7,fontStyle:"italic"}}>{narrative}</div>
|
||
</div>}
|
||
<div style={{marginBottom:20}}>
|
||
<div style={{color:C.txM,fontSize:10,fontWeight:700,textTransform:"uppercase",letterSpacing:1,marginBottom:8}}>Composition Breakdown</div>
|
||
<div style={{background:C.bg,borderRadius:8,border:"1px solid "+C.bd,padding:"8px 14px"}}>
|
||
<SubBar label="Volatility" icon="📉" value={subs.vol}/>
|
||
<SubBar label="Currency Flow" icon="💱" value={subs.fx}/>
|
||
<SubBar label="Safe Haven" icon="🛡" value={subs.haven}/>
|
||
<SubBar label="EM Risk" icon="🌍" value={subs.em}/>
|
||
</div>
|
||
</div>
|
||
<div style={{color:C.txM,fontSize:10,fontWeight:700,textTransform:"uppercase",letterSpacing:1,marginBottom:8}}>Signal Drivers</div>
|
||
<div style={{display:"grid",gap:6}}>{drivers.map((d,i)=><div key={i} style={{display:"flex",justifyContent:"space-between",alignItems:"center",padding:"8px 12px",background:C.bg,borderRadius:6,border:"1px solid "+C.bd}}>
|
||
<span style={{color:C.txD,fontSize:12,fontWeight:600}}>{d.label}</span>
|
||
<div style={{display:"flex",alignItems:"center",gap:12}}>
|
||
<span style={{color:C.tx,fontFamily:Mn,fontSize:12}}>{d.val}</span>
|
||
<span style={{color:d.cl,fontSize:11,fontWeight:600,minWidth:140,textAlign:"right"}}>{d.weight}</span>
|
||
</div>
|
||
</div>)}</div>
|
||
</div>;
|
||
}
|
||
|
||
/* ═══ SCENARIO ANALYSIS ══════════════════════════════════════════ */
|
||
function buildScenarios(risk,yld,g10,em,cmd){
|
||
const sc=[];const vix=risk.VIX?.level||20,dxy=risk.DXY?.level,dChg=risk.DXY?.chg||0,us10=yld.US?.y10;
|
||
const eur=g10.find(x=>x.pair==="EURUSD"),gbp=g10.find(x=>x.pair==="GBPUSD"),jpy=g10.find(x=>x.pair==="USDJPY");
|
||
sc.push({pair:"DXY",spot:dxy,bull:{target:dxy?+(dxy*1.03).toFixed(1):null,label:"USD Strength",drivers:["Hot CPI → Fed repricing hawkish","Risk-off episode drives safe-haven USD bid","US growth outperformance vs rest of G10"],prob:vix>25||dChg>0.3?"40%":"25%"},base:{target:dxy?+dxy.toFixed(1):null,label:"Range-bound",drivers:["Gradual disinflation keeps Fed on hold","Growth differential narrows modestly","Cross-currents offset — no dominant theme"],prob:"45%"},bear:{target:dxy?+(dxy*0.97).toFixed(1):null,label:"USD Weakness",drivers:["Dovish Fed pivot / rate cut acceleration","Global growth recovery reduces USD haven demand","Twin deficit concerns resurface"],prob:vix<18&&dChg<-0.3?"40%":"30%"}});
|
||
if(eur?.spot)sc.push({pair:"EURUSD",spot:eur.spot,bull:{target:+(eur.spot*1.04).toFixed(4),label:"EUR Rally",drivers:["ECB holds while Fed cuts → rate differential narrows","Eurozone PMIs inflect higher","Ukraine ceasefire catalyst"],prob:"30%"},base:{target:+eur.spot.toFixed(4),label:"Consolidation",drivers:["ECB & Fed both on hold — carry neutral","EZ growth stagnant but not contracting","Range trading 1.05–1.10"],prob:"45%"},bear:{target:+(eur.spot*0.96).toFixed(4),label:"EUR Sell-off",drivers:["Eurozone recession deepens","US exceptionalism trade resumes","Energy shock / geopolitical premium"],prob:"25%"}});
|
||
if(jpy?.spot)sc.push({pair:"USDJPY",spot:jpy.spot,bull:{target:+(jpy.spot*1.04).toFixed(2),label:"JPY Weakness",drivers:["BoJ stays ultra-dovish, US yields stay elevated","Carry trade revival on low vol","Japan CPI rolls over"],prob:"30%"},base:{target:+jpy.spot.toFixed(2),label:"Sideways",drivers:["BoJ gradual normalization priced in","US-JP yield gap stable","MoF intervention threat caps upside"],prob:"40%"},bear:{target:+(jpy.spot*0.96).toFixed(2),label:"JPY Strength",drivers:["BoJ rate hike surprise / YCC exit","Global risk-off → JPY safe haven","US recession fears → Treasury rally"],prob:"30%"}});
|
||
if(gbp?.spot)sc.push({pair:"GBPUSD",spot:gbp.spot,bull:{target:+(gbp.spot*1.03).toFixed(4),label:"GBP Rally",drivers:["BoE holds while Fed cuts","UK services inflation stays sticky","Fiscal outlook stabilises"],prob:"30%"},base:{target:+gbp.spot.toFixed(4),label:"Range-bound",drivers:["BoE cautious easing — 25bp per quarter","UK growth muddles through","Brexit frictions fade from focus"],prob:"45%"},bear:{target:+(gbp.spot*0.97).toFixed(4),label:"GBP Sell-off",drivers:["UK recession + rapid BoE cuts","Gilt market stress returns","Broad USD strength"],prob:"25%"}});
|
||
return sc;
|
||
}
|
||
function ScenPanel({s}){
|
||
const cols=[{k:"bull",label:"BULL",emoji:"🟢",c:C.g},{k:"base",label:"BASE",emoji:"🟡",c:C.am},{k:"bear",label:"BEAR",emoji:"🔴",c:C.r}];
|
||
return <div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:16,marginBottom:12}}>
|
||
<div style={{display:"flex",alignItems:"center",gap:8,marginBottom:12}}>
|
||
<span style={{color:C.b,fontSize:14,fontWeight:800,fontFamily:Mn}}>{s.pair}</span>
|
||
<span style={{color:C.txD,fontSize:12}}>Spot: <span style={{color:C.tx,fontFamily:Mn}}>{s.spot!=null?s.spot:"—"}</span></span>
|
||
</div>
|
||
<div className="g3" style={{display:"grid",gridTemplateColumns:"1fr 1fr 1fr",gap:10}}>
|
||
{cols.map(cl=>{
|
||
const sc=s[cl.k];if(!sc)return null;
|
||
return <div key={cl.k} style={{background:C.bg,borderRadius:6,border:"1px solid "+cl.c+"30",padding:12}}>
|
||
<div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:8}}>
|
||
<span style={{color:cl.c,fontSize:11,fontWeight:700,textTransform:"uppercase",letterSpacing:1}}>{cl.emoji} {cl.label}</span>
|
||
<span style={{color:cl.c,fontSize:11,fontWeight:700,fontFamily:Mn,background:cl.c+"18",padding:"2px 8px",borderRadius:3}}>{sc.prob}</span>
|
||
</div>
|
||
<div style={{color:C.tx,fontWeight:700,fontSize:13,marginBottom:4}}>{sc.label}</div>
|
||
{sc.target!=null&&<div style={{color:cl.c,fontFamily:Mn,fontSize:12,marginBottom:8}}>Target: {sc.target}</div>}
|
||
<div style={{display:"grid",gap:4}}>{sc.drivers.map((d,i)=><div key={i} style={{color:C.txD,fontSize:11,lineHeight:1.5}}>→ {d}</div>)}</div>
|
||
</div>;
|
||
})}
|
||
</div>
|
||
</div>;
|
||
}
|
||
function Scenarios({risk,yld,g10,em,cmd}){
|
||
const scenarios=buildScenarios(risk,yld,g10,em,cmd);
|
||
return <div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:20}}>
|
||
<div style={{display:"flex",alignItems:"center",gap:10,marginBottom:16}}><span style={{fontSize:20}}>🎯</span><div><div style={{color:C.tx,fontSize:16,fontWeight:700}}>Scenario Analysis</div><div style={{color:C.txM,fontSize:11}}>Bull / Base / Bear for key FX pairs — computed from live data</div></div></div>
|
||
{scenarios.map((s,i)=><ScenPanel key={i} s={s}/>)}
|
||
</div>;
|
||
}
|
||
|
||
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>}
|
||
|
||
/* ═══ DEEP DIVE ═══════════════════════════════════════════════════ */
|
||
function Deep({ctry,g10,em,yld,news,nL,risk,macro,policyRates}){
|
||
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 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 [ai,setAi]=useState(null);
|
||
const [aiLd,setAiLd]=useState(false);
|
||
const [aiErr,setAiErr]=useState("");
|
||
|
||
const gen = async () => {
|
||
setAiLd(true);
|
||
setAiErr("");
|
||
|
||
try {
|
||
const body = {
|
||
country: ctry,
|
||
currency: ccy,
|
||
fx: fp ? {
|
||
pair: fp.pair,
|
||
spot: fp.spot,
|
||
d1: fp.d1,
|
||
w1: fp.w1,
|
||
m1: fp.m1,
|
||
ytd: fp.ytd
|
||
} : null,
|
||
yields: y,
|
||
centralBank: tpl ? {
|
||
country: tpl.c,
|
||
policyRate: cbRate
|
||
} : null,
|
||
macro: m,
|
||
riskBar: risk,
|
||
headlines: dN.slice(0, 8).map(n => ({
|
||
headline: n.headline,
|
||
source: n.source,
|
||
datetime: n.datetime
|
||
}))
|
||
};
|
||
|
||
const res = await fetch("/.netlify/functions/deep-dive-summary", {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json"
|
||
},
|
||
body: JSON.stringify(body)
|
||
});
|
||
|
||
const data = await res.json();
|
||
|
||
if (!res.ok) {
|
||
throw new Error(data.error || "Failed to generate AI summary");
|
||
}
|
||
|
||
setAi(data);
|
||
} catch (err) {
|
||
setAiErr(err.message || "AI request failed");
|
||
} finally {
|
||
setAiLd(false);
|
||
}
|
||
};
|
||
|
||
return <div>
|
||
<div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:20,marginBottom:16}}>
|
||
<div style={{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:12}}>
|
||
<div style={{color:C.tx,fontSize:15,fontWeight:700}}>🧠 AI MACRO SUMMARY — {ctry} ({ccy})</div>
|
||
<button
|
||
onClick={gen}
|
||
disabled={aiLd}
|
||
style={{
|
||
padding:"8px 18px",
|
||
background:"linear-gradient(135deg,"+C.tl+",#38bdf8)",
|
||
color:"#fff",
|
||
border:"none",
|
||
borderRadius:6,
|
||
fontSize:12,
|
||
fontWeight:700
|
||
}}
|
||
>
|
||
{aiLd ? "Generating..." : ai ? "↻ Regenerate View" : "Generate View"}
|
||
</button>
|
||
</div>
|
||
|
||
{aiErr && (
|
||
<div style={{padding:12,background:"#2a1111",border:"1px solid #7f1d1d",borderRadius:6,color:"#fca5a5",marginBottom:12}}>
|
||
{aiErr}
|
||
</div>
|
||
)}
|
||
|
||
{!ai && !aiLd && !aiErr && (
|
||
<div style={{padding:20,background:C.bg,borderRadius:6,border:"1px dashed "+C.bd,textAlign:"center",color:C.txM,fontSize:12}}>
|
||
Click "Generate View" for a live AI deep dive
|
||
</div>
|
||
)}
|
||
|
||
{ai && (
|
||
<>
|
||
<div style={{marginBottom:14}}>
|
||
<div style={{color:C.tl,fontSize:11,fontWeight:700,textTransform:"uppercase",letterSpacing:1,marginBottom:6}}>
|
||
Trading Narrative
|
||
</div>
|
||
<div style={{padding:14,background:C.bg,borderRadius:6,border:"1px solid "+C.tl+"25",color:C.tx,fontSize:13,lineHeight:1.75}}>
|
||
{ai.narrative}
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{color:C.tl,fontSize:11,fontWeight:700,textTransform:"uppercase",letterSpacing:1,marginBottom:8}}>
|
||
Macroeconomic Data & Trends
|
||
</div>
|
||
|
||
<div style={{display:"grid",gridTemplateColumns:"1fr",gap:10,marginBottom:14}}>
|
||
{ai.macro_sections.map((sec, i) => (
|
||
<div key={i} style={{background:C.bg,padding:14,borderRadius:6,border:"1px solid "+C.bd}}>
|
||
<div style={{color:C.b,fontSize:11,fontWeight:700,textTransform:"uppercase",letterSpacing:1,marginBottom:8}}>
|
||
{sec.title}
|
||
</div>
|
||
<div style={{display:"grid",gap:6}}>
|
||
{sec.bullets.map((b, j) => (
|
||
<div key={j} style={{color:C.txD,fontSize:12,lineHeight:1.6}}>
|
||
• {b}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div className="g2" style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:12}}>
|
||
<div style={{background:C.bg,padding:14,borderRadius:6,border:"1px solid "+C.bd}}>
|
||
<div style={{color:C.g,fontSize:11,fontWeight:700,textTransform:"uppercase",letterSpacing:1,marginBottom:8}}>
|
||
FX View
|
||
</div>
|
||
<div style={{color:C.tx,fontWeight:700,marginBottom:6}}>{ai.fx_view.direction}</div>
|
||
<div style={{color:C.txD,fontSize:12,lineHeight:1.6,marginBottom:8}}>{ai.fx_view.rationale}</div>
|
||
{ai.fx_view.levels_to_watch.map((x, i) => (
|
||
<div key={i} style={{color:C.am,fontSize:11,marginBottom:4}}>• {x}</div>
|
||
))}
|
||
</div>
|
||
|
||
<div style={{background:C.bg,padding:14,borderRadius:6,border:"1px solid "+C.bd}}>
|
||
<div style={{color:C.r,fontSize:11,fontWeight:700,textTransform:"uppercase",letterSpacing:1,marginBottom:8}}>
|
||
Rates View
|
||
</div>
|
||
<div style={{color:C.tx,fontWeight:700,marginBottom:6}}>{ai.rates_view.bias}</div>
|
||
<div style={{color:C.txD,fontSize:12,lineHeight:1.6}}>{ai.rates_view.rationale}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="g2" style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:12,marginTop:12}}>
|
||
<div style={{background:C.bg,padding:14,borderRadius:6,border:"1px solid "+C.bd}}>
|
||
<div style={{color:C.am,fontSize:11,fontWeight:700,textTransform:"uppercase",letterSpacing:1,marginBottom:8}}>
|
||
Key Risks
|
||
</div>
|
||
{ai.key_risks.map((x, i) => (
|
||
<div key={i} style={{color:C.txD,fontSize:12,lineHeight:1.6,marginBottom:4}}>• {x}</div>
|
||
))}
|
||
</div>
|
||
|
||
<div style={{background:C.bg,padding:14,borderRadius:6,border:"1px solid "+C.bd}}>
|
||
<div style={{color:C.tl,fontSize:11,fontWeight:700,textTransform:"uppercase",letterSpacing:1,marginBottom:8}}>
|
||
Headline Takeaways
|
||
</div>
|
||
{ai.headline_takeaways.map((x, i) => (
|
||
<div key={i} style={{color:C.txD,fontSize:12,lineHeight:1.6,marginBottom:4}}>• {x}</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
<div className="g2" style={{display:"grid",gridTemplateColumns:"1.1fr 0.9fr",gap:16}}>
|
||
<div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:16}}>
|
||
<div style={{display:"flex",justifyContent:"space-between",alignItems:"flex-start",marginBottom:8}}>
|
||
<div>
|
||
<div style={{color:C.txM,fontSize:10,fontWeight:700,textTransform:"uppercase",letterSpacing:1,marginBottom:4}}>
|
||
FX — {pOf[ctry]}
|
||
</div>
|
||
<div style={{color:C.tx,fontSize:34,fontWeight:800,fontFamily:Mn}}>
|
||
{fp?.spot!=null?Number(fp.spot).toFixed(fp.spot>100?2:4):"—"}
|
||
</div>
|
||
<div style={{display:"flex",gap:8,flexWrap:"wrap",marginTop:6}}>
|
||
{[["1D",fp?.d1],["1W",fp?.w1],["1M",fp?.m1],["YTD",fp?.ytd]].map(([t,v])=>(
|
||
<span key={t} style={{fontSize:11,fontFamily:Mn,color:v!=null?cc(v):C.txM}}>
|
||
{t}: {v!=null?fc(v):"—"}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<TV sym={tvF[ctry]} h={290}/>
|
||
</div>
|
||
|
||
<div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:16}}>
|
||
<div style={{color:C.tx,fontSize:14,fontWeight:700,marginBottom:10}}>Yield Curve</div>
|
||
|
||
<div style={{display:"grid",gap:6,marginBottom:12}}>
|
||
{Object.entries(y)
|
||
.filter(([k,v]) => k.startsWith("y") && v != null)
|
||
.map(([k,v]) => (
|
||
<div key={k} style={{display:"flex",justifyContent:"space-between",padding:"4px 0",borderBottom:"1px solid "+C.bd+"30"}}>
|
||
<span style={{color:C.txD,fontSize:11}}>{k.replace("y","").toUpperCase()}</span>
|
||
<span style={{color:C.tx,fontFamily:Mn,fontSize:12}}>{v.toFixed(2)}%</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}}>
|
||
Yield chart available for US, Eurozone, UK, Japan, Canada, Australia
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="g3" style={{display:"grid",gridTemplateColumns:"1fr 1fr 1fr",gap:16,marginTop:16}}>
|
||
<div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:16}}>
|
||
<div style={{color:C.tx,fontSize:13,fontWeight:700,marginBottom:10}}>Central Bank</div>
|
||
<div style={{display:"grid",gap:8}}>
|
||
<div style={{display:"flex",justifyContent:"space-between"}}><span style={{color:C.txM,fontSize:11}}>Policy Rate</span><span style={{color:C.tx,fontWeight:700,fontFamily:Mn}}>{cbRate!=null ? cbRate.toFixed(2)+"%" : "—"}</span></div>
|
||
<div style={{display:"flex",justifyContent:"space-between"}}><span style={{color:C.txM,fontSize:11}}>Stance</span><span style={{color:C.am,fontSize:11,fontWeight:700}}>{m.stance||"—"}</span></div>
|
||
<div style={{display:"flex",justifyContent:"space-between"}}><span style={{color:C.txM,fontSize:11}}>Last Decision</span><span style={{color:C.tx,fontSize:11}}>{m.last||"—"}</span></div>
|
||
<div style={{display:"flex",justifyContent:"space-between"}}><span style={{color:C.txM,fontSize:11}}>Next Meeting</span><span style={{color:C.g,fontSize:11}}>{m.next||"—"}</span></div>
|
||
<div style={{display:"flex",justifyContent:"space-between"}}><span style={{color:C.txM,fontSize:11}}>Pricing</span><span style={{color:C.tx,fontSize:11}}>{m.pricing||"—"}</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:16}}>
|
||
<div style={{color:C.tx,fontSize:13,fontWeight:700,marginBottom:10}}>Macro Snapshot</div>
|
||
<div className="g3" style={{display:"grid",gridTemplateColumns:"1fr 1fr 1fr",gap:8}}>
|
||
<div style={{background:C.bg,padding:10,borderRadius:6}}>
|
||
<div style={{color:C.txM,fontSize:10}}>CPI YoY</div>
|
||
<div style={{color:C.tx,fontSize:22,fontWeight:700,fontFamily:Mn}}>{m.cpi||"—"}</div>
|
||
<div style={{color:C.am,fontSize:10}}>Prior {m.cpiP||"—"}</div>
|
||
</div>
|
||
<div style={{background:C.bg,padding:10,borderRadius:6}}>
|
||
<div style={{color:C.txM,fontSize:10}}>GDP</div>
|
||
<div style={{color:C.tx,fontSize:22,fontWeight:700,fontFamily:Mn}}>{m.gdp||"—"}</div>
|
||
<div style={{color:C.am,fontSize:10}}>{m.gdpT||"—"}</div>
|
||
</div>
|
||
<div style={{background:C.bg,padding:10,borderRadius:6}}>
|
||
<div style={{color:C.txM,fontSize:10}}>Unemployment</div>
|
||
<div style={{color:C.tx,fontSize:22,fontWeight:700,fontFamily:Mn}}>{m.unemp||"—"}</div>
|
||
<div style={{color:C.am,fontSize:10}}>{m.unempT||"—"}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:16}}>
|
||
<div style={{display:"flex",alignItems:"center",gap:8,marginBottom:10}}>
|
||
<span style={{color:C.tx,fontSize:13,fontWeight:700}}>News</span>
|
||
<Dot on={nL}/>
|
||
</div>
|
||
<div style={{display:"grid",gap:8}}>
|
||
{dN.slice(0,4).map((n,i)=>(
|
||
<div key={i} style={{padding:"8px 10px",background:C.bg,borderRadius:6}}>
|
||
<a href={n.url||"#"} target="_blank" rel="noopener" style={{color:C.tx,fontSize:11,fontWeight:500,lineHeight:1.4,textDecoration:"none",display:"block"}}>
|
||
{n.headline}
|
||
</a>
|
||
<div style={{color:C.txM,fontSize:9,marginTop:3}}>
|
||
{n.source} · {n.datetime?new Date(n.datetime*1000).toLocaleString("en-GB",{day:"numeric",month:"short",hour:"2-digit",minute:"2-digit"}):"—"}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>;
|
||
}
|
||
|
||
/* ═══ 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 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>}
|
||
|
||
function Clock(){const [clk,setClk]=useState(new Date());useEffect(()=>{const t=setInterval(()=>setClk(new Date()),1000);return()=>clearInterval(t)},[]);const lt=clk.toLocaleString("en-GB",{timeZone:"Europe/London",hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:false});const nt=clk.toLocaleString("en-US",{timeZone:"America/New_York",hour:"2-digit",minute:"2-digit",hour12:false});const tt=clk.toLocaleString("en-US",{timeZone:"Asia/Tokyo",hour:"2-digit",minute:"2-digit",hour12:false});return <span className="hmob" style={{fontSize:10,color:C.txM}}>LON <span style={{color:C.tx,fontFamily:Mn}}>{lt}</span> NY <span style={{color:C.tx,fontFamily:Mn}}>{nt}</span> TKY <span style={{color:C.tx,fontFamily:Mn}}>{tt}</span></span>}
|
||
|
||
/* ═══ APP ══════════════════════════════════════════════════════════ */
|
||
function App(){
|
||
const [sec,setSec]=useState("brief");const [ctry,setCtry]=useState("US");const [sO,setSO]=useState(false);
|
||
const [keys,setKeys]=useState({finnhub:gk("finnhub")||_K.finnhub});
|
||
const [g10D,sG10]=useState([]);const [emD,sEM]=useState([]);const [fxL,sFxL]=useState(false);
|
||
const [yld,sYld]=useState({});const [yL,sYL]=useState(false);
|
||
const [risk,sRisk]=useState({DXY:{level:null,chg:null},VIX:{level:null,chg:null},Gold:{level:null,chg:null},WTI:{level:null,chg:null},Brent:{level:null,chg:null},"US 10Y":{level:null,chg:null}});
|
||
const [rL,sRL]=useState(false);const [cal,sCal]=useState([]);const [cL,sCL]=useState(false);const [news,sNews]=useState([]);const [nL,sNL]=useState(false);
|
||
const [cmd,sCmd]=useState({gold:{},wti:{},brent:{}});const [ld,sLd]=useState(false);const [lr,sLR]=useState(null);
|
||
const [etfs,setEtfs]=useState({});const [vol,setVol]=useState({});const [bondEtfs,setBondEtfs]=useState({});
|
||
const [macro,setMacro]=useState({});const [policyRates,setPolicyRates]=useState({});
|
||
const svK=useCallback(nk=>{setKeys(nk);Object.entries(nk).forEach(([k,v])=>sk(k,v))},[]);
|
||
const load=useCallback(async()=>{
|
||
sLd(true);
|
||
try{
|
||
const data=await fData();
|
||
setEtfs(data.etfs||{});setVol(data.volatility||{});setBondEtfs(data.bond_etfs||{});
|
||
setMacro(data.macro||{});setPolicyRates(data.policy_rates||{});
|
||
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",beat:e.beat||null}));sCal(parsedCal);sCL(true);
|
||
sLR(new Date());
|
||
}catch(err){console.error("Failed to load dashboard data",err)}finally{sLd(false)}
|
||
},[]);
|
||
useEffect(()=>{load()},[load]);
|
||
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: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}/></div>}
|
||
{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>}
|
||
{sec==="deep"&&<div><div style={{display:"flex",flexWrap:"wrap",gap:4,marginBottom:16}}>{RT.map(c=><button key={c.c} onClick={()=>setCtry(c.c)} style={{padding:"6px 12px",fontSize:11,fontWeight:600,borderRadius:4,background:ctry===c.c?C.ac+"25":C.bgC,color:ctry===c.c?C.ac:C.txD,border:"1px solid "+(ctry===c.c?C.ac+"50":C.bd)}}>{c.f} {c.c}</button>)}</div><Deep ctry={ctry} g10={g10D} em={emD} yld={yld} news={news} nL={nL} risk={risk} macro={macro} policyRates={policyRates}/></div>}
|
||
{sec==="cal"&&<div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:16}}><div style={{marginBottom:12}}><span style={{color:C.tx,fontSize:15,fontWeight:700}}>Economic Calendar</span></div><Cal evts={cal} live={cL}/></div>}
|
||
</div>
|
||
<div style={{textAlign:"center",padding:"12px 0",borderTop:"1px solid "+C.bd+"20"}}><span style={{color:C.txM,fontSize:10}}>FX RADAR v4 · <Dot on={fxL}/>{fxL?"Live":"..."}</span></div>
|
||
<Sett open={sO} close={()=>setSO(false)} keys={keys} save={svK}/>
|
||
</div>;
|
||
}
|
||
ReactDOM.createRoot(document.getElementById("root")).render(<App/>);
|
||
</script></body></html>
|