diff --git a/web/templates/index.html b/web/templates/index.html
index fadb286..ff7d37d 100644
--- a/web/templates/index.html
+++ b/web/templates/index.html
@@ -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;
+ }
@@ -513,6 +693,12 @@
@@ -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 += `
+
+
+
+
${ma.type}(${ma.length})${smoothText}
+
${ma.visible ? latestValue : '--'}
+
+
+
+
+
+
+
+ `;
+
+ 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);
});
+
+
+