2026-04-06 22:21:36 +01:00
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "UTF-8" />< meta name = "viewport" content = "width=device-width,initial-scale=1.0" />
< title > FX RADAR 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 : 13 px }:: -webkit-scrollbar { width : 5 px ; height : 5 px }:: -webkit-scrollbar-track { background : #0a0e17 }:: -webkit-scrollbar-thumb { background : #334155 ; border-radius : 3 px } button { font-family : inherit ; cursor : pointer } table { border-spacing : 0 }@ keyframes spin { to { transform : rotate ( 360 deg )}}. sp { width : 12 px ; height : 12 px ; border : 2 px solid #334155 ; border-top-color : #6366f1 ; border-radius : 50 % ; animation : spin .8 s linear infinite ; display : inline-block ; vertical-align : middle }@ media ( max-width : 900px ) {. g2 { grid-template-columns : 1 fr !important }. g3 { grid-template-columns : 1 fr !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 = { fred : _d ( "OTRlZGYwZDQxNThhZmEwZGNhNDRlYjdmMjRiN2U1MTY=" ), 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 ( s , k ){ if ( ! k ) return null ; try { const d = await jf ( "https://api.stlouisfed.org/fred/series/observations?series_id=" + s + "&api_key=" + k + "&file_type=json&sort_order=desc&limit=5" ); const o = d . observations ? . filter ( x => x . value !== "." ); return o ? . length ?+ o [ 0 ]. value : null } catch { 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 }}
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 FY = { "DGS1MO" : "y1m" , "DGS3MO" : "y3m" , "DGS6MO" : "y6m" , "DGS1" : "y1" , "DGS2" : "y2" , "DGS5" : "y5" , "DGS10" : "y10" , "DGS30" : "y30" };
const RT = [{ c : "US" , f : "🇺🇸" , cb : 5.50 , ccy : "USD" },{ c : "Eurozone" , f : "🇪🇺" , cb : 4.50 , ccy : "EUR" },{ c : "UK" , f : "🇬🇧" , cb : 5.25 , ccy : "GBP" },{ c : "Japan" , f : "🇯🇵" , cb : 0.10 , ccy : "JPY" },{ c : "Switzerland" , f : "🇨🇭" , cb : 1.75 , ccy : "CHF" },{ c : "Australia" , f : "🇦🇺" , cb : 4.35 , ccy : "AUD" },{ c : "New Zealand" , f : "🇳🇿" , cb : 5.50 , ccy : "NZD" },{ c : "Canada" , f : "🇨🇦" , cb : 5.00 , ccy : "CAD" },{ c : "Sweden" , f : "🇸🇪" , cb : 4.00 , ccy : "SEK" },{ c : "Norway" , f : "🇳🇴" , cb : 4.50 , ccy : "NOK" },{ c : "Brazil" , f : "🇧🇷" , cb : 10.75 , ccy : "BRL" },{ c : "Mexico" , f : "🇲🇽" , cb : 11.00 , ccy : "MXN" },{ c : "South Africa" , f : "🇿🇦" , cb : 8.25 , ccy : "ZAR" },{ c : "India" , f : "🇮🇳" , cb : 6.50 , ccy : "INR" },{ c : "South Korea" , f : "🇰🇷" , cb : 3.50 , ccy : "KRW" },{ c : "Turkey" , f : "🇹🇷" , cb : 50.00 , ccy : "TRY" },{ c : "Poland" , f : "🇵🇱" , cb : 5.75 , 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" ]};
// ═══ FULL MACRO DATABASE ═════════════════════════════════════════
const MD = {
2026-04-06 22:29:52 +00:00
US : { cpi : "3.2%" , cpiP : "3.1%" , cpiT : "re-accelerating" , core : "3.8%" , gdp : "2.5%" , gdpT : "resilient" , unemp : "3.7%" , unempT : "near cycle lows" , wages : "4.1% YoY" , pmi : "52.6" , tb : "-$68.5B" , stance : "Hawkish hold" , next : "2026-05-01" , last : "Hold 5.50%" , pricing : "2 cuts in 12m" },
Eurozone : { cpi : "2.6%" , cpiP : "2.8%" , cpiT : "falling toward target" , core : "3.1%" , gdp : "0.3%" , gdpT : "anaemic" , unemp : "6.4%" , unempT : "stable" , wages : "3.5% YoY" , pmi : "49.2" , tb : "+€28B" , stance : "Easing bias" , next : "2026-04-10" , last : "Hold 4.50%" , pricing : "3 cuts in 12m" },
UK : { cpi : "3.4%" , cpiP : "4.0%" , cpiT : "falling but sticky services" , core : "4.5%" , gdp : "0.2%" , gdpT : "tepid" , unemp : "4.2%" , unempT : "rising" , wages : "5.6% YoY" , pmi : "53.8" , tb : "-£4.8B" , stance : "Cautious hold" , next : "2026-05-09" , last : "Hold 5.25%" , pricing : "2 cuts in 12m" },
Japan : { cpi : "2.8%" , cpiP : "2.5%" , cpiT : "rising and broadening" , core : "2.5%" , gdp : "1.2%" , gdpT : "recovering" , unemp : "2.5%" , unempT : "tight" , wages : "2.1% (Shunto: 5.3%)" , pmi : "51.4" , tb : "-¥580B" , stance : "Cautious normalisation" , next : "2026-04-26" , last : "Hold 0.10%" , pricing : "1 hike in 12m" },
Switzerland : { cpi : "1.4%" , cpiP : "1.2%" , cpiT : "low" , core : "1.2%" , gdp : "0.6%" , gdpT : "modest" , unemp : "2.3%" , unempT : "stable" , wages : "1.8% YoY" , pmi : "48.5" , tb : "+CHF5.2B" , stance : "Easing" , next : "2026-06-19" , last : "Cut to 1.75%" , pricing : "1-2 more cuts" },
Australia : { cpi : "3.6%" , cpiP : "3.4%" , cpiT : "sticky" , core : "3.9%" , gdp : "1.5%" , gdpT : "slowing" , unemp : "3.9%" , unempT : "stable" , wages : "4.2% YoY" , pmi : "50.1" , tb : "+A$8.5B" , stance : "On hold" , next : "2026-05-20" , last : "Hold 4.35%" , pricing : "2 cuts in 12m" },
"New Zealand" : { cpi : "4.7%" , cpiP : "4.7%" , cpiT : "sticky" , core : "4.3%" , gdp : "-0.3%" , gdpT : "recession" , unemp : "4.3%" , unempT : "rising" , wages : "3.8% YoY" , pmi : "47.1" , tb : "-NZ$1.2B" , stance : "Easing" , next : "2026-05-28" , last : "Cut to 5.50%" , pricing : "3-4 cuts" },
Canada : { cpi : "2.9%" , cpiP : "2.8%" , cpiT : "near target" , core : "3.2%" , gdp : "1.0%" , gdpT : "slowing" , unemp : "5.8%" , unempT : "rising" , wages : "3.9% YoY" , pmi : "49.8" , tb : "-C$1.5B" , stance : "Easing" , next : "2026-04-16" , last : "Cut to 5.00%" , pricing : "2-3 more cuts" },
Sweden : { cpi : "2.2%" , cpiP : "2.5%" , cpiT : "at target" , core : "2.5%" , gdp : "0.1%" , gdpT : "stagnant" , unemp : "8.2%" , unempT : "high" , wages : "3.5% YoY" , pmi : "48.2" , tb : "+SEK12B" , stance : "Easing" , next : "2026-05-07" , last : "Cut to 4.00%" , pricing : "2-3 more cuts" },
Norway : { cpi : "3.6%" , cpiP : "3.9%" , cpiT : "sticky" , core : "4.1%" , gdp : "0.8%" , gdpT : "stable" , unemp : "3.7%" , unempT : "low" , wages : "5.2% YoY" , pmi : "51.8" , tb : "+NOK80B" , stance : "Hawkish hold" , next : "2026-05-08" , last : "Hold 4.50%" , pricing : "1-2 cuts" },
Brazil : { cpi : "4.5%" , cpiP : "4.2%" , cpiT : "rising" , core : "4.0%" , gdp : "2.1%" , gdpT : "strong" , unemp : "7.8%" , unempT : "falling" , wages : "N/A" , pmi : "52.1" , tb : "+$7.2B" , stance : "Tightening" , next : "2026-05-07" , last : "Hike to 10.75%" , pricing : "Terminal ~12%" },
Mexico : { cpi : "4.2%" , cpiP : "4.4%" , cpiT : "slowly falling" , core : "4.6%" , gdp : "2.5%" , gdpT : "resilient" , unemp : "2.8%" , unempT : "tight" , wages : "N/A" , pmi : "51.5" , tb : "-$5.8B" , stance : "Cautious hold" , next : "2026-05-15" , last : "Hold 11.00%" , pricing : "2-3 cuts" },
"South Africa" : { cpi : "5.3%" , cpiP : "5.1%" , cpiT : "elevated" , core : "4.6%" , gdp : "0.7%" , gdpT : "weak" , unemp : "32.1%" , unempT : "structural" , wages : "N/A" , pmi : "49.5" , tb : "-ZAR15B" , stance : "Hold" , next : "2026-05-29" , last : "Hold 8.25%" , pricing : "1-2 cuts" },
India : { cpi : "5.1%" , cpiP : "5.3%" , cpiT : "moderating" , core : "4.2%" , gdp : "6.5%" , gdpT : "strong" , unemp : "7.1%" , unempT : "stable" , wages : "N/A" , pmi : "58.3" , tb : "-$20B" , stance : "Easing bias" , next : "2026-04-09" , last : "Cut to 6.50%" , pricing : "2 more cuts" },
"South Korea" : { cpi : "2.1%" , cpiP : "2.3%" , cpiT : "at target" , core : "2.0%" , gdp : "2.0%" , gdpT : "recovering" , unemp : "2.7%" , unempT : "stable" , wages : "N/A" , pmi : "50.8" , tb : "+$4.5B" , stance : "Easing" , next : "2026-05-29" , last : "Cut to 3.50%" , pricing : "1-2 cuts" },
Turkey : { cpi : "65%" , cpiP : "67%" , cpiT : "falling from peak" , core : "70%" , gdp : "3.2%" , gdpT : "slowing" , unemp : "9.4%" , unempT : "stable" , wages : "N/A" , pmi : "49.1" , tb : "-$7.5B" , stance : "Hawkish hold" , next : "2026-04-17" , last : "Hold 50.00%" , pricing : "Cuts H2 2026" },
Poland : { cpi : "3.9%" , cpiP : "3.7%" , cpiT : "rising" , core : "4.1%" , gdp : "2.8%" , gdpT : "solid" , unemp : "5.1%" , unempT : "stable" , wages : "12% YoY" , pmi : "50.2" , tb : "-PLN5B" , stance : "On hold" , next : "2026-05-07" , last : "Hold 5.75%" , pricing : "Cuts unlikely H1" },
2026-04-06 22:21:36 +01:00
};
const DCAL = [{ date : "2026-04-08" , time : "12:30" , ctry : "🇺🇸" , ev : "FOMC Minutes" , prev : "—" , fcast : "—" , act : "—" , imp : "red" },{ date : "2026-04-09" , time : "12:30" , ctry : "🇺🇸" , ev : "US CPI YoY" , prev : "3.2%" , fcast : "3.1%" , act : "—" , imp : "red" },{ date : "2026-04-10" , time : "11:00" , ctry : "🇪🇺" , ev : "ECB Rate Decision" , prev : "4.50%" , fcast : "4.25%" , act : "—" , imp : "red" },{ date : "2026-04-10" , time : "07:00" , ctry : "🇬🇧" , ev : "UK GDP MoM" , prev : "-0.1%" , fcast : "0.2%" , act : "—" , imp : "red" }];
const CP = [ "EURUSD" , "GBPUSD" , "USDJPY" , "AUDUSD" , "USDCAD" , "NZDUSD" , "Gold" , "US10Y" ]; const CM = [[ 1 ,. 82 , - . 31 ,. 65 , - . 72 ,. 58 ,. 42 , - . 35 ],[. 82 , 1 , - . 22 ,. 55 , - . 61 ,. 48 ,. 35 , - . 28 ],[ - . 31 , - . 22 , 1 , - . 45 ,. 38 , - . 42 , - . 18 ,. 72 ],[. 65 ,. 55 , - . 45 , 1 , - . 58 ,. 85 ,. 52 , - . 42 ],[ - . 72 , - . 61 ,. 38 , - . 58 , 1 , - . 52 , - . 38 ,. 32 ],[. 58 ,. 48 , - . 42 ,. 85 , - . 52 , 1 ,. 45 , - . 38 ],[. 42 ,. 35 , - . 18 ,. 52 , - . 38 ,. 45 , 1 , - . 55 ],[ - . 35 , - . 28 ,. 72 , - . 42 ,. 32 , - . 38 , - . 55 , 1 ]];
/* ═══ 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>}
2026-04-06 22:29:52 +00:00
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 < div style = { ph } > No chart < /div>;return <div ref={ref} style={{width:"100%",height:h}}>{vis?<iframe src={"https:/ / s . tradingview . com / widgetembed /? frameElementId = tv_ "+sym.replace(/[^a-zA-Z0-9]/g,"")+" & symbol = "+encodeURIComponent(sym)+" & interval = D & range = 3 M & theme = dark & style = 1 & locale = en & hide_legend = 1 & hide_volume = 1 & hide_side_toolbar = 1 & allow_symbol_change = 0 & withdateranges = 1 & backgroundColor = 111827 & gridColor = 1e293 b "} style={{width:" 100 % ",height:h,borderRadius:6,border:" 1 px solid "+C.bd}} frameBorder=" 0 " loading=" lazy "/>:<div style={ph}>Loading chart…</div>}</div>}
2026-04-06 22:21:36 +01:00
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 ( 90 deg , "+bc+" 18 0 % , "+C.bg+" 50 % , "+bc+" 18 100 % ) ",borderBottom:" 1 px solid "+bc+" 40 ",padding:" 5 px 16 px ",display:" flex ",alignItems:" center ",gap:16,overflowX:" auto ",whiteSpace:" nowrap ",fontSize:12}}><span style={{color:bc,fontWeight:700,fontSize:10,letterSpacing:2,padding:" 2 px 8 px ",border:" 1 px 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(" 10 Y ")?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:" 1 D % ",rn:r=>fc(r.d1),cl:r=>cc(r.d1)},{k:" w1 ",l:" 1 W % ",rn:r=>fc(r.w1),cl:r=>cc(r.w1)},{k:" m1 ",l:" 1 M % ",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 · Others : CB rate only ":" 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.toFixed(2)+" % "},{k:" y2 ",l:" 2 Y ",rn:r=>r.y2!=null?r.y2.toFixed(2):" — ",cl:r=>r.y2?C.tx:C.txM},{k:" y10 ",l:" 10 Y ",rn:r=>r.y10!=null?r.y10.toFixed(2):" — ",cl:r=>r.y10?C.tx:C.txM},{k:" s ",l:" 2 s10s ",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 , 1 fr ) ",gap:12,marginBottom:16}}>{items.map(it=>{const v=d[it.k];return <div key={it.k} style={{background:C.bg,border:" 1 px 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 , 1 fr ) ",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)??a.cb,yb=gY(b.c)??b.cb,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})}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??5.5;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:" cb ",l:" CB Rate ",rn:r=>r.cb.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={RT.filter(r=>r.c!==" US ").map(r=>({...r,carry:+((yld[r.c]?.y2??r.cb)-uY).toFixed(2)})).sort((a,b)=>b.carry-a.carry).map((r,i)=>({...r,rk:i+1}))} 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 Markets({g10,em,fxL,yld,yL}){const [t,sT]=useState(" G10 FX ");return <div><Tabs tabs={[" G10 FX "," EM FX "," Rates "," Carry "]} 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}/>}</div>}
function CorrMx(){const cC=v=>v>=.7?" rgba ( 34 , 197 , 94 , "+v*.7+" ) ":v>=.3?" rgba ( 34 , 197 , 94 , "+v*.4+" ) ":v>=-.3?" rgba ( 148 , 163 , 184 , 0.1 ) ":v>=-.7?" rgba ( 239 , 68 , 68 , "+Math.abs(v)*.4+" ) ":" rgba ( 239 , 68 , 68 , "+Math.abs(v)*.7+" ) ";return <div><div style={{marginBottom:12}}><span style={{color:C.ac,fontSize:13,fontWeight:700}}>30-Day Correlation Matrix</span></div><div style={{overflowX:" auto "}}><div style={{display:" inline - block "}}><div style={{display:" flex "}}><div style={{width:70}}/>{CP.map(p=><div key={p} style={{width:40,textAlign:" center ",fontSize:8,color:C.txM,fontWeight:600,transform:" rotate ( - 45 deg ) ",height:40,display:" flex ",alignItems:" flex - end ",justifyContent:" center ",paddingBottom:4}}>{p}</div>)}</div>{CP.map((p,i)=><div key={p} style={{display:" flex "}}><div style={{width:70,fontSize:10,color:C.txD,fontWeight:600,textAlign:" right ",paddingRight:8,display:" flex ",alignItems:" center ",justifyContent:" flex - end "}}>{p}</div>{CM[i].map((v,j)=><div key={j} title={v.toFixed(2)} style={{width:40,height:40,display:" flex ",alignItems:" center ",justifyContent:" center ",background:i===j?" rgba ( 99 , 102 , 241 , 0.3 ) ":cC(v),border:" 1 px solid "+C.bd+" 40 ",fontSize:9,fontFamily:Mn,fontWeight:600,color:Math.abs(v)>.5?C.tx:C.txM}}>{i===j?" 1 ":v.toFixed(1)}</div>)}</div>)}</div></div></div>}
/* ═══ DEEP DIVE ═══════════════════════════════════════════════════ */
2026-04-06 22:29:52 +00:00
function Deep({ctry,g10,em,yld,news,nL,risk}){
2026-04-06 22:21:36 +01:00
const ccy=cOf[ctry],fp=[...g10,...em].find(x=>x.pair===pOf[ctry]),y=yld[ctry]||{},tpl=RT.find(x=>x.c===ctry),m=MD[ctry]||{};
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]);
2026-04-06 22:29:52 +00:00
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: tpl.cb
} : 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);
}
};
2026-04-06 22:21:36 +01:00
return <div>
<div style={{background:C.bgC,border:" 1 px solid "+C.bd,borderRadius:8,padding:20,marginBottom:16}}>
<div style={{display:" flex ",alignItems:" center ",justifyContent:" space - between ",marginBottom:12}}>
2026-04-06 22:29:52 +00:00
<div style={{color:C.tx,fontSize:15,fontWeight:700}}>🧠 AI MACRO SUMMARY — {ctry} ({ccy})</div>
<button
onClick={gen}
disabled={aiLd}
style={{
padding:" 8 px 18 px ",
background:" linear - gradient ( 135 deg , "+C.tl+" , # 38 bdf8 ) ",
color:" # fff ",
border:" none ",
borderRadius:6,
fontSize:12,
fontWeight:700
}}
>
{aiLd ? " Generating ... " : ai ? " ↻ Regenerate View " : " Generate View "}
</button>
2026-04-06 22:21:36 +01:00
</div>
2026-04-06 22:29:52 +00:00
{aiErr && (
<div style={{padding:12,background:" # 2 a1111 ",border:" 1 px solid # 7 f1d1d ",borderRadius:6,color:" # fca5a5 ",marginBottom:12}}>
{aiErr}
2026-04-06 22:21:36 +01:00
</div>
2026-04-06 22:29:52 +00:00
)}
{!ai && !aiLd && !aiErr && (
<div style={{padding:20,background:C.bg,borderRadius:6,border:" 1 px dashed "+C.bd,textAlign:" center ",color:C.txM,fontSize:12}}>
Click " Generate View " for a live AI deep dive
2026-04-06 22:21:36 +01:00
</div>
2026-04-06 22:29:52 +00:00
)}
{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:" 1 px 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:" 1 fr ",gap:10,marginBottom:14}}>
{ai.macro_sections.map((sec, i) => (
<div key={i} style={{background:C.bg,padding:14,borderRadius:6,border:" 1 px 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:" 1 fr 1 fr ",gap:12}}>
<div style={{background:C.bg,padding:14,borderRadius:6,border:" 1 px 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:" 1 px 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:" 1 fr 1 fr ",gap:12,marginTop:12}}>
<div style={{background:C.bg,padding:14,borderRadius:6,border:" 1 px 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:" 1 px 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>
</>
)}
2026-04-06 22:21:36 +01:00
</div>
2026-04-06 22:29:52 +00:00
<div className=" g2 " style={{display:" grid ",gridTemplateColumns:" 1.1 fr 0.9 fr ",gap:16}}>
<div style={{background:C.bgC,border:" 1 px 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}}>
{[[" 1 D ",fp?.d1],[" 1 W ",fp?.w1],[" 1 M ",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:" 1 px 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:" 4 px 0 ",borderBottom:" 1 px 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:" 1 px 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:" 1 fr 1 fr 1 fr ",gap:16,marginTop:16}}>
<div style={{background:C.bgC,border:" 1 px 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}}>{tpl?.cb!=null ? tpl.cb.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:" 1 px 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:" 1 fr 1 fr 1 fr ",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:" 1 px 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:" 8 px 10 px ",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>
2026-04-06 22:21:36 +01:00
</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 ").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 2 Y "+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:" 1 px 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:" 10 px 24 px ",background:ld?" transparent ":" linear - gradient ( 135 deg , "+C.ac+" , "+C.acD+" ) ",color:ld?C.ac:" # fff ",border:ld?" 1 px 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:" 1 px 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:" 1 px 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:" 5 px 14 px ",fontSize:11,fontWeight:600,borderRadius:4,background:fl===f?C.ac+" 25 ":C.bg,color:fl===f?C.ac:C.txD,border:" 1 px 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.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})}/></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:" 1 px solid "+C.bd,borderRadius:10,padding:24,width:520,maxWidth:" 92 vw "}} 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:" 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=><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>}
2026-04-06 22:29:52 +00:00
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>}
2026-04-06 22:21:36 +01:00
/* ═══ APP ══════════════════════════════════════════════════════════ */
function App (){
2026-04-06 22:29:52 +00:00
const [ sec , setSec ] = useState ( "brief" ); const [ ctry , setCtry ] = useState ( "US" ); const [ sO , setSO ] = useState ( false );
2026-04-06 22:21:36 +01:00
const [ keys , setKeys ] = useState ({ fred : gk ( "fred" ) || _K . fred , 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 ( DCAL ); 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 svK = useCallback ( nk =>{ setKeys ( nk ); Object . entries ( nk ). forEach (([ k , v ])=> sk ( k , v ))},[]);
const load = useCallback ( async ()=>{
sLd ( true );
try { const [ now , d1 , w1 , m1 , ytd ] = await Promise . all ([ fFr ( "latest" , SYM ), fFr ( ago ( 1 ), SYM ), fFr ( ago ( 7 ), SYM ), fFr ( ago ( 30 ), SYM ), fFr ( jan2 (), SYM )]); if ( now ? . rates ){ const bld = defs => defs . map ( d =>{ const s = now . rates [ d . q ]; if ( ! s ) return { pair : d . p , spot : null , d1 : null , w1 : null , m1 : null , ytd : null }; const sp = d . i ? 1 / s : s ; const ch = h =>{ const v = h ? . rates ? .[ d . q ]; if ( ! v ) return null ; return d . i ? pc ( 1 / s , 1 / v ) : pc ( s , v )}; return { pair : d . p , spot : sp , d1 : ch ( d1 ), w1 : ch ( w1 ), m1 : ch ( m1 ), ytd : ch ( ytd )}}); sG10 ( bld ( G10 )); sEM ( bld ( EMP )); sFxL ( true ); const eur = now . rates . EUR , eurY = d1 ? . rates ? . EUR ; if ( eur ) sRisk ( p =>({... p , DXY : { level :+ ( 1 / eur * 104.5 ). toFixed ( 2 ), chg : eurY ?+ pc ( 1 / eur , 1 / eurY ) : null }}))}} catch {}
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 {}}
if ( keys . finnhub ){
2026-04-06 22:29:52 +00:00
const [ vR , gR , wR , bR ] = await Promise . allSettled ([ fFh ( "quote" ,{ symbol : "CBOE:VIX" }, keys . finnhub ), fFh ( "forex/rates" ,{ base : "USD" }, keys . finnhub ), fFh ( "quote" ,{ symbol : "NYMEX:CL1!" }, keys . finnhub ), fFh ( "quote" ,{ symbol : "NYMEX:BZ1!" }, keys . finnhub )]);
const v = vR . status === "fulfilled" ? vR . value : null ; if ( v ? . c ) sRisk ( p =>({... p , VIX : { level : v . c , chg :+ ( v . c - v . pc ). toFixed ( 2 )}}));
const g = gR . status === "fulfilled" ? gR . value : null ; if ( g ? . quote ? . XAU ){ const gp = 1 / g . quote . XAU ; sCmd ( p =>({... p , gold : { level : gp , chg : null }})); sRisk ( p =>({... p , Gold : { level : gp , chg : null }}));}
const w = wR . status === "fulfilled" ? wR . value : null ; if ( w ? . c ){ sCmd ( p =>({... p , wti : { level : w . c , chg : w . pc ?+ pc ( w . c , w . pc ) : null }})); sRisk ( p =>({... p , WTI : { level : w . c , chg : w . pc ?+ pc ( w . c , w . pc ) : null }}));}
const b = bR . status === "fulfilled" ? bR . value : null ; if ( b ? . c ){ sCmd ( p =>({... p , brent : { level : b . c , chg : b . pc ?+ pc ( b . c , b . pc ) : null }})); sRisk ( p =>({... p , Brent : { level : b . c , chg : b . pc ?+ pc ( b . c , b . pc ) : null }}));}
2026-04-06 22:21:36 +01:00
sRL ( true )}
sLd ( false ); sLR ( new Date ());
},[ keys ]);
useEffect (()=>{ load ()},[ load ]);
2026-04-06 22:29:52 +00:00
useEffect (()=>{ if (( sec === "deep" || sec === "brief" ) && keys . finnhub &&! nL ){( async ()=>{ try { const nd = await fFh ( "news" ,{ category : "forex" }, keys . finnhub ); if ( Array . isArray ( nd )){ sNews ( nd . slice ( 0 , 30 )); sNL ( true )}} catch {}})()}},[ sec , keys . finnhub , nL ]);
useEffect (()=>{ if ( sec === "cal" && keys . finnhub &&! cL ){( async ()=>{ try { const cd = await fFh ( "calendar/economic" ,{ from : ago ( 0 ), to : fwd ( 7 )}, keys . finnhub ); if ( cd ? . economicCalendar ? . length ){ const fl = { "US" : "🇺🇸" , "EU" : "🇪🇺" , "GB" : "🇬🇧" , "JP" : "🇯🇵" , "CH" : "🇨🇭" , "AU" : "🇦🇺" , "NZ" : "🇳🇿" , "CA" : "🇨🇦" , "SE" : "🇸🇪" , "NO" : "🇳🇴" , "BR" : "🇧🇷" , "MX" : "🇲🇽" , "ZA" : "🇿🇦" , "IN" : "🇮🇳" , "KR" : "🇰🇷" , "TR" : "🇹🇷" , "PL" : "🇵🇱" , "CN" : "🇨🇳" , "DE" : "🇩🇪" }; sCal ( cd . economicCalendar . slice ( 0 , 30 ). map ( e =>({ date : e . time ? . slice ( 0 , 10 ) || "" , time : e . time ? . slice ( 11 , 16 ) || "" , ctry : fl [ e . country ] || e . country || "" , ev : e . event || "" , prev : e . prev != null ? String ( e . prev ) : "—" , fcast : e . estimate != null ? String ( e . estimate ) : "—" , act : e . actual != null ? String ( e . actual ) : "—" , imp : e . impact === "high" ? "red" : "orange" }))); sCL ( true )}} catch {}})()}},[ sec , keys . finnhub , cL ]);
2026-04-06 22:21:36 +01:00
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:"markets",l:"📊 Markets"},{id:"commod",l:"🛢 Commodities"},{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>
2026-04-06 22:29:52 +00:00
< 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>
2026-04-06 22:21:36 +01:00
< /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 === "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 } />< /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 === "corr" &&< div style = {{ background : C . bgC , border : "1px solid " + C . bd , borderRadius : 8 , padding : 20 }} >< CorrMx />< /div>}
2026-04-06 22:29:52 +00:00
{ 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}/></div>}
2026-04-06 22:21:36 +01:00
{ 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 >