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:
+110
-11
@@ -26,6 +26,8 @@
|
|||||||
var followingLatest = true;
|
var followingLatest = true;
|
||||||
var autoFollow = true;
|
var autoFollow = true;
|
||||||
var DEFAULT_VISIBLE_BARS = 80;
|
var DEFAULT_VISIBLE_BARS = 80;
|
||||||
|
var EMA_STORAGE_KEY = 'qihuo-market-ema-periods';
|
||||||
|
var DEFAULT_EMA = { fast: 21, slow: 55 };
|
||||||
|
|
||||||
var PERIOD_SECONDS = {
|
var PERIOD_SECONDS = {
|
||||||
timeshare: 60,
|
timeshare: 60,
|
||||||
@@ -138,13 +140,75 @@
|
|||||||
return out;
|
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 = [];
|
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++) {
|
for (var i = 0; i < bars.length; i++) {
|
||||||
if (i < period - 1) continue;
|
var close = bars[i].close;
|
||||||
var sum = 0;
|
if (ema == null) {
|
||||||
for (var j = 0; j < period; j++) sum += bars[i - j].close;
|
if (i < period - 1) continue;
|
||||||
result.push({ time: bars[i].time, value: +(sum / period).toFixed(4) });
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -340,10 +404,11 @@
|
|||||||
color: up ? c.up : c.down,
|
color: up ? c.up : c.down,
|
||||||
});
|
});
|
||||||
if (chartOpts.ma && ma21Series && ma55Series && prepared && prepared.length) {
|
if (chartOpts.ma && ma21Series && ma55Series && prepared && prepared.length) {
|
||||||
var ma21 = calcMA(21, prepared);
|
var emaP = getEmaPeriods();
|
||||||
var ma55 = calcMA(55, prepared);
|
var emaFast = calcEMA(emaP.fast, prepared);
|
||||||
if (ma21.length) ma21Series.update(ma21[ma21.length - 1]);
|
var emaSlow = calcEMA(emaP.slow, prepared);
|
||||||
if (ma55.length) ma55Series.update(ma55[ma55.length - 1]);
|
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) {
|
if (chartOpts.ma && ma21Series && ma55Series) {
|
||||||
ma21Series.setData(calcMA(21, prepared));
|
var emaP = getEmaPeriods();
|
||||||
ma55Series.setData(calcMA(55, prepared));
|
ma21Series.setData(calcEMA(emaP.fast, prepared));
|
||||||
|
ma55Series.setData(calcEMA(emaP.slow, prepared));
|
||||||
}
|
}
|
||||||
applyPrevCloseLine(lastPrevClose != null ? lastPrevClose : data.prev_close);
|
applyPrevCloseLine(lastPrevClose != null ? lastPrevClose : data.prev_close);
|
||||||
}
|
}
|
||||||
@@ -663,6 +729,17 @@
|
|||||||
var prevCb = document.getElementById('chart-opt-prev-close');
|
var prevCb = document.getElementById('chart-opt-prev-close');
|
||||||
var maCb = document.getElementById('chart-opt-ma');
|
var maCb = document.getElementById('chart-opt-ma');
|
||||||
var gapCb = document.getElementById('chart-opt-gap-day');
|
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) {
|
if (prevCb) {
|
||||||
prevCb.addEventListener('change', function () {
|
prevCb.addEventListener('change', function () {
|
||||||
chartOpts.prevClose = prevCb.checked;
|
chartOpts.prevClose = prevCb.checked;
|
||||||
@@ -674,12 +751,33 @@
|
|||||||
if (maCb) {
|
if (maCb) {
|
||||||
maCb.addEventListener('change', function () {
|
maCb.addEventListener('change', function () {
|
||||||
chartOpts.ma = maCb.checked;
|
chartOpts.ma = maCb.checked;
|
||||||
|
syncEmaInputsEnabled();
|
||||||
if (lastData) {
|
if (lastData) {
|
||||||
destroyChart();
|
destroyChart();
|
||||||
renderChart(lastData, { forceFull: true });
|
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) {
|
if (gapCb) {
|
||||||
gapCb.addEventListener('change', function () {
|
gapCb.addEventListener('change', function () {
|
||||||
chartOpts.gapDay = gapCb.checked;
|
chartOpts.gapDay = gapCb.checked;
|
||||||
@@ -702,6 +800,7 @@
|
|||||||
bindPeriodTabs();
|
bindPeriodTabs();
|
||||||
bindZoomButtons();
|
bindZoomButtons();
|
||||||
bindAutoButton();
|
bindAutoButton();
|
||||||
|
initEmaPeriodInputs();
|
||||||
bindChartOptions();
|
bindChartOptions();
|
||||||
|
|
||||||
var active = document.querySelector('.period-tab.active');
|
var active = document.querySelector('.period-tab.active');
|
||||||
|
|||||||
+14
-1
@@ -31,7 +31,12 @@
|
|||||||
<div class="market-chart-toolbar">
|
<div class="market-chart-toolbar">
|
||||||
<div class="market-chart-options">
|
<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-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>
|
<label class="chart-opt"><input type="checkbox" id="chart-opt-gap-day">间隔日</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="market-chart-zoom">
|
<div class="market-chart-zoom">
|
||||||
@@ -97,6 +102,14 @@
|
|||||||
color:var(--text-muted);cursor:pointer;user-select:none;
|
color:var(--text-muted);cursor:pointer;user-select:none;
|
||||||
}
|
}
|
||||||
.chart-opt input{width:auto;margin:0;cursor:pointer}
|
.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}
|
.market-chart-zoom{display:flex;gap:.35rem;align-items:center}
|
||||||
.chart-zoom-btn{
|
.chart-zoom-btn{
|
||||||
width:32px;height:32px;padding:0;border-radius:8px;
|
width:32px;height:32px;padding:0;border-radius:8px;
|
||||||
|
|||||||
Reference in New Issue
Block a user