diff --git a/static/js/market.js b/static/js/market.js index e8c95e4..d524129 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -26,6 +26,8 @@ var followingLatest = true; var autoFollow = true; var DEFAULT_VISIBLE_BARS = 80; + var EMA_STORAGE_KEY = 'qihuo-market-ema-periods'; + var DEFAULT_EMA = { fast: 21, slow: 55 }; var PERIOD_SECONDS = { timeshare: 60, @@ -138,13 +140,75 @@ return out; } - function calcMA(period, bars) { + function clampEmaPeriod(v, fallback) { + var n = parseInt(v, 10); + if (isNaN(n)) return fallback; + return Math.max(2, Math.min(500, n)); + } + + function loadEmaPeriodsFromStorage() { + try { + var raw = localStorage.getItem(EMA_STORAGE_KEY); + if (raw) { + var o = JSON.parse(raw); + return { + fast: clampEmaPeriod(o.fast, DEFAULT_EMA.fast), + slow: clampEmaPeriod(o.slow, DEFAULT_EMA.slow), + }; + } + } catch (e) { /* ignore */ } + return { fast: DEFAULT_EMA.fast, slow: DEFAULT_EMA.slow }; + } + + function saveEmaPeriods(fast, slow) { + try { + localStorage.setItem(EMA_STORAGE_KEY, JSON.stringify({ fast: fast, slow: slow })); + } catch (e) { /* ignore */ } + } + + function getEmaPeriods() { + var fastEl = document.getElementById('chart-ema-fast'); + var slowEl = document.getElementById('chart-ema-slow'); + var fast = clampEmaPeriod(fastEl && fastEl.value, DEFAULT_EMA.fast); + var slow = clampEmaPeriod(slowEl && slowEl.value, DEFAULT_EMA.slow); + if (fastEl) fastEl.value = String(fast); + if (slowEl) slowEl.value = String(slow); + return { fast: fast, slow: slow }; + } + + function syncEmaInputsEnabled() { + var fastEl = document.getElementById('chart-ema-fast'); + var slowEl = document.getElementById('chart-ema-slow'); + var on = !!chartOpts.ma; + if (fastEl) fastEl.disabled = !on; + if (slowEl) slowEl.disabled = !on; + } + + function initEmaPeriodInputs() { + var stored = loadEmaPeriodsFromStorage(); + var fastEl = document.getElementById('chart-ema-fast'); + var slowEl = document.getElementById('chart-ema-slow'); + if (fastEl) fastEl.value = String(stored.fast); + if (slowEl) slowEl.value = String(stored.slow); + syncEmaInputsEnabled(); + } + + function calcEMA(period, bars) { var result = []; + if (!bars.length || period < 1) return result; + var k = 2 / (period + 1); + var ema = null; for (var i = 0; i < bars.length; i++) { - if (i < period - 1) continue; - var sum = 0; - for (var j = 0; j < period; j++) sum += bars[i - j].close; - result.push({ time: bars[i].time, value: +(sum / period).toFixed(4) }); + var close = bars[i].close; + if (ema == null) { + if (i < period - 1) continue; + var sum = 0; + for (var j = i - period + 1; j <= i; j++) sum += bars[j].close; + ema = sum / period; + } else { + ema = close * k + ema * (1 - k); + } + result.push({ time: bars[i].time, value: +ema.toFixed(4) }); } return result; } @@ -340,10 +404,11 @@ color: up ? c.up : c.down, }); if (chartOpts.ma && ma21Series && ma55Series && prepared && prepared.length) { - var ma21 = calcMA(21, prepared); - var ma55 = calcMA(55, prepared); - if (ma21.length) ma21Series.update(ma21[ma21.length - 1]); - if (ma55.length) ma55Series.update(ma55[ma55.length - 1]); + var emaP = getEmaPeriods(); + var emaFast = calcEMA(emaP.fast, prepared); + var emaSlow = calcEMA(emaP.slow, prepared); + if (emaFast.length) ma21Series.update(emaFast[emaFast.length - 1]); + if (emaSlow.length) ma55Series.update(emaSlow[emaSlow.length - 1]); } } @@ -386,8 +451,9 @@ }; })); if (chartOpts.ma && ma21Series && ma55Series) { - ma21Series.setData(calcMA(21, prepared)); - ma55Series.setData(calcMA(55, prepared)); + var emaP = getEmaPeriods(); + ma21Series.setData(calcEMA(emaP.fast, prepared)); + ma55Series.setData(calcEMA(emaP.slow, prepared)); } applyPrevCloseLine(lastPrevClose != null ? lastPrevClose : data.prev_close); } @@ -663,6 +729,17 @@ var prevCb = document.getElementById('chart-opt-prev-close'); var maCb = document.getElementById('chart-opt-ma'); var gapCb = document.getElementById('chart-opt-gap-day'); + var emaFastEl = document.getElementById('chart-ema-fast'); + var emaSlowEl = document.getElementById('chart-ema-slow'); + + function onEmaPeriodChange() { + var emaP = getEmaPeriods(); + saveEmaPeriods(emaP.fast, emaP.slow); + if (chartOpts.ma && lastData) { + renderChart(lastData, { forceFull: true }); + } + } + if (prevCb) { prevCb.addEventListener('change', function () { chartOpts.prevClose = prevCb.checked; @@ -674,12 +751,33 @@ if (maCb) { maCb.addEventListener('change', function () { chartOpts.ma = maCb.checked; + syncEmaInputsEnabled(); if (lastData) { destroyChart(); renderChart(lastData, { forceFull: true }); } }); } + if (emaFastEl) { + emaFastEl.addEventListener('change', onEmaPeriodChange); + emaFastEl.addEventListener('keydown', function (ev) { + if (ev.key === 'Enter') { + ev.preventDefault(); + emaFastEl.blur(); + onEmaPeriodChange(); + } + }); + } + if (emaSlowEl) { + emaSlowEl.addEventListener('change', onEmaPeriodChange); + emaSlowEl.addEventListener('keydown', function (ev) { + if (ev.key === 'Enter') { + ev.preventDefault(); + emaSlowEl.blur(); + onEmaPeriodChange(); + } + }); + } if (gapCb) { gapCb.addEventListener('change', function () { chartOpts.gapDay = gapCb.checked; @@ -702,6 +800,7 @@ bindPeriodTabs(); bindZoomButtons(); bindAutoButton(); + initEmaPeriodInputs(); bindChartOptions(); var active = document.querySelector('.period-tab.active'); diff --git a/templates/market.html b/templates/market.html index 59abf9e..45d3ab7 100644 --- a/templates/market.html +++ b/templates/market.html @@ -31,7 +31,12 @@