} function ema(values, period) { const k = 2 / (period + 1); const out = Array(values.length).fill(null); let prev; for (let i = 0; i < values.length; i++) { const v = values[i]; prev = (prev === undefined) ? v : (v - prev) * k + prev; out[i] = prev; } return out; } async function load() { const ticker = tickerEl.value.trim().toUpperCase() || 'AAPL'; let timespan = tfEl.value; let multiplier = 1; if (timespan === '5' || timespan === '15') { multiplier = parseInt(timespan, 10); timespan = 'minute'; } const params = new URLSearchParams({ ticker, multiplier: String(multiplier), timespan, from: fromEl.value, to: toEl.value, limit: '5000', }); const barsResp = await fetch(`${WORKER_URL}/api/bars?${params.toString()}`); const barsJson = await barsResp.json(); const results = Array.isArray(barsJson.results) ? barsJson.results : []; const cdata = results.map(b => ({ time: Math.floor(b.t / 1000), open: b.o, high: b.h, low: b.l, close: b.c, })); candleSeries.setData(cdata); // Volume const vdata = results.map(b => ({ time: Math.floor(b.t / 1000), value: b.v })); volSeries.setData(vdata); // Indicators const closes = results.map(b => b.c); const sma20 = sma(closes, 20).map((v, i) => v ? { time: Math.floor(results[i].t / 1000), value: v } : null).filter(Boolean); const ema50 = ema(closes, 50).map((v, i) => v ? { time: Math.floor(results[i].t / 1000), value: v } : null).filter(Boolean); sma20Series.setData(sma20); ema50Series.setData(ema50); sma20Series.applyOptions({ visible: sma20El.checked }); ema50Series.applyOptions({ visible: ema50El.checked }); // Quote snapshot const snapResp = await fetch(`${WORKER_URL}/api/snapshot?ticker=${ticker}`); const snap = await snapResp.json(); if (snap && snap.ticker) { const q = snap.ticker; const last = q.lastTrade?.p ?? '—'; const bid = q.lastQuote?.p ?? '—'; const ask = q.lastQuote?.P ?? '—'; quoteEl.innerHTML = `
${ticker}
Last: ${last}
Bid: ${bid} / Ask: ${ask}
`; } else { quoteEl.textContent = '—'; } } loadBtn.addEventListener('click', load); sma20El.addEventListener('change', () => sma20Series.applyOptions({ visible: sma20El.checked })); ema50El.addEventListener('change', () => ema50Series.applyOptions({ visible: ema50El.checked })); // Initial load load(); // Handle resize const fit = () => { chart.timeScale().fitContent(); subChart.timeScale().fitContent(); }; window.addEventListener('resize', fit);