Files
Chan/web/templates/index.html
T
2025-04-25 21:39:24 +08:00

3775 lines
178 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>缠论分析系统</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css" rel="stylesheet">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.datatables.net/1.11.5/js/jquery.dataTables.min.js"></script>
<link href="https://cdn.datatables.net/1.11.5/css/jquery.dataTables.min.css" rel="stylesheet">
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- TradingView Widget BEGIN -->
<script src="https://cdn.jsdelivr.net/npm/lightweight-charts@4.0.1/dist/lightweight-charts.standalone.production.js"></script>
<!-- TradingView Widget END -->
<style>
body {
font-family: "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
margin: 0;
padding: 20px;
background-color: #f8f9fa;
color: #333;
}
.container {
max-width: 100%;
margin: 0 auto;
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.header {
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.controls {
margin-bottom: 20px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.form-label {
font-weight: 500;
margin-bottom: 6px;
color: #495057;
}
.chart-container {
width: 100%;
height: 600px;
margin-top: 20px;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 10px;
background-color: #fff;
position: relative;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
#tradingview_chart {
width: 100%;
height: 100%;
}
.chart-options {
position: absolute;
top: 10px;
right: 10px;
z-index: 100;
background-color: rgba(255, 255, 255, 0.9);
padding: 12px;
border-radius: 8px;
font-size: 13px;
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
}
.data-container {
margin-top: 30px;
}
.nav-tabs {
margin-bottom: 15px;
}
.table-container {
overflow-x: auto;
}
.btn-primary {
background-color: #0d6efd;
border-color: #0d6efd;
color: white;
padding: 8px 16px;
font-weight: 500;
transition: all 0.2s;
}
.btn-primary:hover {
background-color: #0a58ca;
border-color: #0a53be;
}
#loadingIndicator {
display: none;
text-align: center;
padding: 20px;
font-size: 18px;
color: #666;
}
/* 方向列颜色 */
.direction-up {
color: #dc3545;
font-weight: bold;
}
.direction-down {
color: #28a745;
font-weight: bold;
}
/* MACD列颜色 */
.positive {
color: #dc3545;
}
.negative {
color: #28a745;
}
/* 自定义控制面板 */
.form-check-label {
cursor: pointer;
user-select: none;
}
/* 增加表单元素间距 */
.g-3 {
--bs-gutter-y: 1rem;
}
.point-tooltip {
position: absolute;
background: rgba(40, 40, 40, 0.9);
color: white;
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
z-index: 1000;
pointer-events: none;
max-width: 300px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
display: none;
}
.buy-point {
color: #ff1744;
font-weight: bold;
}
.sell-point {
color: #00e676;
font-weight: bold;
}
.buy-marker {
background-color: #ff1744;
border: 2px solid white;
}
.sell-marker {
background-color: #00e676;
border: 2px solid white;
}
/* 主次周期元素样式区分 */
.main-bi {
color: #dc3545;
font-weight: bold;
}
.element-bi {
color: #9c27b0;
font-weight: bold;
}
.main-seg {
color: #FF6B6B;
font-weight: bold;
}
.element-seg {
color: #673ab7;
font-weight: bold;
}
.main-zs {
color: #F1C40F;
font-weight: bold;
}
.element-zs {
color: #3f51b5;
font-weight: bold;
}
/* 加载指示器旋转动画 */
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-spinner {
display: inline-block;
width: 18px;
height: 18px;
border: 2px solid rgba(0, 0, 0, 0.1);
border-left-color: #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-left: 8px;
vertical-align: middle;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1 class="my-4">缠论分析系统</h1>
</div>
<div class="controls">
<div class="row g-3 align-items-end">
<div class="col-md-3">
<label for="symbol" class="form-label">交易对:</label>
<select id="symbol" class="form-select">
{% for symbol in symbols %}
<option value="{{ symbol }}" {% if symbol == 'SOL/USDT:USDT' %}selected{% endif %}>{{ symbol }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<label for="timeframe" class="form-label">时间周期:</label>
<select id="timeframe" class="form-select">
{% for value, label in timeframes.items() %}
<option value="{{ value }}" {% if value == '5m' %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<label for="timezone" class="form-label">时区:</label>
<select id="timezone" class="form-select">
<option value="UTC">UTC</option>
<option value="Asia/Shanghai" selected>Asia/Shanghai (UTC+8)</option>
<option value="America/New_York">America/New_York (UTC-4/5)</option>
<option value="Europe/London">Europe/London (UTC+0/1)</option>
<option value="Europe/Berlin">Europe/Berlin (UTC+1/2)</option>
<option value="Asia/Tokyo">Asia/Tokyo (UTC+9)</option>
</select>
</div>
<div class="col-md-2">
<label for="start_time" class="form-label">开始时间:</label>
<input type="datetime-local" id="start_time" class="form-control">
</div>
<div class="col-md-2">
<label for="end_time" class="form-label">结束时间:</label>
<input type="datetime-local" id="end_time" class="form-control">
</div>
<div class="col-md-1">
<button class="btn btn-primary w-100" onclick="updateChart()">分析</button>
</div>
</div>
<div class="row mt-3">
<div class="col-md-5">
<div class="d-flex align-items-center">
<label class="form-label me-3 mb-0">基础显示:</label>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showOriginalKline" checked>
<label class="form-check-label" for="showOriginalKline">原始K线</label>
</div>
<!-- 添加K线周期切换 -->
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="klinePeriod" id="mainPeriodKline" checked>
<label class="form-check-label" for="mainPeriodKline">主周期</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="klinePeriod" id="elementPeriodKline">
<label class="form-check-label" for="elementPeriodKline">小周期</label>
<i class="bi bi-info-circle" data-bs-toggle="tooltip" title="显示小周期K线,同时可以叠加大周期分型和笔段"></i>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showVolume" checked>
<label class="form-check-label" for="showVolume">成交量</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showMacd" checked>
<label class="form-check-label" for="showMacd">MACD</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showKlcFxType">
<label class="form-check-label" for="showKlcFxType">分型类型</label>
</div>
<!-- 添加小周期分型显示控制 -->
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showElementKlcFxType" checked>
<label class="form-check-label" for="showElementKlcFxType">小周期分型</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showTradePoints" checked>
<label class="form-check-label" for="showTradePoints">买卖点</label>
</div>
</div>
<div class="d-flex align-items-center mt-2">
<label class="form-label me-3 mb-0">主周期:</label>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showMainBi" checked>
<label class="form-check-label" for="showMainBi"></label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showMainSeg" checked>
<label class="form-check-label" for="showMainSeg">线段</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showMainZs" checked>
<label class="form-check-label" for="showMainZs">中枢</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showMainUncompletedZs" checked>
<label class="form-check-label" for="showMainUncompletedZs">未完成中枢</label>
</div>
</div>
<div class="d-flex align-items-center mt-2">
<label class="form-label me-3 mb-0">次周期:</label>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showElementBi" checked>
<label class="form-check-label" for="showElementBi"></label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showElementSeg" checked>
<label class="form-check-label" for="showElementSeg">线段</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showElementZs" checked>
<label class="form-check-label" for="showElementZs">中枢</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showElementUncompletedZs" checked>
<label class="form-check-label" for="showElementUncompletedZs">未完成中枢</label>
</div>
</div>
</div>
<div class="col-md-6">
<div class="d-flex align-items-center">
<label for="elementTimeframe" class="form-label me-3 mb-0">次级别时间周期:</label>
<select id="elementTimeframe" class="form-select form-select-sm" style="width: auto;">
{% for value, label in timeframes.items() %}
<option value="{{ value }}" {% if value == '1m' %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
<small class="text-muted ms-2">仅影响笔、线段和中枢分析</small>
</div>
<div class="d-flex align-items-center mt-2">
<label for="refreshInterval" class="form-label me-3 mb-0">自动刷新:</label>
<select id="refreshInterval" class="form-select form-select-sm me-2" style="width: auto;">
<option value="0.0833">5秒</option>
<option value="0.1667">10秒</option>
<option value="0.25">15秒</option>
<option value="0.5">30秒</option>
<option value="1">1分钟</option>
<option value="2">2分钟</option>
<option value="3">3分钟</option>
<option value="5" selected>5分钟</option>
<option value="10">10分钟</option>
</select>
<div class="form-check form-check-inline m-0">
<input class="form-check-input" type="checkbox" id="autoRefresh">
<label class="form-check-label" for="autoRefresh">启用</label>
</div>
<span id="nextRefreshTime" class="ms-2 text-muted" style="display:none;font-size:0.85rem;"></span>
<div id="refreshLoadingSpinner" class="loading-spinner ms-2" style="display:none;"></div>
</div>
</div>
</div>
</div>
<div id="loadingIndicator" style="display:none;"></div>
<div class="chart-container">
<div id="tradingview_chart"></div>
</div>
<div class="data-container">
<ul class="nav nav-tabs" id="dataTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="kline-tab" data-bs-toggle="tab" data-bs-target="#kline" type="button" role="tab">K线数据</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="bi-tab" data-bs-toggle="tab" data-bs-target="#bi" type="button" role="tab"></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="seg-tab" data-bs-toggle="tab" data-bs-target="#seg" type="button" role="tab">线段</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="zs-tab" data-bs-toggle="tab" data-bs-target="#zs" type="button" role="tab">中枢</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="macd-tab" data-bs-toggle="tab" data-bs-target="#macd" type="button" role="tab">MACD</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="trade-points-tab" data-bs-toggle="tab" data-bs-target="#trade-points" type="button" role="tab">买卖点</button>
</li>
</ul>
<div class="data-source-info alert alert-info py-2 mt-1 mb-2" style="display:none;">
<small id="dataSourceText"></small>
</div>
<div class="tab-content" id="dataTabsContent">
<div class="tab-pane fade show active" id="kline" role="tabpanel">
<div class="table-container">
<table id="klineTable" class="display compact" style="width:100%">
<thead>
<tr>
<th>时间</th>
<th>开盘价</th>
<th>最高价</th>
<th>最低价</th>
<th>收盘价</th>
<th>成交量</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="tab-pane fade" id="bi" role="tabpanel">
<div class="table-container">
<table id="biTable" class="display compact" style="width:100%">
<thead>
<tr>
<th>起始时间</th>
<th>结束时间</th>
<th>起始价格</th>
<th>结束价格</th>
<th>方向</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="tab-pane fade" id="seg" role="tabpanel">
<div class="table-container">
<table id="segTable" class="display compact" style="width:100%">
<thead>
<tr>
<th>起始时间</th>
<th>结束时间</th>
<th>起始价格</th>
<th>结束价格</th>
<th>方向</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="tab-pane fade" id="zs" role="tabpanel">
<div class="table-container">
<h5>已完成中枢</h5>
<table id="zsTable" class="display compact" style="width:100%">
<thead>
<tr>
<th>起始时间</th>
<th>结束时间</th>
<th>中枢上沿(ZG)</th>
<th>中枢下沿(ZD)</th>
</tr>
</thead>
<tbody></tbody>
</table>
<h5 class="mt-4">未完成中枢</h5>
<table id="uncompletedZsTable" class="display compact" style="width:100%">
<thead>
<tr>
<th>起始时间</th>
<th>中枢上沿(ZG)</th>
<th>中枢下沿(ZD)</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="tab-pane fade" id="macd" role="tabpanel">
<div class="table-container">
<table id="macdTable" class="display compact" style="width:100%">
<thead>
<tr>
<th>时间</th>
<th>收盘价</th>
<th>MACD</th>
<th>信号线</th>
<th>直方图</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="tab-pane fade" id="trade-points" role="tabpanel">
<div class="table-container">
<table id="tradePointsTable" class="display compact" style="width:100%">
<thead>
<tr>
<th>时间</th>
<th>价格</th>
<th>类型</th>
<th>描述</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
let currentData = null;
const tables = {};
let tvWidget = {
mainChart: null,
volumeChart: null,
macdChart: null,
series: {
candleSeries: null,
lineSeries: null,
volumeSeries: null,
macdLineSeries: null,
signalLineSeries: null,
histogramSeries: null,
mainBiSeries: [],
mainSegSeries: [],
mainZsSeries: [],
mainUncompletedZsSeries: [],
elementBiSeries: [],
elementSegSeries: [],
elementZsSeries: [],
elementUncompletedZsSeries: [],
tradePointSeries: []
},
state: {
isInitialized: false,
visibleRange: null,
logicalRange: null
}
};
// 买卖点类型定义
const TRADE_POINT_TYPE = {
BUY1: 1, // 一类买点
BUY2: 2, // 二类买点
BUY3: 3, // 三类买点
SELL1: -1, // 一类卖点
SELL2: -2, // 二类卖点
SELL3: -3 // 三类卖点
};
// 买卖点样式定义
const TRADE_POINT_STYLE = {
[TRADE_POINT_TYPE.BUY1]: {color: '#FF1744', shape: 'arrowUp', text: '买1', size: 2},
[TRADE_POINT_TYPE.BUY2]: {color: '#F50057', shape: 'circle', text: '买2', size: 2},
[TRADE_POINT_TYPE.BUY3]: {color: '#D500F9', shape: 'square', text: '买3', size: 2},
[TRADE_POINT_TYPE.SELL1]: {color: '#00E676', shape: 'arrowDown', text: '卖1', size: 2},
[TRADE_POINT_TYPE.SELL2]: {color: '#00B0FF', shape: 'circle', text: '卖2', size: 2},
[TRADE_POINT_TYPE.SELL3]: {color: '#FFEA00', shape: 'square', text: '卖3', size: 2}
};
// 定义标记垂直偏移系数 - 合约市场通常波动较大,减小偏移防止显示在范围外
const TRADE_POINT_OFFSET = {
[TRADE_POINT_TYPE.BUY1]: 0, // 一类买点向下偏移2.0%的价格
[TRADE_POINT_TYPE.BUY2]: 0, // 二类买点向下偏移1.5%的价格
[TRADE_POINT_TYPE.BUY3]: 0, // 三类买点向下偏移1.0%的价格
[TRADE_POINT_TYPE.SELL1]: 0, // 一类卖点向上偏移2.0%的价格
[TRADE_POINT_TYPE.SELL2]: 0,// 二类卖点向上偏移1.5%的价格
[TRADE_POINT_TYPE.SELL3]: 0 // 三类卖点向上偏移1.0%的价格
};
// 更改为基于价格百分比的垂直偏移 - 合约市场适用的更小偏移
const PRICE_PERCENT_OFFSET = {
[TRADE_POINT_TYPE.BUY1]: 0, // 一类买点向下偏移价格的0.2%
[TRADE_POINT_TYPE.BUY2]: 0, // 二类买点向下偏移价格的0.15%
[TRADE_POINT_TYPE.BUY3]: 0, // 三类买点向下偏移价格的0.1%
[TRADE_POINT_TYPE.SELL1]: 0, // 一类卖点向上偏移价格的0.2%
[TRADE_POINT_TYPE.SELL2]: 0, // 二类卖点向上偏移价格的0.15%
[TRADE_POINT_TYPE.SELL3]: 0 // 三类卖点向上偏移价格的0.1%
};
// 对于高价格标的如BTC,设置零偏移,完全不影响价格显示
const USE_FIXED_OFFSET = true; // 是否使用固定偏移而非百分比
const PRICE_FIXED_OFFSET = {
[TRADE_POINT_TYPE.BUY1]: 0, // 一类买点零偏移
[TRADE_POINT_TYPE.BUY2]: 0, // 二类买点零偏移
[TRADE_POINT_TYPE.BUY3]: 0, // 三类买点零偏移
[TRADE_POINT_TYPE.SELL1]: 0, // 一类卖点零偏移
[TRADE_POINT_TYPE.SELL2]: 0, // 二类卖点零偏移
[TRADE_POINT_TYPE.SELL3]: 0 // 三类卖点零偏移
};
// 同一时间点的标记堆叠间距系数
const STACK_OFFSET_FACTOR = 5; // 增加堆叠标记的间距
// 添加CSS样式定义买卖点标记的样式
const styleElement = document.createElement('style');
styleElement.textContent = `
.point-tooltip {
position: absolute;
background: rgba(40, 40, 40, 0.9);
color: white;
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
z-index: 1000;
pointer-events: none;
max-width: 300px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
display: none;
}
.buy-point {
color: #ff1744;
font-weight: bold;
}
.sell-point {
color: #00e676;
font-weight: bold;
}
.buy-marker {
background-color: #ff1744;
border: 2px solid white;
}
.sell-marker {
background-color: #00e676;
border: 2px solid white;
}
`;
document.head.appendChild(styleElement);
// 添加买卖点悬浮提示元素
const tooltipElement = document.createElement('div');
tooltipElement.className = 'point-tooltip';
document.body.appendChild(tooltipElement);
// 助手函数:转换UTC时间到所选时区
function convertToTimezone(utcDate, timezone) {
return new Date(utcDate).toLocaleString('zh-CN', {
timeZone: timezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
// 获取UTC时间戳(秒)
function getTimestamp(dateStr) {
return new Date(dateStr).getTime() / 1000;
}
// 获取时区偏移量(小时)
function getTimezoneOffset(timezone) {
// 手动定义已知时区的偏移量
const offsets = {
'UTC': 0,
'Asia/Shanghai': 8,
'America/New_York': -4, // 夏令时可能是-4,冬令时是-5
'Europe/London': 0, // 夏令时可能是+1,冬令时是0
'Europe/Berlin': 1, // 夏令时可能是+2,冬令时是+1
'Asia/Tokyo': 9
};
return offsets[timezone] || 0;
}
// 从日期字符串获取时间戳,应用时区偏移
function getAdjustedTimestamp(dateStr) {
// 基础时间戳(秒)- 直接使用UTC时间
return Math.floor(new Date(dateStr).getTime() / 1000);
}
// 添加时区选择器变更事件
$('#timezone').change(function() {
if (currentData) {
// 重新渲染图表和数据表以使用新的时区
initTradingView($('#symbol').val(), $('#timeframe').val());
updateTables(currentData);
}
});
// 添加MACD复选框变更事件
$('#showMacd').change(function() {
updateChartDisplay();
});
// 添加原始K线复选框变更事件
$('#showOriginalKline').change(function() {
updateChartDisplay();
});
// 添加笔复选框变更事件
$('#showMainBi').change(function() {
updateChartDisplay();
});
// 添加线段复选框变更事件
$('#showMainSeg').change(function() {
updateChartDisplay();
});
// 添加中枢复选框变更事件
$('#showMainZs').change(function() {
updateChartDisplay();
});
// 添加未完成中枢复选框变更事件
$('#showMainUncompletedZs').change(function() {
updateChartDisplay();
});
// 添加买卖点复选框变更事件
$('#showElementBi').change(function() {
updateChartDisplay();
});
// 添加线段复选框变更事件
$('#showElementSeg').change(function() {
updateChartDisplay();
});
// 添加中枢复选框变更事件
$('#showElementZs').change(function() {
updateChartDisplay();
});
// 添加未完成中枢复选框变更事件
$('#showElementUncompletedZs').change(function() {
updateChartDisplay();
});
// 添加买卖点复选框变更事件
$('#showTradePoints').change(function() {
refreshChart(currentData);
});
// 添加分型类型复选框变更事件
$('#showKlcFxType').change(function() {
updateChartDisplay();
});
// 当选择不同的元素时间周期时
$('#elementTimeframe').change(function() {
const elementTimeframe = $(this).val();
const mainTimeframe = $('#timeframe').val();
// 检查选择的元素时间周期是否小于等于主周期
if (compareTimeframes(elementTimeframe, mainTimeframe) > 0) {
alert('元素时间周期必须小于或等于主图表时间周期。');
setSmallerOrEqualTimeframe(); // 重置为最大的小于等于时间周期
return;
}
console.log(`当前选择的元素时间周期: ${elementTimeframe}`);
// 设置API请求参数,只获取元素数据(笔、线段、中枢)
const params = {
symbol: $('#symbol').val(),
timeframe: mainTimeframe,
element_timeframe: elementTimeframe,
timezone: $('#timezone').val(),
elements_only: true // 只获取元素数据
};
// 获取并正确处理时间范围
const startTimeVal = $('#start_time').val();
const endTimeVal = $('#end_time').val();
// 如果有设置开始和结束时间,添加到参数
if (startTimeVal) {
try {
params.start_time = new Date(startTimeVal).getTime();
console.log(`设置起始时间: ${startTimeVal} => ${params.start_time}`);
} catch (e) {
console.error('无效的开始时间格式:', startTimeVal, e);
}
}
if (endTimeVal) {
try {
params.end_time = new Date(endTimeVal).getTime();
console.log(`设置结束时间: ${endTimeVal} => ${params.end_time}`);
} catch (e) {
console.error('无效的结束时间格式:', endTimeVal, e);
}
}
// 显示加载指示器
$('#loadingIndicator').show();
console.log('发送API请求获取元素数据:', params);
// 调用API获取数据
$.getJSON('/api/analyze', params, function(data) {
// 隐藏加载指示器
$('#loadingIndicator').hide();
// 检查是否有错误
if (data.error) {
alert('错误: ' + data.error);
console.error('API返回错误:', data.error);
return;
}
console.log('API返回数据:', data);
// 使用现有K线数据,添加新的元素数据
if (currentData) {
// 保存主周期的K线和MACD数据
const originalKlineData = currentData.original_kline_data || currentData.kline_data;
const originalMacd = currentData.original_macd || currentData.macd;
// 首次保存原始数据
if (!currentData.original_kline_data) {
currentData.original_kline_data = originalKlineData;
}
if (!currentData.original_macd) {
currentData.original_macd = originalMacd;
}
// 更新当前数据对象,但保留原始K线和指标数据
currentData.element_timeframe = data.element_timeframe;
currentData.element_bi_list = data.element_bi_list;
currentData.element_seg_list = data.element_seg_list;
currentData.element_zs_list = data.element_zs_list;
// 确保K线和MACD数据始终使用主周期数据
currentData.kline_data = originalKlineData;
currentData.macd = originalMacd;
console.log(`更新元素数据成功,元素周期: ${data.element_timeframe}`);
console.log(`笔: ${data.element_bi_list ? data.element_bi_list.length : 0}条`);
console.log(`线段: ${data.element_seg_list ? data.element_seg_list.length : 0}条`);
console.log(`中枢: ${data.element_zs_list ? data.element_zs_list.length : 0}条`);
// 只重绘分形元素图表(笔、线段、中枢)
redrawFractalElements();
// 更新分形元素的表格数据
updateFractalTables();
} else {
console.error('当前没有主周期数据,无法更新元素数据');
alert('请先获取主周期数据');
}
}).fail(function(jqxhr, textStatus, error) {
// 隐藏加载指示器
$('#loadingIndicator').hide();
console.error('API请求失败:', textStatus, error);
console.error('错误详情:', jqxhr.responseText);
alert('请求失败: ' + textStatus + ', ' + error);
});
});
// 比较两个时间周期的大小
function compareTimeframes(tf1, tf2) {
const tfValues = {
'1m': 1,
'3m': 3,
'5m': 5,
'15m': 15,
'30m': 30,
'1h': 60,
'2h': 120,
'4h': 240,
'6h': 360,
'8h': 480,
'12h': 720,
'1d': 1440,
'3d': 4320,
'1w': 10080,
'1M': 43200
};
return tfValues[tf1] - tfValues[tf2];
}
// 设置比主周期小的最大周期
function setSmallestLargerTimeframe(mainTimeframe) {
const timeframes = ['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '1d', '3d', '1w', '1M'];
const mainIndex = timeframes.indexOf(mainTimeframe);
if (mainIndex > 0) {
$('#elementTimeframe').val(timeframes[mainIndex - 1]);
} else {
$('#elementTimeframe').val(timeframes[0]);
}
}
// 设置小于或等于主周期的时间周期
function setSmallerOrEqualTimeframe(mainTimeframe) {
const timeframes = ['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '1d', '3d', '1w', '1M'];
const mainIndex = timeframes.indexOf(mainTimeframe);
// 默认选择相同的时间周期
$('#elementTimeframe').val(mainTimeframe);
}
// 当主时间周期变更时,确保分形元素时间周期正确
$('#timeframe').change(function() {
const mainTimeframe = $(this).val();
const elementTimeframe = $('#elementTimeframe').val();
if (compareTimeframes(elementTimeframe, mainTimeframe) > 0) {
setSmallerOrEqualTimeframe(mainTimeframe);
}
});
function updateChartDisplay() {
if (currentData) {
// 保存当前的可见逻辑范围
let logicalRange = null;
let visibleRange = null;
if (tvWidget && tvWidget.mainChart) {
// 优先使用逻辑范围,这样在缩放级别上更准确
logicalRange = tvWidget.mainChart.timeScale().getVisibleLogicalRange();
// 备用方案,获取实际时间戳范围
visibleRange = tvWidget.mainChart.timeScale().getVisibleRange();
}
console.log('更新图表显示');
console.log('- 显示K线:', $('#showOriginalKline').is(':checked'));
console.log('- 显示笔:', $('#showMainBi').is(':checked'));
console.log('- 显示线段:', $('#showMainSeg').is(':checked'));
console.log('- 显示中枢:', $('#showMainZs').is(':checked'));
console.log('- 显示未完成中枢:', $('#showMainUncompletedZs').is(':checked'));
console.log('- 显示MACD:', $('#showMacd').is(':checked'));
console.log('- 显示成交量:', $('#showVolume').is(':checked'));
console.log('- 显示分型类型:', $('#showKlcFxType').is(':checked'));
console.log('- 显示小周期分型:', $('#showElementKlcFxType').is(':checked'));
console.log('- 显示买卖点:', $('#showTradePoints').is(':checked'));
console.log('- 显示原始K线:', $('#showOriginalKline').is(':checked'));
// 重新初始化图表,这将清除旧图形并重新绘制
initTradingView($('#symbol').val(), $('#timeframe').val());
// 如果有保存的范围,恢复它
setTimeout(() => {
if (tvWidget) {
if (logicalRange) {
// 应用保存的逻辑范围到所有图表
tvWidget.mainChart.timeScale().setVisibleLogicalRange(logicalRange);
tvWidget.volumeChart.timeScale().setVisibleLogicalRange(logicalRange);
if (tvWidget.macdChart) {
tvWidget.macdChart.timeScale().setVisibleLogicalRange(logicalRange);
}
} else if (visibleRange) {
// 如果没有逻辑范围,使用时间戳范围
tvWidget.mainChart.timeScale().setVisibleRange(visibleRange);
tvWidget.volumeChart.timeScale().setVisibleRange(visibleRange);
if (tvWidget.macdChart) {
tvWidget.macdChart.timeScale().setVisibleRange(visibleRange);
}
}
}
}, 100);
}
}
// 确保所有时间处理都使用UTC时间,包括表格数据显示
function formatTime(timeStr) {
if (!timeStr) return '';
try {
// 使用用户选择的时区
const timezone = $('#timezone').val();
const date = new Date(timeStr);
// 使用toLocaleString带时区参数
return date.toLocaleString('zh-CN', {
timeZone: timezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
} catch (e) {
console.error('时间格式化错误:', e, timeStr);
// 如果出错,尝试使用简单格式化
try {
return new Date(timeStr).toLocaleString();
} catch (e2) {
// 如果还是出错,返回原始字符串
return timeStr;
}
}
}
function formatDirection(direction) {
const dirText = direction === 1 ? '向上' : '向下';
const dirClass = direction === 1 ? 'direction-up' : 'direction-down';
return '<span class="' + dirClass + '">' + dirText + '</span>';
}
function formatPrice(price) {
return price !== null ? parseFloat(price).toFixed(2) : '';
}
function formatMacdValue(value) {
const numValue = parseFloat(value);
const valueClass = numValue >= 0 ? 'positive' : 'negative';
return '<span class="' + valueClass + '">' + numValue.toFixed(4) + '</span>';
}
function updateChart() {
// 只显示旋转加载图标
$('#refreshLoadingSpinner').show();
// 获取参数
const symbol = $('#symbol').val() || 'SOL/USDT:USDT';
const timeframe = $('#timeframe').val() || '5m';
const timezone = $('#timezone').val() || 'Asia/Shanghai';
const elementTimeframe = $('#elementTimeframe').val() || '1m';
// 如果symbol为空,不发送请求
if (!symbol) {
console.error('交易对不能为空');
$('#refreshLoadingSpinner').hide();
return;
}
console.log(`更新图表: symbol=${symbol}, timeframe=${timeframe}, elementTimeframe=${elementTimeframe}`);
// 获取开始和结束时间(如果已设置)
let startTimeMs = null;
let endTimeMs = null;
if ($('#start_time').val()) {
startTimeMs = new Date($('#start_time').val()).getTime();
}
if ($('#end_time').val()) {
endTimeMs = new Date($('#end_time').val()).getTime();
}
// 发送请求
$.ajax({
url: '/api/analyze',
data: {
symbol: symbol,
timeframe: timeframe,
timezone: timezone,
element_timeframe: elementTimeframe,
start_time: startTimeMs,
end_time: endTimeMs,
elements_only: false
},
success: function(data) {
// 隐藏加载图标
$('#refreshLoadingSpinner').hide();
// 保存当前数据
currentData = data;
// 刷新图表
refreshChart(data);
},
error: function(jqXHR, textStatus, errorThrown) {
// 隐藏加载图标
$('#refreshLoadingSpinner').hide();
// 显示错误信息
console.error('加载数据失败:', errorThrown);
alert('加载数据失败: ' + (jqXHR.responseJSON?.error || errorThrown));
}
});
}
// 初始化图表
function initTradingView(symbol, timeframe) {
try {
console.log('初始化图表:', symbol, timeframe);
// 清除图表容器
document.getElementById('tradingview_chart').innerHTML = '';
// 重置图表对象
tvWidget = {
mainChart: null,
volumeChart: null,
macdChart: null,
series: {
candleSeries: null,
lineSeries: null,
volumeSeries: null,
macdLineSeries: null,
signalLineSeries: null,
histogramSeries: null,
mainBiSeries: [],
mainSegSeries: [],
mainZsSeries: [],
mainUncompletedZsSeries: [],
elementBiSeries: [],
elementSegSeries: [],
elementZsSeries: [],
elementUncompletedZsSeries: [],
tradePointSeries: []
},
state: {
isInitialized: false,
visibleRange: null,
logicalRange: null
}
};
// 设置父容器样式
const container = document.getElementById('tradingview_chart');
container.style.position = 'relative';
container.style.width = '100%';
container.style.height = '100%';
// 是否显示MACD
const showMacd = $('#showMacd').is(':checked');
const showOriginalKline = $('#showOriginalKline').is(':checked');
// 创建主图容器
const mainChartContainer = document.createElement('div');
mainChartContainer.style.width = '100%';
mainChartContainer.style.position = 'absolute';
mainChartContainer.style.top = '0';
mainChartContainer.style.left = '0';
mainChartContainer.style.right = '0';
// 创建成交量副图容器
const volumeChartContainer = document.createElement('div');
volumeChartContainer.style.width = '100%';
volumeChartContainer.style.position = 'absolute';
volumeChartContainer.style.left = '0';
volumeChartContainer.style.right = '0';
volumeChartContainer.style.borderTop = '1px solid #e0e0e0';
// 如果需要显示MACD,创建MACD容器
let macdChartContainer = null;
if (showMacd) {
// 设置各图表高度
mainChartContainer.style.height = '60%';
volumeChartContainer.style.top = '60%';
volumeChartContainer.style.height = '20%';
macdChartContainer = document.createElement('div');
macdChartContainer.style.width = '100%';
macdChartContainer.style.height = '20%';
macdChartContainer.style.position = 'absolute';
macdChartContainer.style.top = '80%';
macdChartContainer.style.left = '0';
macdChartContainer.style.right = '0';
macdChartContainer.style.borderTop = '1px solid #e0e0e0';
} else {
// 不显示MACD时的高度
mainChartContainer.style.height = '75%';
volumeChartContainer.style.top = '75%';
volumeChartContainer.style.height = '25%';
}
container.appendChild(mainChartContainer);
container.appendChild(volumeChartContainer);
if (showMacd) container.appendChild(macdChartContainer);
// 防止同步过程中的无限循环
let syncInProgress = false;
// 创建统一的图表选项
const createChartOptions = (showTimeScale = true) => ({
width: mainChartContainer.clientWidth,
height: mainChartContainer.clientHeight,
layout: {
background: { color: '#ffffff' },
textColor: '#333',
},
grid: {
vertLines: { color: '#f0f0f0' },
horzLines: { color: '#f0f0f0' },
},
crosshair: {
mode: LightweightCharts.CrosshairMode.Normal,
},
rightPriceScale: {
borderColor: '#ddd',
},
timeScale: {
timeVisible: true,
secondsVisible: false,
visible: showTimeScale,
borderColor: '#ddd',
barSpacing: 10,
tickMarkFormatter: (time) => {
const selectedTimezone = $('#timezone').val();
try {
return new Date(time * 1000).toLocaleString('zh-CN', {
timeZone: selectedTimezone,
month: 'numeric',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
} catch (e) {
console.error('时间格式化错误:', e);
// 如果时区格式化失败,返回简单格式
return new Date(time * 1000).toLocaleString();
}
},
},
});
// 创建主图表
const mainChart = LightweightCharts.createChart(mainChartContainer, createChartOptions(true));
// 创建成交量图表 - 只显示底部的时间轴
const volumeChart = LightweightCharts.createChart(volumeChartContainer, createChartOptions(false));
// 创建MACD图表(如果需要)
let macdChart = null;
if (showMacd) {
macdChart = LightweightCharts.createChart(macdChartContainer, createChartOptions(false));
}
// 转换K线数据 - 根据选中的周期使用主周期或小周期数据
let candles;
const useElementPeriod = $('#elementPeriodKline').is(':checked') &&
currentData.element_timeframe &&
currentData.element_kline_data;
// 输出K线周期选择状态
console.log('K线周期选择:', useElementPeriod ? '小周期' : '主周期');
if (useElementPeriod) {
// 使用小周期K线数据
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 {
// 使用主周期K线数据
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),
};
});
}
// 创建蜡烛图系列并设置数据
if (showOriginalKline) {
const candleSeries = mainChart.addCandlestickSeries({
upColor: '#dc3545',
downColor: '#28a745',
borderVisible: false,
wickUpColor: '#dc3545',
wickDownColor: '#28a745',
});
candleSeries.setData(candles);
tvWidget.series.candleSeries = candleSeries;
} else {
// 如果不显示原始K线,只显示线图
const lineSeries = mainChart.addLineSeries({
color: '#2962FF',
lineWidth: 2,
crosshairMarkerVisible: true,
lastValueVisible: true,
priceLineVisible: true,
});
// 提取收盘价数据
const lineData = candles.map(candle => ({
time: candle.time,
value: candle.close
}));
lineSeries.setData(lineData);
tvWidget.series.lineSeries = lineSeries;
}
// 转换成交量数据 - 始终使用主K线周期数据
const volumes = useElementPeriod ?
currentData.element_kline_data.map(kline => {
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
return {
time: timestamp,
value: parseFloat(kline.volume),
color: parseFloat(kline.close) >= parseFloat(kline.open) ? 'rgba(220, 53, 69, 0.5)' : 'rgba(40, 167, 69, 0.5)',
};
}) :
currentData.kline_data.map(kline => {
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
return {
time: timestamp,
value: parseFloat(kline.volume),
color: parseFloat(kline.close) >= parseFloat(kline.open) ? 'rgba(220, 53, 69, 0.5)' : 'rgba(40, 167, 69, 0.5)',
};
});
// 添加成交量图表
const volumeSeries = volumeChart.addHistogramSeries({
color: '#26a69a',
priceFormat: {
type: 'volume',
},
title: '成交量',
});
volumeSeries.setData(volumes);
tvWidget.series.volumeSeries = volumeSeries;
// 添加MACD图表 - 始终使用主K线周期的MACD数据
if (showMacd && currentData.macd) {
// 创建MACD线
const macdLineSeries = macdChart.addLineSeries({
color: '#2962FF',
lineWidth: 1,
title: 'MACD',
lastValueVisible: true,
});
// 创建信号线
const signalLineSeries = macdChart.addLineSeries({
color: '#FF6B6B',
lineWidth: 1,
title: 'Signal',
lastValueVisible: true,
});
// 创建直方图
const histogramSeries = macdChart.addHistogramSeries({
color: '#26a69a',
title: 'Histogram',
priceFormat: {
type: 'price',
precision: 4,
},
});
// 提取MACD数据
const macdData = [];
const signalData = [];
const histogramData = [];
for (let i = 0; i < currentData.kline_data.length; i++) {
const kline = currentData.kline_data[i];
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
if (currentData.macd && currentData.macd.macd && currentData.macd.macd[i] !== undefined) {
macdData.push({
time: timestamp,
value: currentData.macd.macd[i]
});
signalData.push({
time: timestamp,
value: currentData.macd.signal[i]
});
// 设置直方图颜色
const histValue = currentData.macd.histogram[i];
histogramData.push({
time: timestamp,
value: histValue,
color: histValue >= 0 ? 'rgba(220, 53, 69, 0.5)' : 'rgba(40, 167, 69, 0.5)'
});
}
}
macdLineSeries.setData(macdData);
signalLineSeries.setData(signalData);
histogramSeries.setData(histogramData);
tvWidget.series.macdLineSeries = macdLineSeries;
tvWidget.series.signalLineSeries = signalLineSeries;
tvWidget.series.histogramSeries = histogramSeries;
}
// 实现三图联动滚动
// 同步图表的时间范围
function syncCharts(sourceChart, sourceContainer) {
// 防止无限循环
if (syncInProgress) return;
syncInProgress = true;
try {
if (sourceChart) {
const logicalRange = sourceChart.timeScale().getVisibleLogicalRange();
if (logicalRange) {
if (sourceChart !== mainChart) {
mainChart.timeScale().setVisibleLogicalRange(logicalRange);
}
if (sourceChart !== volumeChart) {
volumeChart.timeScale().setVisibleLogicalRange(logicalRange);
}
if (showMacd && macdChart && sourceChart !== macdChart) {
macdChart.timeScale().setVisibleLogicalRange(logicalRange);
}
}
}
} catch (e) {
console.error('同步图表出错:', e);
}
setTimeout(() => { syncInProgress = false; }, 50);
}
// 为每个图表添加事件监听
const addChartSyncEvents = (chartContainer, chart) => {
// 鼠标滚轮事件
chartContainer.addEventListener('wheel', () => {
syncCharts(chart, chartContainer);
});
// 拖动事件
let isDragging = false;
chartContainer.addEventListener('mousedown', () => { isDragging = true; });
chartContainer.addEventListener('mousemove', () => {
if (isDragging) {
syncCharts(chart, chartContainer);
}
});
chartContainer.addEventListener('mouseup', () => { isDragging = false; });
chartContainer.addEventListener('mouseleave', () => { isDragging = false; });
// 触摸事件(移动设备)
chartContainer.addEventListener('touchmove', () => {
syncCharts(chart, chartContainer);
});
};
// 添加事件监听
addChartSyncEvents(mainChartContainer, mainChart);
addChartSyncEvents(volumeChartContainer, volumeChart);
if (showMacd && macdChart) {
addChartSyncEvents(macdChartContainer, macdChart);
}
// 全局鼠标抬起事件
document.addEventListener('mouseup', () => {
isDragging = false;
});
// 窗口大小变化时重绘图表
window.addEventListener('resize', () => {
// 调整主图大小
mainChart.applyOptions({
width: mainChartContainer.clientWidth,
height: mainChartContainer.clientHeight
});
// 调整成交量图大小
volumeChart.applyOptions({
width: volumeChartContainer.clientWidth,
height: volumeChartContainer.clientHeight
});
// 调整MACD图大小
if (showMacd && macdChart) {
macdChart.applyOptions({
width: macdChartContainer.clientWidth,
height: macdChartContainer.clientHeight
});
}
// 重新同步
setTimeout(() => syncCharts(mainChart, null), 100);
});
// ------------------------------
// 分形元素(笔、线段、中枢)绘制
// ------------------------------
// 显示笔的绘制 - 分别处理主周期和次周期
if ($('#showMainBi').is(':checked') || $('#showElementBi').is(':checked')) {
console.log('绘制笔 - 已启用');
let biLines = [];
// 主周期笔
if ($('#showMainBi').is(':checked') && currentData.bi_list && currentData.bi_list.length > 0) {
console.log(`绘制主周期笔数据,共${currentData.bi_list.length}条`);
// 清空已有的主周期笔系列
tvWidget.series.mainBiSeries = [];
// 遍历处理每个笔
currentData.bi_list.forEach(function(bi) {
try {
// 直接使用UTC时间戳(秒)
const startTime = Math.floor(new Date(bi.start_time).getTime() / 1000);
const endTime = Math.floor(new Date(bi.end_time).getTime() / 1000);
if (isNaN(startTime) || isNaN(endTime)) {
console.error('主周期笔时间转换错误:', bi.start_time, bi.end_time);
return;
}
const startPrice = parseFloat(bi.start_price);
const endPrice = parseFloat(bi.end_price);
if (isNaN(startPrice) || isNaN(endPrice)) {
console.error('主周期笔价格转换错误:', bi.start_price, bi.end_price);
return;
}
// 添加线段
biLines.push({
startTime: startTime,
endTime: endTime,
startPrice: startPrice,
endPrice: endPrice,
color: bi.direction === 1 ? '#dc3545' : '#28a745', // 主周期笔颜色
lineWidth: 1,
});
// 添加到图表对象
tvWidget.series.mainBiSeries.push({
time: startTime,
value: startPrice,
color: bi.direction === 1 ? '#dc3545' : '#28a745',
lineWidth: 1
});
} catch (e) {
console.error('主周期笔处理出错:', e);
}
});
}
// 次周期笔
if ($('#showElementBi').is(':checked') && currentData.element_bi_list && currentData.element_bi_list.length > 0) {
console.log(`绘制次周期笔数据,共${currentData.element_bi_list.length}条`);
currentData.element_bi_list.forEach(function(bi) {
try {
// 直接使用UTC时间戳(秒)
const startTime = Math.floor(new Date(bi.start_time).getTime() / 1000);
const endTime = Math.floor(new Date(bi.end_time).getTime() / 1000);
if (isNaN(startTime) || isNaN(endTime)) {
console.error('次周期笔时间转换错误:', bi.start_time, bi.end_time);
return;
}
const startPrice = parseFloat(bi.start_price);
const endPrice = parseFloat(bi.end_price);
if (isNaN(startPrice) || isNaN(endPrice)) {
console.error('次周期笔价格转换错误:', bi.start_price, bi.end_price);
return;
}
// 添加线段
biLines.push({
startTime: startTime,
endTime: endTime,
startPrice: startPrice,
endPrice: endPrice,
color: bi.direction === 1 ? '#9c27b0' : '#673ab7', // 次周期笔颜色
lineWidth: 1,
});
// 添加到图表对象
tvWidget.series.elementBiSeries.push({
time: startTime,
value: startPrice,
color: bi.direction === 1 ? '#9c27b0' : '#673ab7',
lineWidth: 1
});
} catch (e) {
console.error('次周期笔处理出错:', e);
}
});
}
// 添加所有笔到图表
biLines.forEach(line => {
const lineSeries = mainChart.addLineSeries({
color: line.color,
lineWidth: line.lineWidth,
lastValueVisible: false,
priceLineVisible: false,
});
lineSeries.setData([
{ time: line.startTime, value: line.startPrice },
{ time: line.endTime, value: line.endPrice }
]);
});
} else {
console.log('绘制笔 - 已禁用');
}
// 显示线段的绘制 - 分别处理主周期和次周期
if ($('#showMainSeg').is(':checked') || $('#showElementSeg').is(':checked')) {
console.log('绘制线段 - 已启用');
let segLines = [];
// 主周期线段
if ($('#showMainSeg').is(':checked') && currentData.seg_list && currentData.seg_list.length > 0) {
console.log(`绘制主周期线段数据,共${currentData.seg_list.length}条`);
currentData.seg_list.forEach(function(seg) {
try {
// 直接使用UTC时间戳(秒)
const startTime = Math.floor(new Date(seg.start_time).getTime() / 1000);
const endTime = Math.floor(new Date(seg.end_time).getTime() / 1000);
if (isNaN(startTime) || isNaN(endTime)) {
console.error('主周期线段时间转换错误:', seg.start_time, seg.end_time);
return;
}
const startPrice = parseFloat(seg.start_price);
const endPrice = parseFloat(seg.end_price);
if (isNaN(startPrice) || isNaN(endPrice)) {
console.error('主周期线段价格转换错误:', seg.start_price, seg.end_price);
return;
}
// 添加线段
segLines.push({
startTime: startTime,
endTime: endTime,
startPrice: startPrice,
endPrice: endPrice,
color: seg.direction === 1 ? '#FF6B6B' : '#4CAF50', // 主周期线段颜色
lineWidth: 2,
});
// 添加到图表对象
tvWidget.series.mainSegSeries.push({
time: startTime,
value: startPrice,
color: seg.direction === 1 ? '#FF6B6B' : '#4CAF50',
lineWidth: 2
});
} catch (e) {
console.error('主周期线段处理出错:', e);
}
});
}
// 次周期线段
if ($('#showElementSeg').is(':checked') && currentData.element_seg_list && currentData.element_seg_list.length > 0) {
console.log(`绘制次周期线段数据,共${currentData.element_seg_list.length}条`);
currentData.element_seg_list.forEach(function(seg) {
try {
// 直接使用UTC时间戳(秒)
const startTime = Math.floor(new Date(seg.start_time).getTime() / 1000);
const endTime = Math.floor(new Date(seg.end_time).getTime() / 1000);
if (isNaN(startTime) || isNaN(endTime)) {
console.error('次周期线段时间转换错误:', seg.start_time, seg.end_time);
return;
}
const startPrice = parseFloat(seg.start_price);
const endPrice = parseFloat(seg.end_price);
if (isNaN(startPrice) || isNaN(endPrice)) {
console.error('次周期线段价格转换错误:', seg.start_price, seg.end_price);
return;
}
// 添加线段
segLines.push({
startTime: startTime,
endTime: endTime,
startPrice: startPrice,
endPrice: endPrice,
color: seg.direction === 1 ? '#673ab7' : '#9c27b0', // 次周期线段颜色
lineWidth: 2,
});
// 添加到图表对象
tvWidget.series.elementSegSeries.push({
time: startTime,
value: startPrice,
color: seg.direction === 1 ? '#673ab7' : '#9c27b0',
lineWidth: 2
});
} catch (e) {
console.error('次周期线段处理出错:', e);
}
});
}
// 添加所有线段到图表
segLines.forEach(line => {
const lineSeries = mainChart.addLineSeries({
color: line.color,
lineWidth: line.lineWidth,
lastValueVisible: false,
priceLineVisible: false,
});
lineSeries.setData([
{ time: line.startTime, value: line.startPrice },
{ time: line.endTime, value: line.endPrice }
]);
});
} else {
console.log('绘制线段 - 已禁用');
}
// 显示中枢的绘制 - 分别处理主周期和次周期
if ($('#showMainZs').is(':checked') || $('#showElementZs').is(':checked')) {
console.log('绘制中枢 - 已启用');
// 主周期中枢
if ($('#showMainZs').is(':checked') && currentData.zs_list && currentData.zs_list.length > 0) {
console.log(`绘制主周期中枢数据,共${currentData.zs_list.length}条`);
currentData.zs_list.forEach(function(zs) {
try {
// 直接使用UTC时间戳(秒)
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
const endTime = Math.floor(new Date(zs.end_time).getTime() / 1000);
if (isNaN(startTime) || isNaN(endTime)) {
console.error('主周期中枢时间转换错误:', zs.start_time, zs.end_time);
return;
}
const zg = parseFloat(zs.zg); // 中枢上沿
const zd = parseFloat(zs.zd); // 中枢下沿
if (isNaN(zg) || isNaN(zd)) {
console.error('主周期中枢价格转换错误:', zs.zg, zs.zd);
return;
}
// 创建中枢上边界
const topSeries = mainChart.addLineSeries({
color: '#F1C40F', // 主周期中枢颜色
lineWidth: 1,
lastValueVisible: false,
priceLineVisible: false,
});
topSeries.setData([
{ time: startTime, value: zg },
{ time: endTime, value: zg }
]);
// 为下边界创建另一条线
const bottomSeries = mainChart.addLineSeries({
color: '#F1C40F', // 主周期中枢颜色
lineWidth: 1,
lastValueVisible: false,
priceLineVisible: false,
});
bottomSeries.setData([
{ time: startTime, value: zd },
{ time: endTime, value: zd }
]);
// 添加左边界
const leftSeries = mainChart.addLineSeries({
color: '#F1C40F', // 主周期中枢颜色
lineWidth: 1,
lastValueVisible: false,
priceLineVisible: false,
});
leftSeries.setData([
{ time: startTime, value: zd },
{ time: startTime, value: zg }
]);
// 添加右边界
const rightSeries = mainChart.addLineSeries({
color: '#F1C40F', // 主周期中枢颜色
lineWidth: 1,
lastValueVisible: false,
priceLineVisible: false,
});
rightSeries.setData([
{ time: endTime, value: zd },
{ time: endTime, value: zg }
]);
// 添加到图表对象
tvWidget.series.mainZsSeries.push({
time: startTime,
value: zg,
color: '#F1C40F',
lineWidth: 1
});
tvWidget.series.mainZsSeries.push({
time: endTime,
value: zg,
color: '#F1C40F',
lineWidth: 1
});
tvWidget.series.mainZsSeries.push({
time: startTime,
value: zd,
color: '#F1C40F',
lineWidth: 1
});
tvWidget.series.mainZsSeries.push({
time: endTime,
value: zd,
color: '#F1C40F',
lineWidth: 1
});
} catch (e) {
console.error('主周期中枢处理出错:', e);
}
});
}
// 次周期中枢
if ($('#showElementZs').is(':checked') && currentData.element_zs_list && currentData.element_zs_list.length > 0) {
console.log(`绘制次周期中枢数据,共${currentData.element_zs_list.length}条`);
currentData.element_zs_list.forEach(function(zs) {
try {
// 直接使用UTC时间戳(秒)
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
const endTime = Math.floor(new Date(zs.end_time).getTime() / 1000);
if (isNaN(startTime) || isNaN(endTime)) {
console.error('次周期中枢时间转换错误:', zs.start_time, zs.end_time);
return;
}
const zg = parseFloat(zs.zg); // 中枢上沿
const zd = parseFloat(zs.zd); // 中枢下沿
if (isNaN(zg) || isNaN(zd)) {
console.error('次周期中枢价格转换错误:', zs.zg, zs.zd);
return;
}
// 创建中枢上边界
const topSeries = mainChart.addLineSeries({
color: '#3f51b5', // 次周期中枢颜色
lineWidth: 1,
lastValueVisible: false,
priceLineVisible: false,
});
topSeries.setData([
{ time: startTime, value: zg },
{ time: endTime, value: zg }
]);
// 为下边界创建另一条线
const bottomSeries = mainChart.addLineSeries({
color: '#3f51b5', // 次周期中枢颜色
lineWidth: 1,
lastValueVisible: false,
priceLineVisible: false,
});
bottomSeries.setData([
{ time: startTime, value: zd },
{ time: endTime, value: zd }
]);
// 添加左边界
const leftSeries = mainChart.addLineSeries({
color: '#3f51b5', // 次周期中枢颜色
lineWidth: 1,
lastValueVisible: false,
priceLineVisible: false,
});
leftSeries.setData([
{ time: startTime, value: zd },
{ time: startTime, value: zg }
]);
// 添加右边界
const rightSeries = mainChart.addLineSeries({
color: '#3f51b5', // 次周期中枢颜色
lineWidth: 1,
lastValueVisible: false,
priceLineVisible: false,
});
rightSeries.setData([
{ time: endTime, value: zd },
{ time: endTime, value: zg }
]);
// 添加到图表对象
tvWidget.series.elementZsSeries.push({
time: startTime,
value: zg,
color: '#3f51b5',
lineWidth: 1
});
tvWidget.series.elementZsSeries.push({
time: endTime,
value: zg,
color: '#3f51b5',
lineWidth: 1
});
tvWidget.series.elementZsSeries.push({
time: startTime,
value: zd,
color: '#3f51b5',
lineWidth: 1
});
tvWidget.series.elementZsSeries.push({
time: endTime,
value: zd,
color: '#3f51b5',
lineWidth: 1
});
} catch (e) {
console.error('次周期中枢处理出错:', e);
}
});
}
} else {
console.log('绘制中枢 - 已禁用');
}
// 显示未完成中枢 - 分别处理主周期和次周期
if ($('#showMainUncompletedZs').is(':checked') || $('#showElementUncompletedZs').is(':checked')) {
console.log('绘制未完成中枢 - 已启用');
// 主周期未完成中枢
if ($('#showMainUncompletedZs').is(':checked') && currentData.uncompleted_zs_list && currentData.uncompleted_zs_list.length > 0) {
console.log(`绘制主周期未完成中枢数据,共${currentData.uncompleted_zs_list.length}条`);
currentData.uncompleted_zs_list.forEach(function(zs) {
try {
// 直接使用UTC时间戳(秒)
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
// 未完成中枢的结束时间设为当前K线的最后时间
const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
if (isNaN(startTime) || isNaN(endTime)) {
console.error('主周期未完成中枢时间转换错误:', zs.start_time);
return;
}
const zg = parseFloat(zs.zg); // 中枢上沿
const zd = parseFloat(zs.zd); // 中枢下沿
if (isNaN(zg) || isNaN(zd)) {
console.error('主周期未完成中枢价格转换错误:', zs.zg, zs.zd);
return;
}
// 创建未完成中枢上边界
const topSeries = mainChart.addLineSeries({
color: '#F1C40F', // 主周期中枢颜色
lineWidth: 1,
lineStyle: 2, // 虚线
lastValueVisible: false,
priceLineVisible: false,
});
topSeries.setData([
{ time: startTime, value: zg },
{ time: endTime, value: zg }
]);
// 为下边界创建另一条线
const bottomSeries = mainChart.addLineSeries({
color: '#F1C40F', // 主周期中枢颜色
lineWidth: 1,
lineStyle: 2, // 虚线
lastValueVisible: false,
priceLineVisible: false,
});
bottomSeries.setData([
{ time: startTime, value: zd },
{ time: endTime, value: zd }
]);
// 添加左边界
const leftSeries = mainChart.addLineSeries({
color: '#F1C40F', // 主周期中枢颜色
lineWidth: 1,
lineStyle: 2, // 虚线
lastValueVisible: false,
priceLineVisible: false,
});
leftSeries.setData([
{ time: startTime, value: zd },
{ time: startTime, value: zg }
]);
// 添加一个标记,标识这是未完成中枢
const markerSeries = mainChart.addLineSeries({
lastValueVisible: false,
priceLineVisible: false,
});
markerSeries.setMarkers([
{
time: startTime,
position: 'aboveBar',
color: '#F1C40F',
shape: 'circle',
text: '未完',
size: 1
}
]);
// 添加到图表对象
tvWidget.series.mainUncompletedZsSeries.push({
time: startTime,
value: zg,
color: '#F1C40F',
lineWidth: 1,
lineStyle: 2
});
tvWidget.series.mainUncompletedZsSeries.push({
time: endTime,
value: zg,
color: '#F1C40F',
lineWidth: 1,
lineStyle: 2
});
tvWidget.series.mainUncompletedZsSeries.push({
time: startTime,
value: zd,
color: '#F1C40F',
lineWidth: 1,
lineStyle: 2
});
tvWidget.series.mainUncompletedZsSeries.push({
time: endTime,
value: zd,
color: '#F1C40F',
lineWidth: 1,
lineStyle: 2
});
} catch (e) {
console.error('主周期未完成中枢处理出错:', e);
}
});
}
// 次周期未完成中枢
if ($('#showElementUncompletedZs').is(':checked') && currentData.element_uncompleted_zs_list && currentData.element_uncompleted_zs_list.length > 0) {
console.log(`绘制次周期未完成中枢数据,共${currentData.element_uncompleted_zs_list.length}条`);
currentData.element_uncompleted_zs_list.forEach(function(zs) {
try {
// 直接使用UTC时间戳(秒)
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
// 未完成中枢的结束时间设为当前K线的最后时间
const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
if (isNaN(startTime) || isNaN(endTime)) {
console.error('次周期未完成中枢时间转换错误:', zs.start_time);
return;
}
const zg = parseFloat(zs.zg); // 中枢上沿
const zd = parseFloat(zs.zd); // 中枢下沿
if (isNaN(zg) || isNaN(zd)) {
console.error('次周期未完成中枢价格转换错误:', zs.zg, zs.zd);
return;
}
// 创建未完成中枢上边界
const topSeries = mainChart.addLineSeries({
color: '#3f51b5', // 次周期中枢颜色
lineWidth: 1,
lineStyle: 2, // 虚线
lastValueVisible: false,
priceLineVisible: false,
});
topSeries.setData([
{ time: startTime, value: zg },
{ time: endTime, value: zg }
]);
// 为下边界创建另一条线
const bottomSeries = mainChart.addLineSeries({
color: '#3f51b5', // 次周期中枢颜色
lineWidth: 1,
lineStyle: 2, // 虚线
lastValueVisible: false,
priceLineVisible: false,
});
bottomSeries.setData([
{ time: startTime, value: zd },
{ time: endTime, value: zd }
]);
// 添加左边界
const leftSeries = mainChart.addLineSeries({
color: '#3f51b5', // 次周期中枢颜色
lineWidth: 1,
lineStyle: 2, // 虚线
lastValueVisible: false,
priceLineVisible: false,
});
leftSeries.setData([
{ time: startTime, value: zd },
{ time: startTime, value: zg }
]);
// 添加一个标记,标识这是未完成中枢
const markerSeries = mainChart.addLineSeries({
lastValueVisible: false,
priceLineVisible: false,
});
markerSeries.setMarkers([
{
time: startTime,
position: 'aboveBar',
color: '#3f51b5',
shape: 'circle',
text: '未完',
size: 1
}
]);
// 添加到图表对象
tvWidget.series.elementUncompletedZsSeries.push({
time: startTime,
value: zg,
color: '#3f51b5',
lineWidth: 1,
lineStyle: 2
});
tvWidget.series.elementUncompletedZsSeries.push({
time: endTime,
value: zg,
color: '#3f51b5',
lineWidth: 1,
lineStyle: 2
});
tvWidget.series.elementUncompletedZsSeries.push({
time: startTime,
value: zd,
color: '#3f51b5',
lineWidth: 1,
lineStyle: 2
});
tvWidget.series.elementUncompletedZsSeries.push({
time: endTime,
value: zd,
color: '#3f51b5',
lineWidth: 1,
lineStyle: 2
});
} catch (e) {
console.error('次周期未完成中枢处理出错:', e);
}
});
}
} else {
console.log('绘制未完成中枢 - 已禁用');
}
// 添加买卖点标记
if ($('#showTradePoints').is(':checked')) {
console.log('绘制买卖点 - 已启用');
// 优先使用小周期数据,如果不存在则使用主周期数据
const tradePointsData = currentData.element_trade_points || currentData.trade_points;
console.log(`绘制${currentData.element_trade_points ? '元素周期' : '主周期'}买卖点数据,共${tradePointsData ? tradePointsData.length : 0}条`);
// 调试信息 - 输出完整的买卖点数据
if (tradePointsData && tradePointsData.length > 0) {
console.log("买卖点数据样例:", tradePointsData[0]);
// 检查数据格式,如果time不是标准格式,进行格式化处理
const checkDataFormat = () => {
for (let i = 0; i < tradePointsData.length; i++) {
if (tradePointsData[i].time) {
// 确保时间是标准格式
try {
const timeValue = new Date(tradePointsData[i].time);
if (isNaN(timeValue.getTime())) {
console.error(`买卖点 #${i} 时间格式无效:`, tradePointsData[i].time);
}
} catch (e) {
console.error(`买卖点 #${i} 时间格式异常:`, e);
}
} else {
console.error(`买卖点 #${i} 缺少时间属性`);
}
}
};
// 执行格式检查
checkDataFormat();
// 对买卖点按时间排序,用于后续优化显示
const sortedPoints = [...tradePointsData].sort((a, b) => {
return new Date(a.time) - new Date(b.time);
});
// 记录已处理的时间点 - 按类型分开计数
const processedTimes = {};
// 创建买卖点标记系列
const buyMarkers = [];
const sellMarkers = [];
// 计数器,追踪成功和失败的处理次数
let successCount = 0;
let errorCount = 0;
sortedPoints.forEach(function(point, index) {
try {
// 检查所有必要的属性是否存在且有效
if (!point.time || !point.price || point.type === undefined) {
console.error(`买卖点 #${index} 数据不完整:`, point);
errorCount++;
return;
}
const time = Math.floor(new Date(point.time).getTime() / 1000);
const price = parseFloat(point.price);
const type = parseInt(point.type);
if (isNaN(time) || isNaN(price) || isNaN(type)) {
console.error(`买卖点 #${index} 数据格式错误:`,
{ time: isNaN(time), price: isNaN(price), type: isNaN(type) }, point);
errorCount++;
return;
}
// 获取买卖点样式
const style = TRADE_POINT_STYLE[type] || {
color: '#999999',
shape: 'circle',
text: '?',
size: 1
};
// 初始化该时间点的类型计数器
if (!processedTimes[time]) {
processedTimes[time] = {};
}
// 优化:检查是否有相同时间点和相同类型的标记,如果有,进行类型内的偏移
let stackIndex = 0;
if (processedTimes[time][type]) {
// 已经有相同时间和类型的标记,记录堆叠索引
stackIndex = processedTimes[time][type];
processedTimes[time][type]++;
} else {
// 第一次出现这个时间点的这个类型
processedTimes[time][type] = 1;
}
// 为不同类型的买卖点获取基础垂直偏移系数
const baseOffset = TRADE_POINT_OFFSET[type] || 0;
// 创建标记对象,包含额外的信息用于悬停提示
const marker = {
time: time,
position: 'inBar', // 改为在K线内部显示,不影响数据
color: style.color,
shape: style.shape,
text: style.text,
size: style.size,
// 记录堆叠索引
stackIndex: stackIndex,
// 添加悬停提示的数据
tooltip: `<span class="${type > 0 ? 'buy-point' : 'sell-point'}">${point.desc || (type > 0 ? '买点' : '卖点')}</span><br>
时间: ${formatTime(point.time)}<br>
价格: ${price.toFixed(2)}`,
// 额外添加基础类型偏移
baseOffset: baseOffset,
// 添加边框
borderColor: 'white',
borderWidth: 1,
// 添加价格偏移系数
pricePercentOffset: PRICE_PERCENT_OFFSET[type] || 0,
// 保存实际价格用于计算
price: price,
// 保存类型
type: type
};
// 区分买卖点
if (type > 0) {
buyMarkers.push(marker);
} else {
sellMarkers.push(marker);
}
successCount++;
} catch (e) {
console.error(`处理买卖点 #${index} 出错:`, e, point);
errorCount++;
}
});
console.log(`买卖点处理完成: 成功=${successCount}, 失败=${errorCount}, 买点=${buyMarkers.length}, 卖点=${sellMarkers.length}`);
// 分别添加买卖点标记
if (buyMarkers.length > 0) {
const buyMarkersSeries = mainChart.addLineSeries({
lastValueVisible: false,
priceLineVisible: false,
lineVisible: false,
title: '买点'
});
// 设置临时的数据点
buyMarkersSeries.setData([{ time: buyMarkers[0].time, value: 0 }]);
try {
// 设置买点标记,并添加偏移
buyMarkersSeries.setMarkers(
buyMarkers.map(marker => {
// 使用固定偏移而非百分比
const fixedOffset = PRICE_FIXED_OFFSET[marker.type] || 20;
// 为同一时间点的多个买点额外增加堆叠偏移
const stackOffset = marker.stackIndex > 0 ?
10 * marker.stackIndex : 0;
// 使用实际价格位置添加标记,不使用偏移
return {
...marker,
position: 'aboveBar', // 显示在K线上方
// 使用原始价格
price: marker.price
};
})
);
console.log(`成功添加 ${buyMarkers.length} 个买点标记`);
} catch (e) {
console.error("设置买点标记时出错:", e);
}
}
if (sellMarkers.length > 0) {
const sellMarkersSeries = mainChart.addLineSeries({
lastValueVisible: false,
priceLineVisible: false,
lineVisible: false,
title: '卖点'
});
// 设置临时的数据点
sellMarkersSeries.setData([{ time: sellMarkers[0].time, value: 0 }]);
try {
// 设置卖点标记,并添加偏移
sellMarkersSeries.setMarkers(
sellMarkers.map(marker => {
// 使用固定偏移而非百分比
const fixedOffset = PRICE_FIXED_OFFSET[marker.type] || 20;
// 为同一时间点的多个卖点额外增加堆叠偏移
const stackOffset = marker.stackIndex > 0 ?
10 * marker.stackIndex : 0;
// 使用实际价格位置添加标记,不使用偏移
return {
...marker,
position: 'belowBar', // 显示在K线下方
// 使用原始价格
price: marker.price
};
})
);
console.log(`成功添加 ${sellMarkers.length} 个卖点标记`);
} catch (e) {
console.error("设置卖点标记时出错:", e);
}
}
// 添加鼠标悬停事件显示提示
mainChart.subscribeCrosshairMove(param => {
if (param.time && param.point) {
const timeStr = param.time;
const markers = [...buyMarkers, ...sellMarkers].filter(m => m.time === timeStr);
if (markers.length > 0) {
// 有买卖点标记,显示自定义提示
const tooltips = markers.map(m => m.tooltip).join('<br>');
tooltipElement.innerHTML = tooltips;
tooltipElement.style.display = 'block';
tooltipElement.style.left = (param.point.x + 15) + 'px';
tooltipElement.style.top = (param.point.y + 15) + 'px';
} else {
// 隐藏提示
tooltipElement.style.display = 'none';
}
} else {
// 隐藏提示
tooltipElement.style.display = 'none';
}
});
// 处理图表缩放、平移等事件,隐藏提示
mainChart.timeScale().subscribeVisibleTimeRangeChange(() => {
tooltipElement.style.display = 'none';
});
}
} else {
console.log('绘制买卖点 - 已禁用');
}
// 显示分型类型标签
if ($('#showKlcFxType').is(':checked') && currentData.klc_fx_info && currentData.klc_fx_info.length > 0) {
console.log(`绘制K线分型类型标签,共${currentData.klc_fx_info.length}条`);
currentData.klc_fx_info.forEach(function(fx) {
try {
// 直接使用UTC时间戳(秒)
const timestamp = Math.floor(new Date(fx.time).getTime() / 1000);
const price = parseFloat(fx.price);
if (isNaN(timestamp) || isNaN(price)) {
console.error('分型类型时间或价格转换错误:', fx.time, fx.price);
return;
}
// 确定颜色和位置
const color = fx.is_bottom ? '#28a745' : '#dc3545'; // 底分型绿色,顶分型红色
// 创建标记系列
const markerSeries = mainChart.addLineSeries({
lastValueVisible: false,
priceLineVisible: false,
});
// 设置文本标记
markerSeries.setMarkers([
{
time: timestamp,
position: fx.is_bottom ? 'belowBar' : 'aboveBar',
color: color,
shape: 'circle',
text: fx.fx_type,
size: 1
}
]);
// 可选:添加更加明显的文本标签
const textSeries = mainChart.addLineSeries({
lastValueVisible: false,
priceLineVisible: false,
});
textSeries.setData([{
time: timestamp,
value: price + (fx.is_bottom ? -0.0005 * price : 0.0005 * price) // 小偏移,避免遮挡
}]);
} catch (e) {
console.error('绘制分型类型标签出错:', e);
}
});
} else {
console.log('绘制分型类型标签 - 已禁用或无数据');
}
// 绘制小周期分型标记
if ($('#showElementKlcFxType').is(':checked') && currentData.element_klc_fx_info && currentData.element_klc_fx_info.length > 0) {
console.log(`绘制小周期分型标记,共${currentData.element_klc_fx_info.length}条`);
currentData.element_klc_fx_info.forEach(function(fx) {
try {
// 直接使用UTC时间戳(秒)
const timestamp = Math.floor(new Date(fx.time).getTime() / 1000);
const price = parseFloat(fx.price);
if (isNaN(timestamp) || isNaN(price)) {
console.error('小周期分型时间或价格转换错误:', fx.time, fx.price);
return;
}
// 小周期分型使用红色标记,不同于主周期分型
const redColor = '#FF0000'; // 红色
// 创建标记系列
const markerSeries = mainChart.addLineSeries({
lastValueVisible: false,
priceLineVisible: false,
});
// 设置文本标记
markerSeries.setMarkers([
{
time: timestamp,
position: fx.is_bottom ? 'belowBar' : 'aboveBar',
color: redColor,
shape: 'arrowUp', // 使用箭头形状,与主周期分型区分
text: fx.is_bottom ? '↓' : '↑', // 显示箭头
size: 1
}
]);
// 为小周期分型添加明显的箭头标记
const arrowSeries = mainChart.addLineSeries({
lastValueVisible: false,
priceLineVisible: false,
lineWidth: 1,
color: redColor
});
// 计算标记位置,底分型在价格下方,顶分型在价格上方
const offset = fx.is_bottom ? -0.001 * price : 0.001 * price;
arrowSeries.setData([{
time: timestamp,
value: price + offset
}]);
} catch (e) {
console.error('绘制小周期分型标记出错:', e);
}
});
} else {
console.log('绘制小周期分型标记 - 已禁用或无数据');
}
// 调整所有图表以适应数据
mainChart.timeScale().fitContent();
volumeChart.timeScale().fitContent();
if (showMacd && macdChart) {
macdChart.timeScale().fitContent();
}
// 保存图表对象
tvWidget.mainChart = mainChart;
tvWidget.volumeChart = volumeChart;
tvWidget.macdChart = macdChart;
tvWidget.state.isInitialized = true;
// 绑定同步事件
bindSyncEvents();
// 设置图表默认时间范围
setDefaultTimeRange();
// 添加买卖点提示
setupTooltip();
// 显示买卖点
if ($('#showTradePoints').is(':checked')) {
displayTradePoints();
}
console.log('图表初始化完成');
} catch (e) {
console.error('图表初始化错误:', e);
}
}
// 增量更新图表数据
function updateTradingViewData() {
try {
console.log('增量更新图表数据');
// 保存当前的可视范围
if (tvWidget.mainChart) {
tvWidget.state.visibleRange = tvWidget.mainChart.timeScale().getVisibleRange();
tvWidget.state.logicalRange = tvWidget.mainChart.timeScale().getVisibleLogicalRange();
}
// 检查是否显示原始K线
const showOriginalKline = $('#showOriginalKline').is(':checked');
// 检查是否使用小周期数据
const useElementPeriod = $('#elementPeriodKline').is(':checked') &&
currentData.element_timeframe &&
currentData.element_kline_data;
// 转换K线数据
let candles;
if (useElementPeriod) {
console.log('使用小周期K线数据');
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 {
console.log('使用主周期K线数据');
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),
};
});
}
// 更新K线数据
if (showOriginalKline && tvWidget.series.candleSeries) {
tvWidget.series.candleSeries.setData(candles);
} else if (!showOriginalKline && tvWidget.series.lineSeries) {
// 提取收盘价数据
const lineData = candles.map(candle => ({
time: candle.time,
value: candle.close
}));
tvWidget.series.lineSeries.setData(lineData);
}
// 更新成交量数据
const volumes = useElementPeriod ?
currentData.element_kline_data.map(kline => {
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
return {
time: timestamp,
value: parseFloat(kline.volume),
color: parseFloat(kline.close) >= parseFloat(kline.open) ? 'rgba(220, 53, 69, 0.5)' : 'rgba(40, 167, 69, 0.5)',
};
}) :
currentData.kline_data.map(kline => {
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
return {
time: timestamp,
value: parseFloat(kline.volume),
color: parseFloat(kline.close) >= parseFloat(kline.open) ? 'rgba(220, 53, 69, 0.5)' : 'rgba(40, 167, 69, 0.5)',
};
});
if (tvWidget.series.volumeSeries) {
tvWidget.series.volumeSeries.setData(volumes);
}
// 更新MACD数据
if ($('#showMacd').is(':checked') && currentData.macd && tvWidget.series.macdLineSeries) {
// 提取MACD数据
const macdData = [];
const signalData = [];
const histogramData = [];
for (let i = 0; i < currentData.kline_data.length; i++) {
const kline = currentData.kline_data[i];
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
if (currentData.macd && currentData.macd.macd && currentData.macd.macd[i] !== undefined) {
macdData.push({
time: timestamp,
value: currentData.macd.macd[i]
});
signalData.push({
time: timestamp,
value: currentData.macd.signal[i]
});
// 设置直方图颜色
const histValue = currentData.macd.histogram[i];
histogramData.push({
time: timestamp,
value: histValue,
color: histValue >= 0 ? 'rgba(220, 53, 69, 0.5)' : 'rgba(40, 167, 69, 0.5)'
});
}
}
tvWidget.series.macdLineSeries.setData(macdData);
tvWidget.series.signalLineSeries.setData(signalData);
tvWidget.series.histogramSeries.setData(histogramData);
}
// 重新显示笔、线段和中枢等图形
redrawFractalElements();
// 恢复之前的可视范围
if (tvWidget.mainChart) {
if (tvWidget.state.logicalRange) {
tvWidget.mainChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange);
if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange);
if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange);
} else if (tvWidget.state.visibleRange) {
tvWidget.mainChart.timeScale().setVisibleRange(tvWidget.state.visibleRange);
if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleRange(tvWidget.state.visibleRange);
if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleRange(tvWidget.state.visibleRange);
}
}
console.log('增量更新图表完成');
} catch (e) {
console.error('增量更新图表错误,回退到完全重绘:', e);
// 出错时回退到完全重绘
initTradingView($('#symbol').val(), $('#timeframe').val());
}
}
function bindSyncEvents() {
// 同步图表的时间范围
function syncCharts(sourceChart, sourceContainer) {
// 防止无限循环
if (syncInProgress) return;
syncInProgress = true;
try {
if (sourceChart) {
const logicalRange = sourceChart.timeScale().getVisibleLogicalRange();
if (logicalRange) {
if (sourceChart !== mainChart) {
mainChart.timeScale().setVisibleLogicalRange(logicalRange);
}
if (sourceChart !== volumeChart) {
volumeChart.timeScale().setVisibleLogicalRange(logicalRange);
}
if (showMacd && macdChart && sourceChart !== macdChart) {
macdChart.timeScale().setVisibleLogicalRange(logicalRange);
}
}
}
} catch (e) {
console.error('同步图表出错:', e);
}
setTimeout(() => { syncInProgress = false; }, 50);
}
// 为每个图表添加事件监听
const addChartSyncEvents = (chartContainer, chart) => {
// 鼠标滚轮事件
chartContainer.addEventListener('wheel', () => {
syncCharts(chart, chartContainer);
});
// 拖动事件
let isDragging = false;
chartContainer.addEventListener('mousedown', () => { isDragging = true; });
chartContainer.addEventListener('mousemove', () => {
if (isDragging) {
syncCharts(chart, chartContainer);
}
});
chartContainer.addEventListener('mouseup', () => { isDragging = false; });
chartContainer.addEventListener('mouseleave', () => { isDragging = false; });
// 触摸事件(移动设备)
chartContainer.addEventListener('touchmove', () => {
syncCharts(chart, chartContainer);
});
};
// 添加事件监听
addChartSyncEvents(mainChartContainer, mainChart);
addChartSyncEvents(volumeChartContainer, volumeChart);
if (showMacd && macdChart) {
addChartSyncEvents(macdChartContainer, macdChart);
}
// 全局鼠标抬起事件
document.addEventListener('mouseup', () => {
isDragging = false;
});
// 窗口大小变化时重绘图表
window.addEventListener('resize', () => {
// 调整主图大小
mainChart.applyOptions({
width: mainChartContainer.clientWidth,
height: mainChartContainer.clientHeight
});
// 调整成交量图大小
volumeChart.applyOptions({
width: volumeChartContainer.clientWidth,
height: volumeChartContainer.clientHeight
});
// 调整MACD图大小
if (showMacd && macdChart) {
macdChart.applyOptions({
width: macdChartContainer.clientWidth,
height: macdChartContainer.clientHeight
});
}
// 重新同步
setTimeout(() => syncCharts(mainChart, null), 100);
});
}
function setupTooltip() {
// 添加买卖点悬浮提示元素
const tooltipElement = document.createElement('div');
tooltipElement.className = 'point-tooltip';
document.body.appendChild(tooltipElement);
// 添加鼠标悬停事件显示提示
mainChart.subscribeCrosshairMove(param => {
if (param.time && param.point) {
const timeStr = param.time;
const markers = [...buyMarkers, ...sellMarkers].filter(m => m.time === timeStr);
if (markers.length > 0) {
// 有买卖点标记,显示自定义提示
const tooltips = markers.map(m => m.tooltip).join('<br>');
tooltipElement.innerHTML = tooltips;
tooltipElement.style.display = 'block';
tooltipElement.style.left = (param.point.x + 15) + 'px';
tooltipElement.style.top = (param.point.y + 15) + 'px';
} else {
// 隐藏提示
tooltipElement.style.display = 'none';
}
} else {
// 隐藏提示
tooltipElement.style.display = 'none';
}
});
// 处理图表缩放、平移等事件,隐藏提示
mainChart.timeScale().subscribeVisibleTimeRangeChange(() => {
tooltipElement.style.display = 'none';
});
}
function displayTradePoints() {
// 优先使用小周期数据,如果不存在则使用主周期数据
const tradePointsData = currentData.element_trade_points || currentData.trade_points;
console.log(`绘制${currentData.element_trade_points ? '元素周期' : '主周期'}买卖点数据,共${tradePointsData ? tradePointsData.length : 0}条`);
// 调试信息 - 输出完整的买卖点数据
if (tradePointsData && tradePointsData.length > 0) {
console.log("买卖点数据样例:", tradePointsData[0]);
// 检查数据格式,如果time不是标准格式,进行格式化处理
const checkDataFormat = () => {
for (let i = 0; i < tradePointsData.length; i++) {
if (tradePointsData[i].time) {
// 确保时间是标准格式
try {
const timeValue = new Date(tradePointsData[i].time);
if (isNaN(timeValue.getTime())) {
console.error(`买卖点 #${i} 时间格式无效:`, tradePointsData[i].time);
}
} catch (e) {
console.error(`买卖点 #${i} 时间格式异常:`, e);
}
} else {
console.error(`买卖点 #${i} 缺少时间属性`);
}
}
};
// 执行格式检查
checkDataFormat();
// 对买卖点按时间排序,用于后续优化显示
const sortedPoints = [...tradePointsData].sort((a, b) => {
return new Date(a.time) - new Date(b.time);
});
// 记录已处理的时间点 - 按类型分开计数
const processedTimes = {};
// 创建买卖点标记系列
const buyMarkers = [];
const sellMarkers = [];
// 计数器,追踪成功和失败的处理次数
let successCount = 0;
let errorCount = 0;
sortedPoints.forEach(function(point, index) {
try {
// 检查所有必要的属性是否存在且有效
if (!point.time || !point.price || point.type === undefined) {
console.error(`买卖点 #${index} 数据不完整:`, point);
errorCount++;
return;
}
const time = Math.floor(new Date(point.time).getTime() / 1000);
const price = parseFloat(point.price);
const type = parseInt(point.type);
if (isNaN(time) || isNaN(price) || isNaN(type)) {
console.error(`买卖点 #${index} 数据格式错误:`,
{ time: isNaN(time), price: isNaN(price), type: isNaN(type) }, point);
errorCount++;
return;
}
// 获取买卖点样式
const style = TRADE_POINT_STYLE[type] || {
color: '#999999',
shape: 'circle',
text: '?',
size: 1
};
// 初始化该时间点的类型计数器
if (!processedTimes[time]) {
processedTimes[time] = {};
}
// 优化:检查是否有相同时间点和相同类型的标记,如果有,进行类型内的偏移
let stackIndex = 0;
if (processedTimes[time][type]) {
// 已经有相同时间和类型的标记,记录堆叠索引
stackIndex = processedTimes[time][type];
processedTimes[time][type]++;
} else {
// 第一次出现这个时间点的这个类型
processedTimes[time][type] = 1;
}
// 为不同类型的买卖点获取基础垂直偏移系数
const baseOffset = TRADE_POINT_OFFSET[type] || 0;
// 创建标记对象,包含额外的信息用于悬停提示
const marker = {
time: time,
position: 'inBar', // 改为在K线内部显示,不影响数据
color: style.color,
shape: style.shape,
text: style.text,
size: style.size,
// 记录堆叠索引
stackIndex: stackIndex,
// 添加悬停提示的数据
tooltip: `<span class="${type > 0 ? 'buy-point' : 'sell-point'}">${point.desc || (type > 0 ? '买点' : '卖点')}</span><br>
时间: ${formatTime(point.time)}<br>
价格: ${price.toFixed(2)}`,
// 额外添加基础类型偏移
baseOffset: baseOffset,
// 添加边框
borderColor: 'white',
borderWidth: 1,
// 添加价格偏移系数
pricePercentOffset: PRICE_PERCENT_OFFSET[type] || 0,
// 保存实际价格用于计算
price: price,
// 保存类型
type: type
};
// 区分买卖点
if (type > 0) {
buyMarkers.push(marker);
} else {
sellMarkers.push(marker);
}
successCount++;
} catch (e) {
console.error(`处理买卖点 #${index} 出错:`, e, point);
errorCount++;
}
});
console.log(`买卖点处理完成: 成功=${successCount}, 失败=${errorCount}, 买点=${buyMarkers.length}, 卖点=${sellMarkers.length}`);
// 分别添加买卖点标记
if (buyMarkers.length > 0) {
const buyMarkersSeries = mainChart.addLineSeries({
lastValueVisible: false,
priceLineVisible: false,
lineVisible: false,
title: '买点'
});
// 设置临时的数据点
buyMarkersSeries.setData([{ time: buyMarkers[0].time, value: 0 }]);
try {
// 设置买点标记,并添加偏移
buyMarkersSeries.setMarkers(
buyMarkers.map(marker => {
// 使用固定偏移而非百分比
const fixedOffset = PRICE_FIXED_OFFSET[marker.type] || 20;
// 为同一时间点的多个买点额外增加堆叠偏移
const stackOffset = marker.stackIndex > 0 ?
10 * marker.stackIndex : 0;
// 使用实际价格位置添加标记,不使用偏移
return {
...marker,
position: 'aboveBar', // 显示在K线上方
// 使用原始价格
price: marker.price
};
})
);
console.log(`成功添加 ${buyMarkers.length} 个买点标记`);
} catch (e) {
console.error("设置买点标记时出错:", e);
}
}
if (sellMarkers.length > 0) {
const sellMarkersSeries = mainChart.addLineSeries({
lastValueVisible: false,
priceLineVisible: false,
lineVisible: false,
title: '卖点'
});
// 设置临时的数据点
sellMarkersSeries.setData([{ time: sellMarkers[0].time, value: 0 }]);
try {
// 设置卖点标记,并添加偏移
sellMarkersSeries.setMarkers(
sellMarkers.map(marker => {
// 使用固定偏移而非百分比
const fixedOffset = PRICE_FIXED_OFFSET[marker.type] || 20;
// 为同一时间点的多个卖点额外增加堆叠偏移
const stackOffset = marker.stackIndex > 0 ?
10 * marker.stackIndex : 0;
// 使用实际价格位置添加标记,不使用偏移
return {
...marker,
position: 'belowBar', // 显示在K线下方
// 使用原始价格
price: marker.price
};
})
);
console.log(`成功添加 ${sellMarkers.length} 个卖点标记`);
} catch (e) {
console.error("设置卖点标记时出错:", e);
}
}
}
}
function updateTables(data) {
// K线数据表 - 始终使用主时间周期数据
if (tables.kline) {
tables.kline.clear().destroy();
}
tables.kline = $('#klineTable').DataTable({
data: data.kline_data,
order: [[0, 'desc']],
pageLength: 25,
columns: [
{ data: 'date', render: function(data) { return formatTime(data); } },
{ data: 'open', render: formatPrice },
{ data: 'high', render: formatPrice },
{ data: 'low', render: formatPrice },
{ data: 'close', render: formatPrice },
{ data: 'volume', render: function(data) { return parseInt(data).toLocaleString(); } }
]
});
// 买卖点数据表 - 优先使用元素时间周期数据
if (tables.tradePoints) {
tables.tradePoints.clear().destroy();
}
// 优先使用小周期数据,如果不存在则使用主周期数据
const tradePointsData = data.element_trade_points || data.trade_points || [];
const tradePointsSource = data.element_trade_points ? '元素周期' : '主周期';
console.log(`表格显示${tradePointsSource}买卖点数据,共${tradePointsData.length}条`);
tables.tradePoints = $('#tradePointsTable').DataTable({
data: tradePointsData,
order: [[0, 'desc']],
pageLength: 25,
columns: [
{ data: 'time', render: formatTime },
{ data: 'price', render: formatPrice },
{
data: 'type',
render: function(type) {
const typeText = type > 0 ? `买${Math.abs(type)}` : `卖${Math.abs(type)}`;
const typeClass = type > 0 ? 'direction-up' : 'direction-down';
return '<span class="' + typeClass + '">' + typeText + '</span>';
}
},
{ data: 'desc' }
]
});
// 笔数据表 - 优先使用元素时间周期数据
if (tables.bi) {
tables.bi.clear().destroy();
}
// 优先使用小周期数据,如果不存在则使用主周期数据
const biData = data.element_bi_list || data.bi_list;
const biSource = data.element_bi_list ? '元素周期' : '主周期';
console.log(`表格显示${biSource}笔数据,共${biData ? biData.length : 0}条`);
tables.bi = $('#biTable').DataTable({
data: biData || [],
order: [[0, 'desc']],
pageLength: 25,
columns: [
{ data: 'start_time', render: formatTime },
{ data: 'end_time', render: formatTime },
{ data: 'start_price', render: formatPrice },
{ data: 'end_price', render: formatPrice },
{ data: 'direction', render: formatDirection }
]
});
// 线段数据表 - 优先使用元素时间周期数据
if (tables.seg) {
tables.seg.clear().destroy();
}
// 优先使用小周期数据,如果不存在则使用主周期数据
const segData = data.element_seg_list || data.seg_list;
const segSource = data.element_seg_list ? '元素周期' : '主周期';
console.log(`表格显示${segSource}线段数据,共${segData ? segData.length : 0}条`);
tables.seg = $('#segTable').DataTable({
data: segData || [],
order: [[0, 'desc']],
pageLength: 25,
columns: [
{ data: 'start_time', render: formatTime },
{ data: 'end_time', render: formatTime },
{ data: 'start_price', render: formatPrice },
{ data: 'end_price', render: formatPrice },
{ data: 'direction', render: formatDirection }
]
});
// 中枢数据表 - 优先使用元素时间周期数据
if (tables.zs) {
tables.zs.clear().destroy();
}
// 使用小周期中枢数据(如果存在)
const zsData = data.element_zs_list || data.zs_list;
const zsSource = data.element_zs_list ? '元素周期' : '主周期';
console.log(`表格显示${zsSource}中枢数据,共${zsData ? zsData.length : 0}条`);
tables.zs = $('#zsTable').DataTable({
data: zsData || [],
order: [[0, 'desc']],
pageLength: 25,
columns: [
{ data: 'start_time', render: formatTime },
{ data: 'end_time', render: formatTime },
{ data: 'zg', render: formatPrice },
{ data: 'zd', render: formatPrice }
]
});
// 未完成中枢数据表更新
if (tables.uncompletedZs) {
tables.uncompletedZs.clear().destroy();
}
// 使用小周期未完成中枢数据(如果存在)
const uncompletedZsData = data.element_uncompleted_zs_list || data.uncompleted_zs_list;
const uncompletedZsSource = data.element_uncompleted_zs_list ? '元素周期' : '主周期';
console.log(`表格显示${uncompletedZsSource}未完成中枢数据,共${uncompletedZsData ? uncompletedZsData.length : 0}条`);
tables.uncompletedZs = $('#uncompletedZsTable').DataTable({
data: uncompletedZsData || [],
order: [[0, 'desc']],
pageLength: 25,
columns: [
{ data: 'start_time', render: formatTime },
{ data: 'zg', render: formatPrice },
{ data: 'zd', render: formatPrice }
]
});
// MACD数据表 - 始终使用主K线周期数据
if (tables.macd) {
tables.macd.clear().destroy();
}
const macdData = data.kline_data.map((item, index) => {
return {
time: item.date,
close: item.close,
macd: data.macd.macd[index],
signal: data.macd.signal[index],
histogram: data.macd.histogram[index]
};
});
tables.macd = $('#macdTable').DataTable({
data: macdData,
order: [[0, 'desc']],
pageLength: 25,
columns: [
{ data: 'time', render: formatTime },
{ data: 'close', render: formatPrice },
{ data: 'macd', render: formatMacdValue },
{ data: 'signal', render: formatMacdValue },
{ data: 'histogram', render: formatMacdValue }
]
});
// 设置数据源信息
setupDataSourceInfo(data);
}
// 设置数据源信息显示
function setupDataSourceInfo(data) {
// 初始化数据源显示
const mainTimeframe = $('#timeframe').val();
const elementTimeframe = data.element_timeframe || mainTimeframe;
// 添加标签点击事件
$('#kline-tab, #macd-tab').off('click').on('click', function() {
$('.data-source-info').show();
$('#dataSourceText').html(`当前显示的是<strong>主周期 (${mainTimeframe})</strong> 数据`);
});
$('#bi-tab').off('click').on('click', function() {
$('.data-source-info').show();
const source = data.element_bi_list ? elementTimeframe : mainTimeframe;
const sourceType = data.element_bi_list ? '元素周期' : '主周期';
$('#dataSourceText').html(`当前显示的是<strong>${sourceType} (${source})</strong> 笔数据`);
});
$('#seg-tab').off('click').on('click', function() {
$('.data-source-info').show();
const source = data.element_seg_list ? elementTimeframe : mainTimeframe;
const sourceType = data.element_seg_list ? '元素周期' : '主周期';
$('#dataSourceText').html(`当前显示的是<strong>${sourceType} (${source})</strong> 线段数据`);
});
$('#zs-tab').off('click').on('click', function() {
$('.data-source-info').show();
const source = data.element_zs_list ? elementTimeframe : mainTimeframe;
const sourceType = data.element_zs_list ? '元素周期' : '主周期';
$('#dataSourceText').html(`当前显示的是<strong>${sourceType} (${source})</strong> 中枢数据`);
});
$('#trade-points-tab').off('click').on('click', function() {
$('.data-source-info').show();
const source = data.element_trade_points ? elementTimeframe : mainTimeframe;
const sourceType = data.element_trade_points ? '元素周期' : '主周期';
$('#dataSourceText').html(`当前显示的是<strong>${sourceType} (${source})</strong> 买卖点数据`);
});
// 初始触发当前标签的点击事件
$('.nav-link.active').trigger('click');
}
// 获取可用交易对
function loadSymbols() {
$.get('/api/symbols', function(data) {
if (Array.isArray(data)) {
const $select = $('#symbol');
const currentSymbol = $select.val(); // 保存当前选中的值
$select.empty();
data.forEach(function(symbol) {
$select.append($('<option>', {
value: symbol,
text: symbol
}));
});
// 如果有保存的选中值,恢复它
if (currentSymbol && data.includes(currentSymbol)) {
$select.val(currentSymbol);
} else {
// 设置默认值为SOL/USDT:USDT
$select.val('SOL/USDT:USDT');
}
}
});
}
// 设置默认时间范围
function setDefaultTimeRange() {
const now = new Date();
const oneDayAgo = new Date(now.getTime() - (24 * 60 * 60 * 1000));
// 格式化为datetime-local输入框所需的格式 YYYY-MM-DDThh:mm
$('#end_time').val(formatDatetimeLocal(now));
$('#start_time').val(formatDatetimeLocal(oneDayAgo));
}
// 格式化日期为datetime-local输入框格式
function formatDatetimeLocal(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day}T${hours}:${minutes}`;
}
// 页面加载时初始化
$(document).ready(function() {
// 初始化交易对下拉菜单
$('#symbol').val('SOL/USDT:USDT');
$('#timeframe').val('5m');
$('#elementTimeframe').val('1m');
// 测试打印时区偏移量
console.log('当前时区偏移量 (UTC+8):', getTimezoneOffset('Asia/Shanghai'));
console.log('当前时区偏移量 (UTC):', getTimezoneOffset('UTC'));
const now = new Date();
console.log('当前时间UTC:', now.toUTCString());
console.log('当前时间本地:', now.toString());
console.log('当前时间戳(秒):', now.getTime()/1000);
console.log('UTC时间戳:', Math.floor(now.getTime()/1000));
// 设置默认时间范围
setDefaultTimeRange();
// 默认禁用买卖点显示
$('#showTradePoints').prop('checked', false);
// 尝试加载更多交易对
loadSymbols();
// 初始化图表并加载默认数据
setTimeout(function() {
updateChart();
}, 500);
// 初始化自动刷新功能
initAutoRefresh();
});
// 自动刷新相关变量
let autoRefreshTimer = null;
let nextRefreshTime = null;
// 初始化自动刷新功能
function initAutoRefresh() {
// 监听自动刷新勾选框变化
$('#autoRefresh').change(function() {
if ($(this).is(':checked')) {
startAutoRefresh();
} else {
stopAutoRefresh();
}
});
// 监听刷新频率变化
$('#refreshInterval').change(function() {
if ($('#autoRefresh').is(':checked')) {
// 如果自动刷新已开启,重启定时器
stopAutoRefresh();
startAutoRefresh();
}
});
}
// 开始自动刷新
function startAutoRefresh() {
// 停止已有的刷新定时器
stopAutoRefresh();
// 获取刷新频率(分钟)
const interval = parseFloat($('#refreshInterval').val()) || 5;
const intervalMs = interval * 60 * 1000;
console.log(`开始自动刷新,频率: ${interval}分钟 (${intervalMs}毫秒)`);
// 计算下次刷新时间
nextRefreshTime = new Date(Date.now() + intervalMs);
updateNextRefreshTimeDisplay();
// 启动定时器
autoRefreshTimer = setInterval(function() {
// 更新结束时间为当前时间
updateEndTimeToNow();
// 刷新图表
updateChart();
// 更新下次刷新时间
nextRefreshTime = new Date(Date.now() + intervalMs);
updateNextRefreshTimeDisplay();
}, intervalMs);
// 启动倒计时显示
startCountdownDisplay();
// 显示下次刷新时间
$('#nextRefreshTime').show();
}
// 更新结束时间为当前时间
function updateEndTimeToNow() {
const now = new Date();
$('#end_time').val(formatDatetimeLocal(now));
console.log('已更新结束时间为当前时间:', formatDatetimeLocal(now));
}
// 停止自动刷新
function stopAutoRefresh() {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer);
autoRefreshTimer = null;
}
// 停止倒计时显示
clearInterval(countdownTimer);
countdownTimer = null;
// 隐藏下次刷新时间
$('#nextRefreshTime').hide();
}
// 更新下次刷新时间显示
function updateNextRefreshTimeDisplay() {
if (!nextRefreshTime) return;
const timeStr = nextRefreshTime.toLocaleTimeString();
$('#nextRefreshTime').text(`下次刷新: ${timeStr}`);
}
// 倒计时定时器
let countdownTimer = null;
// 启动倒计时显示
function startCountdownDisplay() {
// 清除已有的倒计时
if (countdownTimer) {
clearInterval(countdownTimer);
}
// 启动新的倒计时,每秒更新一次
countdownTimer = setInterval(function() {
if (!nextRefreshTime) return;
const now = new Date();
const diffMs = nextRefreshTime - now;
if (diffMs <= 0) {
// 已经到达或超过刷新时间,等待刷新发生
$('#nextRefreshTime').text('正在刷新...');
} else {
// 计算剩余时间
const diffSec = Math.floor(diffMs / 1000);
// 如果时间超过1分钟,显示分和秒
if (diffSec >= 60) {
const minutes = Math.floor(diffSec / 60);
const seconds = diffSec % 60;
// 格式化显示
const timeStr = `${minutes}${seconds.toString().padStart(2, '0')}秒后刷新`;
$('#nextRefreshTime').text(timeStr);
} else {
// 少于1分钟只显示秒数
const timeStr = `${diffSec}秒后刷新`;
$('#nextRefreshTime').text(timeStr);
}
}
}, 1000);
}
// 将时间周期映射到数值(保留此函数以供后端API调用)
function mapTimeframeToInterval(timeframe) {
const mapping = {
'1m': '1',
'3m': '3',
'5m': '5',
'15m': '15',
'30m': '30',
'1h': '60',
'2h': '120',
'4h': '240',
'6h': '360',
'8h': '480',
'12h': '720',
'1d': 'D',
'3d': '3D',
'1w': 'W',
'1M': 'M'
};
return mapping[timeframe] || '5';
}
// 只重绘分形元素(笔、线段、中枢),保留现有的K线、MACD和成交量
function redrawFractalElements() {
// 检查图表是否已初始化
if (!tvWidget || !tvWidget.mainChart) {
console.error('图表未初始化,无法重绘分形元素');
return;
}
console.log('重绘分形元素 - 开始');
console.log('- 显示笔:', $('#showMainBi').is(':checked'));
console.log('- 显示线段:', $('#showMainSeg').is(':checked'));
console.log('- 显示中枢:', $('#showMainZs').is(':checked'));
// 保存当前的图表可见范围
const mainChart = tvWidget.mainChart;
const visibleRange = mainChart.timeScale().getVisibleRange();
const logicalRange = mainChart.timeScale().getVisibleLogicalRange();
// 获取当前图表设置
const showMacd = $('#showMacd').is(':checked');
const showOriginalKline = $('#showOriginalKline').is(':checked');
const showBi = $('#showMainBi').is(':checked');
const showSeg = $('#showMainSeg').is(':checked');
const showZs = $('#showMainZs').is(':checked');
// 保存原始K线和MACD数据,确保即使使用小级别,我们依然使用主周期的K线和MACD数据
const originalKlineData = currentData.kline_data;
const originalMacdData = currentData.macd;
// 重新初始化图表 - 这将清除所有系列并重新创建
console.log('重新初始化图表...');
// 临时存储currentData中可能被修改的字段
const tempCurrentData = {
kline_data: originalKlineData,
macd: originalMacdData
};
// 在重绘前确保K线和MACD数据不变
if (currentData.original_kline_data && currentData.original_macd) {
// 如果已经保存了原始数据,恢复它们
currentData.kline_data = currentData.original_kline_data;
currentData.macd = currentData.original_macd;
} else {
// 首次运行 - 保存原始数据
currentData.original_kline_data = originalKlineData;
currentData.original_macd = originalMacdData;
}
// 调用初始化函数
initTradingView($('#symbol').val(), $('#timeframe').val());
// 恢复原始可见范围
setTimeout(() => {
if (tvWidget && tvWidget.mainChart) {
console.log('恢复图表可见范围...');
if (logicalRange) {
tvWidget.mainChart.timeScale().setVisibleLogicalRange(logicalRange);
if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleLogicalRange(logicalRange);
if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleLogicalRange(logicalRange);
} else if (visibleRange) {
tvWidget.mainChart.timeScale().setVisibleRange(visibleRange);
if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleRange(visibleRange);
if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleRange(visibleRange);
}
console.log('图表可见范围已恢复');
} else {
console.error('恢复图表可见范围失败 - 图表未初始化');
}
}, 200);
console.log('重绘分形元素 - 完成');
}
// 只更新分形元素(笔、线段、中枢)的表格数据
function updateFractalTables() {
if (!currentData) return;
const data = currentData;
// 笔数据表更新
if (tables.bi) {
tables.bi.clear().destroy();
}
// 使用小周期笔数据(如果存在)
const biData = data.element_bi_list || data.bi_list;
const biSource = data.element_bi_list ? '元素周期' : '主周期';
console.log(`表格显示${biSource}笔数据,共${biData ? biData.length : 0}条`);
tables.bi = $('#biTable').DataTable({
data: biData || [],
order: [[0, 'desc']],
pageLength: 25,
columns: [
{ data: 'start_time', render: formatTime },
{ data: 'end_time', render: formatTime },
{ data: 'start_price', render: formatPrice },
{ data: 'end_price', render: formatPrice },
{ data: 'direction', render: formatDirection }
]
});
// 线段数据表更新
if (tables.seg) {
tables.seg.clear().destroy();
}
// 使用小周期线段数据(如果存在)
const segData = data.element_seg_list || data.seg_list;
const segSource = data.element_seg_list ? '元素周期' : '主周期';
console.log(`表格显示${segSource}线段数据,共${segData ? segData.length : 0}条`);
tables.seg = $('#segTable').DataTable({
data: segData || [],
order: [[0, 'desc']],
pageLength: 25,
columns: [
{ data: 'start_time', render: formatTime },
{ data: 'end_time', render: formatTime },
{ data: 'start_price', render: formatPrice },
{ data: 'end_price', render: formatPrice },
{ data: 'direction', render: formatDirection }
]
});
// 中枢数据表更新
if (tables.zs) {
tables.zs.clear().destroy();
}
// 使用小周期中枢数据(如果存在)
const zsData = data.element_zs_list || data.zs_list;
const zsSource = data.element_zs_list ? '元素周期' : '主周期';
console.log(`表格显示${zsSource}中枢数据,共${zsData ? zsData.length : 0}条`);
tables.zs = $('#zsTable').DataTable({
data: zsData || [],
order: [[0, 'desc']],
pageLength: 25,
columns: [
{ data: 'start_time', render: formatTime },
{ data: 'end_time', render: formatTime },
{ data: 'zg', render: formatPrice },
{ data: 'zd', render: formatPrice }
]
});
// 更新数据源信息
setupDataSourceInfo(data);
}
// 刷新图表并更新表格
function refreshChart(data) {
// 检查是否接收到数据
if (!data) {
console.error('未收到数据,无法刷新图表');
return;
}
console.log('API返回数据:', data);
console.log('K线数据点数:', data.kline_data ? data.kline_data.length : 0);
// 检查是否包含小周期数据
if (data.element_timeframe) {
console.log('小周期笔数据点数:', data.element_bi_list ? data.element_bi_list.length : 0);
console.log('小周期线段数据点数:', data.element_seg_list ? data.element_seg_list.length : 0);
console.log('小周期中枢数据点数:', data.element_zs_list ? data.element_zs_list.length : 0);
// 更新小周期选择器
$('#elementTimeframe').val(data.element_timeframe);
} else {
console.log('未提供小周期数据,使用主周期数据');
}
// 保存原始K线和MACD数据,用于后续参考
data.original_kline_data = data.kline_data;
data.original_macd = data.macd;
// 如果图表未初始化,则创建图表,否则更新图表数据
if (!tvWidget.state.isInitialized) {
console.log('图表尚未初始化,创建新图表');
initTradingView($('#symbol').val(), $('#timeframe').val());
} else {
console.log('图表已初始化,进行增量更新');
updateTradingViewData();
}
// 更新表格数据
updateTables(data);
}
// 绑定分型类型显示开关
$('#showKlcFxType').change(function() {
refreshChart(currentData);
});
// 绑定小周期分型显示开关
$('#showElementKlcFxType').change(function() {
refreshChart(currentData);
});
// 绑定买卖点显示开关
$('#showTradePoints').change(function() {
refreshChart(currentData);
});
// 绑定K线周期切换
$('input[name="klinePeriod"]').change(function() {
refreshChart(currentData);
});
// 在控制台输出当前显示状态
console.log('当前显示状态:', {
'showOriginalKline': $('#showOriginalKline').is(':checked'),
'showMainBi': $('#showMainBi').is(':checked'),
'showMainSeg': $('#showMainSeg').is(':checked'),
'showMainZs': $('#showMainZs').is(':checked'),
'showMainUncompletedZs': $('#showMainUncompletedZs').is(':checked'),
'showVolume': $('#showVolume').is(':checked'),
'showMacd': $('#showMacd').is(':checked'),
'showKlcFxType': $('#showKlcFxType').is(':checked'),
'showElementKlcFxType': $('#showElementKlcFxType').is(':checked'),
'showTradePoints': $('#showTradePoints').is(':checked'),
'timeframe': $('#timeframe').val(),
'elementTimeframe': $('#elementTimeframe').val(),
'timezone': $('#timezone').val(),
'start_time': $('#start_time').val(),
'end_time': $('#end_time').val()
});
// 初始化提示工具
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
})
</script>
</body>
</html>