Files
Chan/web/static/js/indicators.js
T
jackyu66gitandCursor 9cf625c413 fix(web): 小周期切换时对齐标记,避免 LWC Value is null
主周期笔/KLC 分型标记在切到 1m/2m 主图时未对齐 K 线 time;过滤均线无效点并钳制视窗恢复。顺带统一 BI 中枢计算路径。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-08 17:06:31 +08:00

103 lines
3.7 KiB
JavaScript

// Namespace setup
window.App = window.App || {};
window.App.Indicators = (function() {
function computeEMA(arr, period) {
const k = 2 / (period + 1);
const out = [];
let emaPrev = null;
for (let i = 0; i < arr.length; i++) {
const price = arr[i];
if (price == null || !isFinite(price)) { out.push(null); continue; }
if (emaPrev == null) {
const start = Math.max(0, i - period + 1);
const windowArr = arr.slice(start, i + 1).filter(v => v != null && isFinite(v));
const sma = windowArr.length ? windowArr.reduce((a,b)=>a+b,0)/windowArr.length : price;
emaPrev = sma;
}
const ema = price * k + emaPrev * (1 - k);
out.push(ema);
emaPrev = ema;
}
return out;
}
function calculateMA(data, type, length, source) {
if (!data || data.length < length) return [];
const sourceData = data.map(candle => {
switch(source) {
case 'open': return candle.open;
case 'high': return candle.high;
case 'low': return candle.low;
case 'close': return candle.close;
case 'hl2': return (candle.high + candle.low) / 2;
case 'hlc3': return (candle.high + candle.low + candle.close) / 3;
case 'ohlc4': return (candle.open + candle.high + candle.low + candle.close) / 4;
default: return candle.close;
}
});
const result = [];
for (let i = length - 1; i < sourceData.length; i++) {
let value;
switch(type) {
case 'SMA':
value = sourceData.slice(i - length + 1, i + 1).reduce((sum, v) => sum + v, 0) / length;
break;
case 'EMA':
const multiplier = 2 / (length + 1);
if (result.length === 0) {
value = sourceData.slice(i - length + 1, i + 1).reduce((sum, v) => sum + v, 0) / length;
} else {
value = sourceData[i] * multiplier + result[result.length - 1].value * (1 - multiplier);
}
break;
case 'WMA':
let weightSum = 0;
let valueSum = 0;
for (let j = 0; j < length; j++) {
const weight = j + 1;
weightSum += weight;
valueSum += sourceData[i - length + 1 + j] * weight;
}
value = valueSum / weightSum;
break;
default:
value = sourceData[i];
}
if (value == null || !isFinite(value) || data[i].time == null || !isFinite(Number(data[i].time))) {
continue;
}
result.push({ time: Math.floor(Number(data[i].time)), value: Number(value) });
}
return result;
}
function calculateBB(data, length, upperMultiplier, lowerMultiplier, source) {
if (!data || data.length < length) return [];
const sourceData = data.map(candle => {
switch(source) {
case 'open': return candle.open;
case 'high': return candle.high;
case 'low': return candle.low;
case 'close': return candle.close;
case 'hl2': return (candle.high + candle.low) / 2;
case 'hlc3': return (candle.high + candle.low + candle.close) / 3;
case 'ohlc4': return (candle.open + candle.high + candle.low + candle.close) / 4;
default: return candle.close;
}
});
const result = [];
for (let i = length - 1; i < sourceData.length; i++) {
const start = Math.max(0, i - length + 1);
const slice = sourceData.slice(start, i + 1);
const avg = slice.reduce((sum, v) => sum + v, 0) / length;
const std = Math.sqrt(slice.reduce((sum, v) => sum + Math.pow(v - avg, 2), 0) / length);
result.push({ time: data[i].time, upper: avg + upperMultiplier * std, middle: avg, lower: avg - lowerMultiplier * std });
}
return result;
}
return { computeEMA, calculateMA, calculateBB };
})();