You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
buffer_platform/app/static/futures_analysis.js

826 lines
29 KiB

const API_BASE = '/api/v1/futures';
let klineChart = null;
let currentSymbol = null;
let currentPeriod = '15';
let allFuturesData = [];
document.addEventListener('DOMContentLoaded', function() {
updateTime();
setInterval(updateTime, 1000);
initEventListeners();
loadFuturesList();
});
function updateTime() {
const now = new Date();
const timeStr = now.getFullYear() + '/' +
String(now.getMonth() + 1).padStart(2, '0') + '/' +
String(now.getDate()).padStart(2, '0') + ' ' +
String(now.getHours()).padStart(2, '0') + ':' +
String(now.getMinutes()).padStart(2, '0') + ':' +
String(now.getSeconds()).padStart(2, '0');
document.getElementById('current-time').textContent = timeStr;
}
function initEventListeners() {
document.getElementById('back-btn').addEventListener('click', showListView);
document.querySelectorAll('.period-btn').forEach(btn => {
btn.addEventListener('click', function() {
document.querySelectorAll('.period-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
currentPeriod = this.dataset.period;
loadKlineData(currentSymbol, currentPeriod);
});
});
document.getElementById('search-input').addEventListener('input', function() {
filterFuturesList(this.value);
});
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', function() {
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
filterByCategory(this.dataset.category);
});
});
document.getElementById('sort-select').addEventListener('change', function() {
sortFuturesList(this.value);
});
}
function showListView() {
document.getElementById('list-view').classList.add('active');
document.getElementById('detail-view').classList.remove('active');
if (klineChart) {
klineChart.dispose();
klineChart = null;
}
}
function showDetailView(symbol) {
currentSymbol = symbol;
document.getElementById('list-view').classList.remove('active');
document.getElementById('detail-view').classList.add('active');
loadFuturesDetail(symbol);
loadKlineData(symbol, currentPeriod);
}
async function loadFuturesList() {
try {
const response = await fetch(`${API_BASE}/list`);
const data = await response.json();
if (data.success) {
allFuturesData = data.data;
renderFuturesGrid(allFuturesData);
updateStats(allFuturesData);
}
} catch (error) {
console.error('加载品种列表失败:', error);
loadFuturesFromConfig();
}
}
async function loadFuturesFromConfig() {
try {
const response = await fetch('/api/v1/config');
const config = await response.json();
const futuresConfig = config.futures || {};
allFuturesData = Object.entries(futuresConfig).map(([name, symbol]) => ({
symbol: symbol,
name: name,
price: 0,
change: 0,
changePct: 0,
suggestion: '等待数据',
suggestionType: 'neutral',
periods: { '5': 'neutral', '15': 'neutral', '30': 'neutral', '60': 'neutral' },
successRate: 0,
trendScore: 0,
resistance: 0,
support: 0,
open: 0,
high: 0,
low: 0,
volume: 0
}));
renderFuturesGrid(allFuturesData);
updateStats(allFuturesData);
} catch (error) {
console.error('加载配置失败:', error);
const mockData = generateMockFuturesData();
allFuturesData = mockData;
renderFuturesGrid(mockData);
updateStats(mockData);
}
}
function generateMockFuturesData() {
return [
{
symbol: 'EC',
name: '集运指数',
price: 2150,
change: 196,
changePct: 10.06,
suggestion: '逢低做多',
suggestionType: 'up',
periods: { '5': 'up', '15': 'up', '30': 'up', '60': 'up' },
successRate: 80,
trendScore: 90,
resistance: 2200,
support: 2000,
open: 1960,
high: 2200,
low: 1940,
volume: 45600
},
{
symbol: 'AU',
name: '黄金',
price: 685.2,
change: 12.45,
changePct: 1.85,
suggestion: '逢低做多',
suggestionType: 'up',
periods: { '5': 'up', '15': 'up', '30': 'up', '60': 'up' },
successRate: 78,
trendScore: 92,
resistance: 692,
support: 678,
open: 672.75,
high: 688,
low: 670,
volume: 128000
},
{
symbol: 'AG',
name: '白银',
price: 8250,
change: 165,
changePct: 2.04,
suggestion: '逢低做多',
suggestionType: 'up',
periods: { '5': 'up', '15': 'up', '30': 'up', '60': 'up' },
successRate: 75,
trendScore: 88,
resistance: 8350,
support: 8100,
open: 8085,
high: 8280,
low: 8050,
volume: 95000
},
{
symbol: 'SC',
name: '原油',
price: 528.6,
change: 12.1,
changePct: 2.35,
suggestion: '逢低做多',
suggestionType: 'up',
periods: { '5': 'up', '15': 'up', '30': 'up', '60': 'neutral' },
successRate: 72,
trendScore: 85,
resistance: 535,
support: 518,
open: 516.5,
high: 530,
low: 515,
volume: 78000
},
{
symbol: 'I',
name: '铁矿石',
price: 785.5,
change: 28,
changePct: 3.7,
suggestion: '逢低做多',
suggestionType: 'up',
periods: { '5': 'up', '15': 'up', '30': 'up', '60': 'up' },
successRate: 68,
trendScore: 82,
resistance: 792,
support: 770,
open: 757.5,
high: 788,
low: 755,
volume: 156000
},
{
symbol: 'CU',
name: '铜',
price: 80610,
change: 112,
changePct: 0.14,
suggestion: '观望等待',
suggestionType: 'neutral',
periods: { '5': 'neutral', '15': 'up', '30': 'neutral', '60': 'up' },
successRate: 58,
trendScore: 65,
resistance: 81200,
support: 79800,
open: 80498,
high: 80850,
low: 80200,
volume: 42000
},
{
symbol: 'P',
name: '棕榈油',
price: 8750,
change: 0,
changePct: 0,
suggestion: '观望等待',
suggestionType: 'neutral',
periods: { '5': 'neutral', '15': 'neutral', '30': 'neutral', '60': 'neutral' },
successRate: 52,
trendScore: 50,
resistance: 8850,
support: 8650,
open: 8750,
high: 8780,
low: 8720,
volume: 65000
},
{
symbol: 'M',
name: '豆粕',
price: 2985,
change: -51,
changePct: -1.68,
suggestion: '逢高做空',
suggestionType: 'down',
periods: { '5': 'down', '15': 'down', '30': 'down', '60': 'neutral' },
successRate: 65,
trendScore: 35,
resistance: 3050,
support: 2920,
open: 3036,
high: 3040,
low: 2980,
volume: 185000
}
];
}
function renderFuturesGrid(data) {
const grid = document.getElementById('futures-grid');
grid.innerHTML = data.map(item => `
<div class="futures-card" onclick="showDetailView('${item.symbol}')">
<div class="card-header">
<div class="card-title">
<span class="card-name">${item.name}</span>
<span class="card-code">(${item.symbol})</span>
</div>
<div class="card-price">
<div class="price-value ${item.change >= 0 ? 'up' : 'down'}">¥${formatNumber(item.price)}</div>
<div class="price-change ${item.change >= 0 ? 'up' : 'down'}">
<i class="fas fa-arrow-${item.change >= 0 ? 'up' : 'down'}"></i>
+${formatNumber(item.change)} (+${item.changePct.toFixed(2)}%)
</div>
</div>
</div>
<span class="suggestion-badge ${item.suggestionType}">${item.suggestion}</span>
<div class="card-section">
<div class="section-label"><i class="far fa-clock"></i> </div>
<div class="period-tags">
<span class="period-tag ${item.periods['5']}"><i class="fas fa-arrow-${getArrow(item.periods['5'])}"></i> 5</span>
<span class="period-tag ${item.periods['15']}"><i class="fas fa-arrow-${getArrow(item.periods['15'])}"></i> 15</span>
<span class="period-tag ${item.periods['30']}"><i class="fas fa-arrow-${getArrow(item.periods['30'])}"></i> 30</span>
<span class="period-tag ${item.periods['60']}"><i class="fas fa-arrow-${getArrow(item.periods['60'])}"></i> 60</span>
</div>
</div>
<div class="card-section">
<div class="section-label"><i class="fas fa-chart-bar"></i> </div>
<div class="progress-bar">
<div class="progress-fill ${item.successRate >= 70 ? 'up' : item.successRate >= 60 ? 'orange' : 'down'}" style="width: ${item.successRate}%"></div>
</div>
<div class="progress-info">
<span class="progress-label"></span>
<span class="progress-value ${item.successRate >= 70 ? 'up' : item.successRate >= 60 ? '' : 'down'}">${item.successRate}%</span>
</div>
</div>
<div class="card-section">
<div class="section-label">趋势评分</div>
<div class="progress-bar">
<div class="progress-fill ${item.trendScore >= 70 ? 'up' : item.trendScore >= 50 ? 'orange' : 'down'}" style="width: ${item.trendScore}%"></div>
</div>
<div class="progress-info">
<span class="progress-label"></span>
<span class="progress-value ${item.trendScore >= 70 ? 'up' : item.trendScore >= 50 ? '' : 'down'}">${item.trendScore}/100</span>
</div>
</div>
<div class="card-section">
<div class="section-label"><i class="fas fa-crosshairs"></i> </div>
<div class="key-levels-row">
<span class="level-label">压力: <span class="level-value down">${formatNumber(item.resistance)}</span></span>
<span class="level-label">支撑: <span class="level-value up">${formatNumber(item.support)}</span></span>
</div>
</div>
<div class="card-footer">
<span class="detail-link">查看详情 <i class="fas fa-chevron-right"></i></span>
</div>
</div>
`).join('');
}
function getArrow(type) {
if (type === 'up') return 'up';
if (type === 'down') return 'down';
return 'right';
}
function formatNumber(num) {
return num.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 });
}
function updateStats(data) {
document.getElementById('total-count').textContent = data.length;
const upCount = data.filter(d => d.change >= 0).length;
const downCount = data.length - upCount;
document.getElementById('up-count').textContent = upCount;
document.getElementById('down-count').textContent = downCount;
}
function filterFuturesList(keyword) {
keyword = keyword.toLowerCase();
const filtered = allFuturesData.filter(item =>
item.name.toLowerCase().includes(keyword) ||
item.symbol.toLowerCase().includes(keyword)
);
renderFuturesGrid(filtered);
updateStats(filtered);
}
function filterByCategory(category) {
if (category === 'all') {
renderFuturesGrid(allFuturesData);
updateStats(allFuturesData);
} else {
const categoryMap = {
'energy': ['SC', 'EC', 'FU', 'LU', 'BU', 'RU', 'NR', 'ZC'],
'metal': ['AU', 'AG', 'CU', 'AL', 'ZN', 'NI', 'SN', 'PB', 'SS', 'RB', 'HC', 'I', 'J', 'JM', 'SF', 'SM'],
'agriculture': ['M', 'RM', 'C', 'CS', 'A', 'B', 'Y', 'P', 'OI', 'CF', 'SR', 'AP', 'JD', 'LH', 'MA', 'TA', 'EG', 'PP', 'L', 'V', 'SA', 'FG', 'UR', 'SP'],
'finance': ['IF', 'IC', 'IH', 'IM', 'T', 'TF', 'TS', 'TL']
};
const symbols = categoryMap[category] || [];
const filtered = allFuturesData.filter(item => {
const symbolBase = item.symbol.replace(/[0-9]/g, '').toUpperCase();
return symbols.includes(symbolBase);
});
renderFuturesGrid(filtered);
updateStats(filtered);
}
}
function sortFuturesList(sortBy) {
let sorted = [...allFuturesData];
switch(sortBy) {
case 'success_rate':
sorted.sort((a, b) => b.successRate - a.successRate);
break;
case 'trend_score':
sorted.sort((a, b) => b.trendScore - a.trendScore);
break;
case 'change_pct':
sorted.sort((a, b) => b.changePct - a.changePct);
break;
case 'name':
sorted.sort((a, b) => a.name.localeCompare(b.name, 'zh'));
break;
}
renderFuturesGrid(sorted);
}
async function loadFuturesDetail(symbol) {
try {
const response = await fetch(`${API_BASE}/detail/${symbol}`);
const data = await response.json();
if (data.success) {
updateDetailView(data.data);
}
} catch (error) {
console.error('加载详情失败:', error);
const item = allFuturesData.find(d => d.symbol === symbol);
if (item) {
updateDetailView({
...item,
entryPrice: item.price * 0.99,
targetPrice: item.resistance,
stopLoss: item.support,
riskLevel: item.trendScore >= 80 ? '低' : item.trendScore >= 60 ? '中' : '高',
macd: { signal: '金叉', detail: 'DIF: -0.0147' },
rsi: { value: 47, status: '正常' },
boll: { signal: '中轨', detail: '区间: 2086-2215' },
kdj: { signal: '中性', detail: 'K: 71 D: 87' },
resistances: [item.resistance, item.resistance * 1.05, item.resistance * 1.1],
supports: [item.support, item.support * 0.95, item.support * 0.9],
periodConsistency: {
'5': item.periods['5'],
'15': item.periods['15'],
'30': item.periods['30'],
'60': item.periods['60']
},
suggestionReason: '技术面突破,趋势明确'
});
}
}
}
function updateDetailView(data) {
document.getElementById('detail-price').textContent = '¥' + formatNumber(data.price);
document.getElementById('detail-price').className = 'current-price ' + (data.change >= 0 ? 'up' : 'down');
const changeEl = document.getElementById('detail-change');
const changeIcon = data.change >= 0 ? 'up' : 'down';
changeEl.className = 'price-change ' + (data.change >= 0 ? 'up' : 'down');
changeEl.innerHTML = `<i class="fas fa-arrow-${changeIcon}"></i> ${data.change >= 0 ? '+' : ''}${formatNumber(data.change)} (${data.changePct >= 0 ? '+' : ''}${data.changePct.toFixed(2)}%)`;
document.getElementById('detail-open').textContent = formatNumber(data.open);
document.getElementById('detail-high').textContent = formatNumber(data.high);
document.getElementById('detail-low').textContent = formatNumber(data.low);
document.getElementById('detail-volume').textContent = formatNumber(data.volume);
const suggestionBox = document.getElementById('suggestion-box');
suggestionBox.className = 'suggestion-box ' + data.suggestionType;
document.getElementById('suggestion-action').textContent = data.suggestion;
document.getElementById('suggestion-reason').textContent = data.suggestionReason || '';
document.getElementById('entry-price').textContent = formatNumber(data.entryPrice || data.price * 0.99);
document.getElementById('target-price').textContent = formatNumber(data.targetPrice || data.resistance);
document.getElementById('stop-loss').textContent = formatNumber(data.stopLoss || data.support);
document.getElementById('risk-level').textContent = data.riskLevel || '中';
if (data.macd) {
document.getElementById('macd-signal').textContent = data.macd.signal;
document.getElementById('macd-detail').textContent = data.macd.detail;
}
if (data.rsi) {
document.getElementById('rsi-value').textContent = data.rsi.value;
document.getElementById('rsi-status').textContent = data.rsi.status;
}
if (data.boll) {
document.getElementById('boll-signal').textContent = data.boll.signal;
document.getElementById('boll-detail').textContent = data.boll.detail;
}
if (data.kdj) {
document.getElementById('kdj-signal').textContent = data.kdj.signal;
document.getElementById('kdj-detail').textContent = data.kdj.detail;
}
if (data.resistances) {
for (let i = 0; i < 3; i++) {
const el = document.getElementById(`resistance-${i + 1}`);
if (el && data.resistances[i]) {
el.querySelector('.level-value').textContent = formatNumber(data.resistances[i]);
}
}
}
if (data.supports) {
for (let i = 0; i < 3; i++) {
const el = document.getElementById(`support-${i + 1}`);
if (el && data.supports[i]) {
el.querySelector('.level-value').textContent = formatNumber(data.supports[i]);
}
}
}
if (data.periodConsistency) {
const container = document.getElementById('period-consistency');
const periodNames = { '5': '5分钟', '15': '15分钟', '30': '30分钟', '60': '60分钟' };
container.innerHTML = Object.entries(data.periodConsistency).map(([period, trend]) => `
<div class="consistency-row">
<span class="period-name">${periodNames[period]}</span>
<span class="consistency-badge ${trend}">
<i class="fas fa-arrow-${trend === 'up' ? 'up' : trend === 'down' ? 'down' : 'right'}"></i>
${trend === 'up' ? '上涨' : trend === 'down' ? '下跌' : '震荡'}
</span>
</div>
`).join('');
}
}
async function loadKlineData(symbol, period) {
try {
const response = await fetch(`${API_BASE}/kline/${symbol}?period=${period}`);
const data = await response.json();
if (data.success) {
renderKlineChart(data.data);
}
} catch (error) {
console.error('加载K线数据失败:', error);
const mockKline = generateMockKlineData();
renderKlineChart(mockKline);
}
}
function generateMockKlineData() {
const data = [];
let basePrice = 2100;
const now = new Date();
now.setHours(13, 0, 0, 0);
for (let i = 0; i < 60; i++) {
const time = new Date(now.getTime() + i * 15 * 60000);
const timeStr = String(time.getHours()).padStart(2, '0') + ':' + String(time.getMinutes()).padStart(2, '0');
const open = basePrice + (Math.random() - 0.5) * 20;
const close = open + (Math.random() - 0.45) * 25;
const high = Math.max(open, close) + Math.random() * 10;
const low = Math.min(open, close) - Math.random() * 10;
const volume = Math.floor(Math.random() * 1000 + 200);
data.push([timeStr, open.toFixed(2), close.toFixed(2), low.toFixed(2), high.toFixed(2), volume]);
basePrice = close;
}
return data;
}
function renderKlineChart(data) {
if (klineChart) {
klineChart.dispose();
}
const chartDom = document.getElementById('kline-chart');
klineChart = echarts.init(chartDom, 'dark');
const dates = data.map(d => d[0]);
const values = data.map(d => [parseFloat(d[1]), parseFloat(d[2]), parseFloat(d[3]), parseFloat(d[4])]);
const volumes = data.map(d => [parseInt(d[5]), d[2] >= d[1] ? 1 : -1]);
const ma5 = calculateMA(data, 5);
const ma10 = calculateMA(data, 10);
const ma20 = calculateMA(data, 20);
const macdData = calculateMACD(data);
const option = {
backgroundColor: 'transparent',
animation: false,
legend: {
data: ['K线', 'MA5', 'MA10', 'MA20', 'DIF', 'DEA', 'MACD'],
top: 10,
left: 10,
textStyle: { color: '#9aa0ab', fontSize: 11 }
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
crossStyle: { color: '#999' }
},
backgroundColor: 'rgba(26, 29, 40, 0.95)',
borderColor: '#2a2d3a',
textStyle: { color: '#e8eaed', fontSize: 12 },
formatter: function(params) {
if (!params || params.length === 0) return '';
let result = `<div style="font-weight:600;margin-bottom:6px">${params[0].axisValue}</div>`;
params.forEach(p => {
if (p.seriesName === 'K线' && p.data) {
const [o, c, l, h] = p.data;
result += `开: ${o} 收: ${c}<br/>低: ${l} 高: ${h}`;
} else if (p.seriesName === '成交量') {
result += `<br/>成交量: ${p.data}`;
} else if (p.seriesName === 'DIF' || p.seriesName === 'DEA') {
result += `<br/>${p.seriesName}: ${p.data}`;
} else if (p.seriesName === 'MACD') {
result += `<br/>MACD: ${p.data}`;
} else {
result += `<br/>${p.seriesName}: ${p.data}`;
}
});
return result;
}
},
axisPointer: {
link: [{ xAxisIndex: 'all' }],
label: {
backgroundColor: '#22c55e'
}
},
grid: [
{ left: 70, right: 20, top: 60, height: '48%' },
{ left: 70, right: 20, top: '54%', height: '14%' },
{ left: 70, right: 20, top: '73%', height: '17%' }
],
xAxis: [
{
type: 'category',
data: dates,
boundaryGap: true,
axisLine: { lineStyle: { color: '#2a2d3a' } },
axisLabel: { color: '#9aa0ab', fontSize: 10 },
splitLine: { show: false }
},
{
type: 'category',
gridIndex: 1,
data: dates,
boundaryGap: true,
axisLine: { lineStyle: { color: '#2a2d3a' } },
axisLabel: { show: false },
splitLine: { show: false }
},
{
type: 'category',
gridIndex: 2,
data: dates,
boundaryGap: true,
axisLine: { lineStyle: { color: '#2a2d3a' } },
axisLabel: { color: '#9aa0ab', fontSize: 10 },
splitLine: { show: false }
}
],
yAxis: [
{
scale: true,
axisLine: { lineStyle: { color: '#2a2d3a' } },
axisLabel: { color: '#9aa0ab' },
splitLine: { lineStyle: { color: '#2a2d3a', type: 'dashed' } }
},
{
scale: true,
gridIndex: 1,
axisLine: { show: false },
axisTick: { show: false },
axisLabel: { show: false },
splitLine: { show: false }
},
{
scale: true,
gridIndex: 2,
axisLine: { lineStyle: { color: '#2a2d3a' } },
axisLabel: { color: '#9aa0ab', fontSize: 10 },
splitLine: { lineStyle: { color: '#2a2d3a', type: 'dashed' } }
}
],
dataZoom: [
{
type: 'inside',
xAxisIndex: [0, 1, 2],
start: 50,
end: 100
},
{
show: true,
xAxisIndex: [0, 1, 2],
type: 'slider',
bottom: 5,
height: 18,
borderColor: 'transparent',
backgroundColor: '#1a1d28',
fillerColor: 'rgba(34, 197, 94, 0.15)',
handleStyle: { color: '#22c55e' },
textStyle: { color: '#9aa0ab' }
}
],
series: [
{
name: 'K线',
type: 'candlestick',
data: values,
itemStyle: {
color: '#22c55e',
color0: '#ef4444',
borderColor: '#22c55e',
borderColor0: '#ef4444'
}
},
{
name: 'MA5',
type: 'line',
data: ma5,
lineStyle: { width: 1, color: '#f59e0b' },
symbol: 'none'
},
{
name: 'MA10',
type: 'line',
data: ma10,
lineStyle: { width: 1, color: '#3b82f6' },
symbol: 'none'
},
{
name: 'MA20',
type: 'line',
data: ma20,
lineStyle: { width: 1, color: '#8b5cf6' },
symbol: 'none'
},
{
name: '成交量',
type: 'bar',
xAxisIndex: 1,
yAxisIndex: 1,
data: volumes.map(v => ({
value: v[0],
itemStyle: {
color: v[1] >= 0 ? '#22c55e' : '#ef4444',
opacity: 0.6
}
}))
},
{
name: 'DIF',
type: 'line',
xAxisIndex: 2,
yAxisIndex: 2,
data: macdData.dif,
lineStyle: { width: 1.5, color: '#3b82f6' },
symbol: 'none'
},
{
name: 'DEA',
type: 'line',
xAxisIndex: 2,
yAxisIndex: 2,
data: macdData.dea,
lineStyle: { width: 1.5, color: '#f59e0b' },
symbol: 'none'
},
{
name: 'MACD',
type: 'bar',
xAxisIndex: 2,
yAxisIndex: 2,
data: macdData.macd.map((val, idx) => ({
value: val,
itemStyle: {
color: val >= 0 ? '#22c55e' : '#ef4444',
opacity: 0.7
}
}))
}
]
};
klineChart.setOption(option);
window.addEventListener('resize', () => {
klineChart && klineChart.resize();
});
}
function calculateMA(data, dayCount) {
const result = [];
for (let i = 0; i < data.length; i++) {
if (i < dayCount - 1) {
result.push('-');
continue;
}
let sum = 0;
for (let j = 0; j < dayCount; j++) {
sum += parseFloat(data[i - j][2]);
}
result.push(parseFloat((sum / dayCount).toFixed(2)));
}
return result;
}
function calculateMACD(data) {
const closes = data.map(d => parseFloat(d[2]));
const ema12 = calcEMA(closes, 12);
const ema26 = calcEMA(closes, 26);
const dif = [];
for (let i = 0; i < closes.length; i++) {
if (ema12[i] !== null && ema26[i] !== null) {
dif.push(ema12[i] - ema26[i]);
} else {
dif.push(0);
}
}
const dea = calcEMA(dif, 9);
const macd = dif.map((d, i) => 2 * (d - (dea[i] || 0)));
return { dif, dea, macd };
}
function calcEMA(data, period) {
const result = new Array(data.length).fill(null);
const multiplier = 2 / (period + 1);
if (data.length < period) return result;
let sum = 0;
for (let i = 0; i < period; i++) {
sum += data[i];
}
result[period - 1] = sum / period;
for (let i = period; i < data.length; i++) {
result[i] = (data[i] - result[i - 1]) * multiplier + result[i - 1];
}
return result;
}