添加均线系统

This commit is contained in:
jackyu66git
2025-06-30 22:29:40 +08:00
parent 770655c53f
commit ceacf3ff03
+742 -2
View File
@@ -286,6 +286,186 @@
}
/* 主次周期元素样式区分 */
/* 均线系统样式 */
.ma-panel {
position: absolute;
top: 10px;
left: 10px;
z-index: 1000;
background: rgba(255, 255, 255, 0.95);
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 8px;
max-width: 300px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.ma-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 0;
font-size: 12px;
border-bottom: 1px solid #f0f0f0;
}
.ma-item:last-child {
border-bottom: none;
}
.ma-info {
display: flex;
align-items: center;
gap: 8px;
}
.ma-label {
font-weight: 500;
}
.ma-value {
color: #666;
}
.ma-actions {
display: flex;
gap: 4px;
}
.ma-action-btn {
background: none;
border: none;
cursor: pointer;
padding: 2px 4px;
border-radius: 3px;
font-size: 11px;
color: #666;
}
.ma-action-btn:hover {
background-color: #f0f0f0;
color: #333;
}
.ma-color-indicator {
width: 12px;
height: 2px;
border-radius: 1px;
margin-right: 4px;
}
.add-ma-btn {
position: absolute;
top: 10px;
right: 120px;
z-index: 100;
background-color: #0d6efd;
color: white;
border: none;
padding: 8px 12px;
border-radius: 6px;
font-size: 13px;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.add-ma-btn:hover {
background-color: #0a58ca;
}
/* 配置弹窗样式 */
.ma-config-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 10000;
display: none;
justify-content: center;
align-items: center;
}
.ma-config-content {
background: white;
border-radius: 8px;
padding: 20px;
width: 400px;
max-width: 90vw;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
}
.ma-config-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 20px;
color: #333;
}
.ma-config-form {
display: grid;
gap: 16px;
}
.ma-config-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.ma-config-label {
font-size: 14px;
font-weight: 500;
color: #555;
}
.ma-config-input, .ma-config-select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.ma-config-input:focus, .ma-config-select:focus {
outline: none;
border-color: #0d6efd;
box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
}
.ma-config-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 20px;
}
.ma-config-btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
}
.ma-config-btn-primary {
background-color: #0d6efd;
color: white;
}
.ma-config-btn-primary:hover {
background-color: #0a58ca;
}
.ma-config-btn-secondary {
background-color: #6c757d;
color: white;
}
.ma-config-btn-secondary:hover {
background-color: #5c636a;
}
</style>
</head>
<body>
@@ -513,6 +693,12 @@
<div class="chart-container">
<div id="tradingview_chart"></div>
<!-- 添加均线按钮 -->
<button class="add-ma-btn" onclick="showMAConfig()">
<i class="bi bi-plus-lg"></i> 添加均线
</button>
<!-- 均线信息面板 -->
<div id="maPanel" class="ma-panel" style="display: none;"></div>
</div>
<div class="data-container">
@@ -1382,7 +1568,8 @@
elementUncompletedZsSeries: [],
tradePointSeries: [],
mainBollingerSeries: [],
elementBollingerSeries: []
elementBollingerSeries: [],
maSeries: [] // 添加均线系列数组
},
state: {
isInitialized: false,
@@ -3387,7 +3574,7 @@
if (fx.fx_strength < 1.0) { // 降低阈值让更多分型显示
displayText = fx.fx_strength >= 0.8 ? '' : '' // 0.8以上显示点,0.8以下不显示文本
}
displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("1", "").replace("2", "").replace("30", "");
displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("3", "");
// 添加标记配置
const markerConfig = {
time: timestamp,
@@ -3735,6 +3922,9 @@
tvWidget.macdChart = macdChart;
tvWidget.state.isInitialized = true;
// 添加均线到图表
addMovingAveragesToChart(candles);
// 绑定同步事件
bindSyncEvents(mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, mainChart, volumeChart, atrChart, macdChart, showMacd);
@@ -3843,6 +4033,9 @@
tvWidget.series.lineSeries.setData(lineData);
}
// 更新均线数据
addMovingAveragesToChart(candles);
// 更新成交量数据
let volumes = [];
if (useElementPeriod && currentData.element_kline_data && Array.isArray(currentData.element_kline_data)) {
@@ -6621,6 +6814,499 @@
document.body.removeChild(link);
}
// 均线系统全局变量
let movingAverages = []; // 存储所有均线配置
let maIdCounter = 0; // 均线ID计数器
// 显示均线配置弹窗
function showMAConfig() {
$('#maConfigModal').css('display', 'flex');
// 重置表单
$('#maType').val('SMA');
$('#maLength').val(20);
$('#maSource').val('close');
$('#maSmoothType').val('none');
$('#maSmoothLength').val(3).prop('disabled', true);
$('#maColor').val(getRandomColor());
}
// 隐藏均线配置弹窗
function hideMAConfig() {
$('#maConfigModal').css('display', 'none');
}
// 获取随机颜色
function getRandomColor() {
const colors = ['#2962FF', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4',
'#FECA57', '#48CAE4', '#F38BA8', '#A8DADC', '#F1C0E8'];
return colors[Math.floor(Math.random() * colors.length)];
}
// 添加均线
function addMovingAverage() {
const config = {
id: ++maIdCounter,
type: $('#maType').val(),
length: parseInt($('#maLength').val()),
source: $('#maSource').val(),
smoothType: $('#maSmoothType').val(),
smoothLength: parseInt($('#maSmoothLength').val()),
color: $('#maColor').val(),
visible: true
};
// 验证输入
if (config.length < 1 || config.length > 200) {
alert('均线长度必须在1-200之间');
return;
}
if (config.smoothType !== 'none' && (config.smoothLength < 1 || config.smoothLength > 50)) {
alert('平滑长度必须在1-50之间');
return;
}
movingAverages.push(config);
hideMAConfig();
// 重新绘制图表以包含新均线
if (typeof updateChart === 'function') {
updateChart();
}
}
// 计算均线数据
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, val) => sum + val, 0) / length;
break;
case 'EMA':
if (i === length - 1) {
// 第一个EMA值使用SMA
value = sourceData.slice(0, length)
.reduce((sum, val) => sum + val, 0) / length;
} else {
const multiplier = 2 / (length + 1);
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];
}
result.push({
time: data[i].time,
value: value
});
}
return result;
}
// 应用平滑处理
function applySmoothToMA(maData, smoothType, smoothLength) {
if (smoothType === 'none' || maData.length < smoothLength) {
return maData;
}
const result = [];
for (let i = smoothLength - 1; i < maData.length; i++) {
let value;
switch(smoothType) {
case 'SMA':
value = maData.slice(i - smoothLength + 1, i + 1)
.reduce((sum, item) => sum + item.value, 0) / smoothLength;
break;
case 'EMA':
if (i === smoothLength - 1) {
value = maData.slice(0, smoothLength)
.reduce((sum, item) => sum + item.value, 0) / smoothLength;
} else {
const multiplier = 2 / (smoothLength + 1);
value = maData[i].value * multiplier + result[result.length - 1].value * (1 - multiplier);
}
break;
case 'WMA':
let weightSum = 0;
let valueSum = 0;
for (let j = 0; j < smoothLength; j++) {
const weight = j + 1;
weightSum += weight;
valueSum += maData[i - smoothLength + 1 + j].value * weight;
}
value = valueSum / weightSum;
break;
default:
value = maData[i].value;
}
result.push({
time: maData[i].time,
value: value
});
}
return result;
}
// 更新均线面板
function updateMAPanel() {
const panel = $('#maPanel');
if (movingAverages.length === 0) {
panel.hide();
return;
}
let html = '';
let hasVisibleMA = false;
movingAverages.forEach(ma => {
// 获取最新价格
const latestValue = ma.data && ma.data.length > 0 ?
ma.data[ma.data.length - 1].value.toFixed(2) : '--';
const smoothText = ma.smoothType !== 'none' ?
`, ${ma.smoothType}(${ma.smoothLength})` : '';
// 显示所有均线(包括隐藏的),但用不同样式区分
const itemStyle = ma.visible ? '' : 'opacity: 0.5;';
const eyeIcon = ma.visible ? 'bi-eye' : 'bi-eye-slash';
html += `
<div class="ma-item" data-ma-id="${ma.id}" style="${itemStyle}">
<div class="ma-info">
<div class="ma-color-indicator" style="background-color: ${ma.color}"></div>
<span class="ma-label">${ma.type}(${ma.length})${smoothText}</span>
<span class="ma-value">${ma.visible ? latestValue : '--'}</span>
</div>
<div class="ma-actions">
<button class="ma-action-btn" onclick="toggleMAVisibility(${ma.id})" title="${ma.visible ? '隐藏' : '显示'}">
<i class="bi ${eyeIcon}"></i>
</button>
<button class="ma-action-btn" onclick="configMA(${ma.id})" title="配置">
<i class="bi bi-gear"></i>
</button>
<button class="ma-action-btn" onclick="deleteMA(${ma.id})" title="删除">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
`;
if (ma.visible) {
hasVisibleMA = true;
}
});
if (html.trim() === '') {
panel.hide();
} else {
panel.html(html);
panel.show();
}
}
// 切换均线可见性
function toggleMAVisibility(maId) {
const ma = movingAverages.find(m => m.id === maId);
if (ma) {
ma.visible = !ma.visible;
// 如果图表已初始化,直接更新均线显示
if (tvWidget.mainChart && currentData && currentData.kline_data) {
// 获取K线数据
const useElementPeriod = $('#elementPeriodKline').is(':checked') &&
currentData.element_kline_data &&
Array.isArray(currentData.element_kline_data);
let candles = [];
if (useElementPeriod) {
candles = currentData.element_kline_data.map((kline) => {
const date = new Date(kline.date);
const timestamp = date.getTime() / 1000;
return {
time: timestamp,
open: parseFloat(kline.open),
high: parseFloat(kline.high),
low: parseFloat(kline.low),
close: parseFloat(kline.close),
};
});
} else {
candles = currentData.kline_data.map((kline) => {
const date = new Date(kline.date);
const timestamp = date.getTime() / 1000;
return {
time: timestamp,
open: parseFloat(kline.open),
high: parseFloat(kline.high),
low: parseFloat(kline.low),
close: parseFloat(kline.close),
};
});
}
// 重新添加均线到图表
addMovingAveragesToChart(candles);
} else {
// 如果图表未初始化,重新加载整个图表
if (typeof updateChart === 'function') {
updateChart();
}
}
}
}
// 配置均线
function configMA(maId) {
const ma = movingAverages.find(m => m.id === maId);
if (ma) {
// 填充表单
$('#maType').val(ma.type);
$('#maLength').val(ma.length);
$('#maSource').val(ma.source);
$('#maSmoothType').val(ma.smoothType);
$('#maSmoothLength').val(ma.smoothLength);
$('#maColor').val(ma.color);
// 启用/禁用平滑长度输入
$('#maSmoothLength').prop('disabled', ma.smoothType === 'none');
// 修改按钮为更新模式
$('.ma-config-btn-primary').text('更新').off('click').on('click', function() {
updateMovingAverage(maId);
});
$('#maConfigModal').css('display', 'flex');
}
}
// 更新均线
function updateMovingAverage(maId) {
const ma = movingAverages.find(m => m.id === maId);
if (ma) {
// 验证输入
const newLength = parseInt($('#maLength').val());
const newSmoothLength = parseInt($('#maSmoothLength').val());
const newSmoothType = $('#maSmoothType').val();
if (newLength < 1 || newLength > 200) {
alert('均线长度必须在1-200之间');
return;
}
if (newSmoothType !== 'none' && (newSmoothLength < 1 || newSmoothLength > 50)) {
alert('平滑长度必须在1-50之间');
return;
}
// 更新配置
ma.type = $('#maType').val();
ma.length = newLength;
ma.source = $('#maSource').val();
ma.smoothType = newSmoothType;
ma.smoothLength = newSmoothLength;
ma.color = $('#maColor').val();
hideMAConfig();
// 恢复按钮为添加模式
$('.ma-config-btn-primary').text('添加').off('click').on('click', addMovingAverage);
// 如果图表已初始化,直接更新均线显示
if (tvWidget.mainChart && currentData && currentData.kline_data) {
const useElementPeriod = $('#elementPeriodKline').is(':checked') &&
currentData.element_kline_data &&
Array.isArray(currentData.element_kline_data);
let candles = [];
if (useElementPeriod) {
candles = currentData.element_kline_data.map((kline) => {
const date = new Date(kline.date);
const timestamp = date.getTime() / 1000;
return {
time: timestamp,
open: parseFloat(kline.open),
high: parseFloat(kline.high),
low: parseFloat(kline.low),
close: parseFloat(kline.close),
};
});
} else {
candles = currentData.kline_data.map((kline) => {
const date = new Date(kline.date);
const timestamp = date.getTime() / 1000;
return {
time: timestamp,
open: parseFloat(kline.open),
high: parseFloat(kline.high),
low: parseFloat(kline.low),
close: parseFloat(kline.close),
};
});
}
addMovingAveragesToChart(candles);
} else {
if (typeof updateChart === 'function') {
updateChart();
}
}
}
}
// 删除均线
function deleteMA(maId) {
movingAverages = movingAverages.filter(m => m.id !== maId);
// 如果图表已初始化,直接更新均线显示
if (tvWidget.mainChart && currentData && currentData.kline_data) {
const useElementPeriod = $('#elementPeriodKline').is(':checked') &&
currentData.element_kline_data &&
Array.isArray(currentData.element_kline_data);
let candles = [];
if (useElementPeriod) {
candles = currentData.element_kline_data.map((kline) => {
const date = new Date(kline.date);
const timestamp = date.getTime() / 1000;
return {
time: timestamp,
open: parseFloat(kline.open),
high: parseFloat(kline.high),
low: parseFloat(kline.low),
close: parseFloat(kline.close),
};
});
} else {
candles = currentData.kline_data.map((kline) => {
const date = new Date(kline.date);
const timestamp = date.getTime() / 1000;
return {
time: timestamp,
open: parseFloat(kline.open),
high: parseFloat(kline.high),
low: parseFloat(kline.low),
close: parseFloat(kline.close),
};
});
}
addMovingAveragesToChart(candles);
} else {
if (typeof updateChart === 'function') {
updateChart();
}
}
}
// 添加均线到图表
function addMovingAveragesToChart(candleData) {
if (!tvWidget.mainChart || !candleData || candleData.length === 0) {
return;
}
// 清除现有的均线系列
if (tvWidget.series.maSeries) {
tvWidget.series.maSeries.forEach(series => {
try {
tvWidget.mainChart.removeSeries(series);
} catch (e) {
console.debug('移除均线系列时出错:', e);
}
});
}
tvWidget.series.maSeries = [];
// 为每个均线配置创建系列
movingAverages.forEach(maConfig => {
if (!maConfig.visible) return;
try {
// 计算均线数据
const maData = calculateMA(candleData, maConfig.type, maConfig.length, maConfig.source);
// 应用平滑处理
const smoothedData = maConfig.smoothType !== 'none' ?
applySmoothToMA(maData, maConfig.smoothType, maConfig.smoothLength) :
maData;
// 创建线系列
const maSeries = tvWidget.mainChart.addLineSeries({
color: maConfig.color,
lineWidth: 2,
title: `${maConfig.type}(${maConfig.length})`,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: true,
});
// 设置数据
maSeries.setData(smoothedData);
// 保存数据到配置中以便显示最新值
maConfig.data = smoothedData;
// 保存系列引用
tvWidget.series.maSeries.push(maSeries);
} catch (e) {
console.error('添加均线时出错:', e, maConfig);
}
});
// 更新均线面板
updateMAPanel();
}
// 监听平滑类型变化
$(document).on('change', '#maSmoothType', function() {
const isNone = $(this).val() === 'none';
$('#maSmoothLength').prop('disabled', isNone);
});
// 点击弹窗外部关闭
$(document).on('click', '#maConfigModal', function(e) {
if (e.target === this) {
hideMAConfig();
}
});
// 页面加载完成后初始化
$(document).ready(function() {
// 设置默认的筛选时间(最近7天)
@@ -6680,5 +7366,59 @@
}, 2000);
});
</script>
<!-- 均线配置弹窗 -->
<div id="maConfigModal" class="ma-config-modal">
<div class="ma-config-content">
<div class="ma-config-title">添加均线</div>
<div class="ma-config-form">
<div class="ma-config-group">
<label class="ma-config-label">类型</label>
<select id="maType" class="ma-config-select">
<option value="SMA">SMA (简单移动平均)</option>
<option value="EMA">EMA (指数移动平均)</option>
<option value="WMA">WMA (加权移动平均)</option>
</select>
</div>
<div class="ma-config-group">
<label class="ma-config-label">长度</label>
<input type="number" id="maLength" class="ma-config-input" value="20" min="1" max="200">
</div>
<div class="ma-config-group">
<label class="ma-config-label">数据来源</label>
<select id="maSource" class="ma-config-select">
<option value="close">收盘价</option>
<option value="open">开盘价</option>
<option value="high">最高价</option>
<option value="low">最低价</option>
<option value="hl2">最高最低平均价 (HL2)</option>
<option value="hlc3">典型价 (HLC3)</option>
<option value="ohlc4">平均价 (OHLC4)</option>
</select>
</div>
<div class="ma-config-group">
<label class="ma-config-label">平滑线类型</label>
<select id="maSmoothType" class="ma-config-select">
<option value="none">无平滑</option>
<option value="SMA">SMA</option>
<option value="EMA">EMA</option>
<option value="WMA">WMA</option>
</select>
</div>
<div class="ma-config-group">
<label class="ma-config-label">平滑长度</label>
<input type="number" id="maSmoothLength" class="ma-config-input" value="3" min="1" max="50" disabled>
</div>
<div class="ma-config-group">
<label class="ma-config-label">颜色</label>
<input type="color" id="maColor" class="ma-config-input" value="#2962FF">
</div>
</div>
<div class="ma-config-actions">
<button class="ma-config-btn ma-config-btn-secondary" onclick="hideMAConfig()">取消</button>
<button class="ma-config-btn ma-config-btn-primary" onclick="addMovingAverage()">添加</button>
</div>
</div>
</div>
</body>
</html>