;
function Tabs({tabs,act,set}){return {tabs.map(t=>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} )}
}
function Tbl({cols,rows,sm}){return {cols.map(c=>{c.l} )} {rows.map((r,i)=>{cols.map(c=>{const v=c.rn?c.rn(r):r[c.k];return {v!=null?v:"โ"} })} )}
}
-function TV({sym,h=280}){const ref=useRef(null);const [vis,setVis]=useState(false);useEffect(()=>{if(!ref.current||!sym)return;const obs=new IntersectionObserver(([e])=>{if(e.isIntersecting){setVis(true);obs.disconnect()}},{rootMargin:"200px"});obs.observe(ref.current);return()=>obs.disconnect()},[sym]);const ph={height:h,borderRadius:6,border:"1px solid "+C.bd,display:"flex",alignItems:"center",justifyContent:"center",color:C.txM,fontSize:11,background:C.bg};if(!sym)return No chart
;return }
+function TV({sym,h=280}){const [shouldLoad,setShouldLoad]=React.useState(false);React.useEffect(()=>{setShouldLoad(false)},[sym]);if(!sym)return No chart
;if(!shouldLoad)return Chart ready to load
{sym}
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 ;return }
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 {on?"RISK ON":off?"RISK OFF":"MIXED"} {Object.entries(d).map(([k,v])=>{k} {v.level!=null?Number(v.level).toLocaleString(undefined,{maximumFractionDigits:2}):"โ"} {v.chg!=null&&{k.includes("10Y")?fb(v.chg):fc(v.chg,k==="VIX"?"":"%")} } )}
}
/* โโโ PAGES โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
@@ -347,14 +347,14 @@ function Deep({ctry,g10,em,yld,news,nL,risk}){
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").map(r=>({ccy:r.ccy,c:+((yld[r.c]?.y2??r.cb)-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 ๐ง AI Morning Brief
Live FX, yields, carry, commodities & calendar
{ts&&Generated {ts} }{ld?<>Analysing>:br?"โป Regen":"โก Generate Brief"}
{br?
:
Click "Generate Brief"
}
}
function Cal({evts,live}){const [fl,sF]=useState("All");const gF=["๐บ๐ธ","๐ช๐บ","๐ฌ๐ง","๐ฏ๐ต","๐จ๐ญ","๐ฆ๐บ","๐ณ๐ฟ","๐จ๐ฆ","๐ธ๐ช","๐ณ๐ด"];return {["All","G10","EM","CB"].map(f=>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} )}
r.imp==="red"?C.r:C.am},{k:"prev",l:"Prev"},{k:"fcast",l:"Fcst"},{k:"act",l:"Act",cl:r=>(r.act&&r.act!=="โ")?C.g:C.txM}]} 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})}/> }
-function Sett({open,close,keys,save}){const [lk,sL]=useState(keys);useEffect(()=>sL(keys),[keys,open]);if(!open)return null;return e.stopPropagation()}>
โ API Keys โ
{[{k:"fred",l:"FRED โ US yields",u:"https://fred.stlouisfed.org/docs/api/api_key.html"},{k:"finnhub",l:"Finnhub โ VIX, commodities, news, calendar",u:"https://finnhub.io/register"}].map(a=>
)}
{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 }
+function Sett({open,close,keys,save}){const [lk,sL]=useState(keys);useEffect(()=>sL(keys),[keys,open]);if(!open)return null;return e.stopPropagation()}>
โ API Keys โ
{[{k:"finnhub",l:"Finnhub โ VIX, commodities, news, calendar",u:"https://finnhub.io/register"}].map(a=>
)}
{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 }
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 LON {lt} NY {nt} TKY {tt} }
/* โโโ APP โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
function App(){
const [sec,setSec]=useState("brief");const [ctry,setCtry]=useState("US");const [sO,setSO]=useState(false);
- const [keys,setKeys]=useState({fred:gk("fred")||_K.fred,finnhub:gk("finnhub")||_K.finnhub});
+ 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}});
@@ -364,7 +364,7 @@ function App(){
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{}
- if(keys.fred){try{const ids=Object.keys(FY);const res=await Promise.all(ids.map(s=>fFD(s,keys.fred)));const uy={};ids.forEach((s,i)=>{if(res[i]!=null)uy[FY[s]]=res[i]});sYld(p=>({...p,US:uy}));sYL(true);if(uy.y10!=null)sRisk(p=>({...p,"US 10Y":{level:uy.y10,chg: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{}
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)}}));
diff --git a/netlify/functions/fred-yields.js b/netlify/functions/fred-yields.js
new file mode 100644
index 0000000..9f16b1c
--- /dev/null
+++ b/netlify/functions/fred-yields.js
@@ -0,0 +1,52 @@
+exports.handler = async () => {
+ try {
+ const key = process.env.FRED_API_KEY;
+ if (!key) {
+ return {
+ statusCode: 500,
+ body: JSON.stringify({ error: "Missing FRED_API_KEY" })
+ };
+ }
+
+ const seriesMap = {
+ y2: "DGS2",
+ y10: "DGS10",
+ y30: "DGS30",
+ y3m: "DGS3MO"
+ };
+
+ const entries = await Promise.all(
+ Object.entries(seriesMap).map(async ([label, seriesId]) => {
+ const url =
+ "https://api.stlouisfed.org/fred/series/observations" +
+ `?series_id=${seriesId}` +
+ `&api_key=${key}` +
+ "&file_type=json&sort_order=desc&limit=10";
+
+ const res = await fetch(url);
+ const data = await res.json();
+
+ if (!res.ok) {
+ throw new Error(data?.error_message || `FRED failed for ${seriesId}`);
+ }
+
+ const obs = (data.observations || []).find(x => x.value !== ".");
+ return [label, obs ? Number(obs.value) : null];
+ })
+ );
+
+ return {
+ statusCode: 200,
+ headers: {
+ "Content-Type": "application/json",
+ "Cache-Control": "public, max-age=300"
+ },
+ body: JSON.stringify(Object.fromEntries(entries))
+ };
+ } catch (err) {
+ return {
+ statusCode: 500,
+ body: JSON.stringify({ error: err.message || "fred-yields failed" })
+ };
+ }
+};