export default { async fetch(request, env, ctx) { const url = new URL(request.url); if (url.pathname === '/stream') return streamPrices(request, env, ctx); if (url.pathname === '/api/fred') return fredMacro(env); if (url.pathname === '/api/news') return newsFinnhub(env); return new Response(getIndexHTML(), { headers: { 'content-type': 'text/html; charset=utf-8' } }); } }; /** SSE stream vers le navigateur; côté serveur on consomme le stream OANDA et on re‑émet. */ async function streamPrices(request, env, ctx){ const account = env.OANDA_ACCOUNT; const token = env.OANDA_TOKEN; const isPractice = (env.OANDA_ENV||'practice').toLowerCase()==='practice'; if(!account || !token){ return json({ error: 'Manque OANDA_ACCOUNT ou OANDA_TOKEN' }, 500); } const host = isPractice ? 'stream-fxpractice.oanda.com' : 'stream-fxtrade.oanda.com'; const endpoint = `https://${host}/v3/accounts/${account}/pricing/stream?instruments=XAU_USD`; const encoder = new TextEncoder(); const { readable, writable } = new TransformStream(); const writer = writable.getWriter(); // Headers SSE const headers = { 'content-type': 'text/event-stream; charset=utf-8', 'cache-control': 'no-cache, no-transform', 'connection': 'keep-alive', 'access-control-allow-origin': '*' }; // Envoi ping toutes 25s pour garder la connexion ouverte côté client const ping = () => writer.write(encoder.encode(`event: ping\ndata: {}\n\n`)); const pingId = setInterval(ping, 25000); ctx.waitUntil((async()=>{ while(true){ await new Promise(r=>setTimeout(r, 60000)); } })()); // Connecte au stream OANDA (async () => { try{ const oandaRes = await fetch(endpoint, { headers: { 'Authorization': `Bearer ${token}` } }); if(!oandaRes.ok) { await writer.write(encoder.encode(`event: error\ndata: ${JSON.stringify({status:oandaRes.status})}\n\n`)); clearInterval(pingId); writer.close(); return; } const reader = oandaRes.body.getReader(); let buffer = ''; while(true){ const {done, value} = await reader.read(); if(done) break; buffer += new TextDecoder().decode(value); // OANDA envoie des lignes JSON séparées par \n let idx; while((idx = buffer.indexOf('\n')) >= 0){ const line = buffer.slice(0, idx).trim(); buffer = buffer.slice(idx+1); if(!line) continue; try{ const msg = JSON.parse(line); if(msg.type === 'PRICE'){ const bid = Number(msg.bids?.[0]?.price); const ask = Number(msg.asks?.[0]?.price); const mid = (bid && ask) ? (bid+ask)/2 : null; const out = { t: msg.time, bid, ask, mid }; await writer.write(encoder.encode(`data: ${JSON.stringify(out)}\n\n`)); } }catch(e){ /* ignore parse errors */ } } } }catch(e){ await writer.write(encoder.encode(`event: error\ndata: ${JSON.stringify({error:String(e)})}\n\n`)); } finally { clearInterval(pingId); writer.close(); } })(); return new Response(readable, { headers }); } /** Proxy simple: dernières obs FRED pour DXY proxy (DTWEXM), DGS10, T10YIE */ async function fredMacro(env){ if(!env.FRED_KEY) return json({ error:'Missing FRED_KEY' }, 500); const base = 'https://api.stlouisfed.org/fred/series/observations'; const last2 = async(series)=>{ const url = `${base}?series_id=${series}&api_key=${env.FRED_KEY}&file_type=json&sort_order=desc&limit=2`; const r = await fetch(url); const j = await r.json(); const obs = (j.observations||[]).filter(o=>o.value!=='.').slice(0,2).map(o=>({date:o.date, v: parseFloat(o.value)})); return { last: obs[0]||null, prev: obs[1]||null }; }; const [dxy, dgs10, t10yie] = await Promise.all([ last2('DTWEXM'), last2('DGS10'), last2('T10YIE') ]); return json({ dxy, dgs10, t10yie }); } /** News Finnhub filtrées "gold" */ async function newsFinnhub(env){ if(!env.FINNHUB_KEY) return json({items:[]}); const r = await fetch(`https://finnhub.io/api/v1/news?category=general&token=${env.FINNHUB_KEY}`); const arr = await r.json(); const KW = ['gold','xau','bullion','precious','comex','silver']; const items = (arr||[]).filter(n=>{ const t = `${n.headline} ${n.summary}`.toLowerCase(); return KW.some(k=>t.includes(k)); }).slice(0,12).map(n=>({headline:n.headline,url:n.url,summary:n.summary,source:n.source,datetime:n.datetime})); return json({ items }); } function json(obj, status=200){ return new Response(JSON.stringify(obj), { status, headers: { 'content-type':'application/json', 'access-control-allow-origin':'*' }}); } /** UI minimaliste avec Chart.js + SSE */ function getIndexHTML(){ return ` Speecy Gold — Temps réel
💛
Speecy Gold – Temps réel
Dernière tick:
XAU/USD (OANDA stream)
Flux practice en temps réel (jusqu'à ~4 updates/s).
Indicateurs clés
DXY*
US10Y
10Y Réel
*DTWEXM comme proxy.
Biais (heuristique)

Intraday – ticks (mid)

News “gold”

source: Finnhub
`; }