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:
+108
-9
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user