Replace K-line SMA with configurable EMA periods.

Default to EMA 21/55 with editable period inputs and localStorage persistence on the market chart.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-29 23:17:18 +08:00
parent 2f5b5c4aae
commit c6c6c3fe83
2 changed files with 124 additions and 12 deletions
+108 -9
View File
@@ -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++) {
var close = bars[i].close;
if (ema == null) {
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) });
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');
+14 -1
View File
@@ -31,7 +31,12 @@
<div class="market-chart-toolbar">
<div class="market-chart-options">
<label class="chart-opt"><input type="checkbox" id="chart-opt-prev-close">昨收线</label>
<label class="chart-opt"><input type="checkbox" id="chart-opt-ma">均线 21/55</label>
<label class="chart-opt"><input type="checkbox" id="chart-opt-ma">EMA</label>
<span class="chart-ema-periods" id="chart-ema-periods">
<input type="number" class="chart-ema-input" id="chart-ema-fast" value="21" min="2" max="500" step="1" title="快线周期" aria-label="EMA快线周期">
<span class="chart-ema-sep">/</span>
<input type="number" class="chart-ema-input" id="chart-ema-slow" value="55" min="2" max="500" step="1" title="慢线周期" aria-label="EMA慢线周期">
</span>
<label class="chart-opt"><input type="checkbox" id="chart-opt-gap-day">间隔日</label>
</div>
<div class="market-chart-zoom">
@@ -97,6 +102,14 @@
color:var(--text-muted);cursor:pointer;user-select:none;
}
.chart-opt input{width:auto;margin:0;cursor:pointer}
.chart-ema-periods{display:flex;align-items:center;gap:.25rem;font-size:.78rem;color:var(--text-muted)}
.chart-ema-input{
width:3.1rem;padding:.28rem .35rem;border-radius:6px;
border:1px solid var(--input-border);background:var(--toggle-bg);
color:var(--text-primary);font-size:.78rem;font-variant-numeric:tabular-nums;
}
.chart-ema-input:disabled{opacity:.45;cursor:not-allowed}
.chart-ema-sep{opacity:.6}
.market-chart-zoom{display:flex;gap:.35rem;align-items:center}
.chart-zoom-btn{
width:32px;height:32px;padding:0;border-radius:8px;