Files
Chan/web/templates/index.html
T
2025-05-22 12:38:02 +08:00

4636 lines
217 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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: 10px;
background-color: #f8f9fa;
color: #333;
}
.container {
max-width: 100%;
margin: 0 auto;
background-color: white;
padding: 5px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.header {
margin-bottom: 0px;
padding-bottom: 0px;
}
.controls {
margin-bottom: 10px;
padding: 10px;
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: 10px;
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: 10px;
}
.nav-tabs {
margin-bottom: 10px;
}
.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;
}
/* 数据回放相关样式 */
#replayStatus {
min-width: 120px;
}
#replayProgress {
border-radius: 4px;
overflow: hidden;
}
.crosshair-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;
}
/* 主次周期元素样式区分 */
</style>
</head>
<body>
<div class="container">
<div class="header">
<h5 class="my-1">缠论分析系统</h5>
</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 == 'BTC/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-7">
<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">
<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>
<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">
<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">
<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">
<label class="form-check-label" for="showMainUncompletedZs">未完成中枢</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showMainMacdDiv">
<label class="form-check-label" for="showMainMacdDiv">MACD背离</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showKlcFxType" checked>
<label class="form-check-label" for="showKlcFxType">分型</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">
<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">
<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">
<label class="form-check-label" for="showElementUncompletedZs">未完成中枢</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showElementMacdDiv">
<label class="form-check-label" for="showElementMacdDiv">MACD背离</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showElementKlcFxType">
<label class="form-check-label" for="showElementKlcFxType">分型</label>
</div>
</div>
</div>
<div class="col-md-5">
<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 class="d-flex align-items-center mt-2">
<label for="replayInterval" class="form-label me-3 mb-0">数据回放:</label>
<select id="replayInterval" class="form-select form-select-sm me-2" style="width: auto;">
<option value="0.5">0.5秒</option>
<option value="1" selected>1秒</option>
<option value="2">2秒</option>
<option value="3">3秒</option>
<option value="5">5秒</option>
</select>
<div class="btn-group me-2">
<button id="startReplay" class="btn btn-sm btn-success">
<i class="bi bi-play-fill"></i> 开始回放
</button>
<button id="pauseReplay" class="btn btn-sm btn-warning" style="display:none;">
<i class="bi bi-pause-fill"></i> 暂停
</button>
<button id="stopReplay" class="btn btn-sm btn-danger" style="display:none;">
<i class="bi bi-stop-fill"></i> 停止
</button>
</div>
<span id="replayStatus" class="ms-2 text-muted" style="font-size:0.85rem;"></span>
<div id="replayProgress" class="progress ms-2" style="width: 100px; height: 8px; display: none;">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%"></div>
</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>
<th>MACD背离值</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, applyOffset = true) {
const date = new Date(dateStr);
const timestamp = Math.floor(date.getTime() / 1000);
if (!applyOffset) {
return timestamp;
}
// 不再手动调整时区偏移,使用JavaScript的内置时区支持
return timestamp;
}
// 添加时区选择器变更事件
$('#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);
// 添加调试信息
console.debug('表格时间格式化:', timeStr, '->',
date.toISOString(), '使用时区:', timezone);
// 使用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);
// 如果格式化失败,返回原始时间字符串
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() || 'BTC/USDT:USDT';
const timeframe = $('#timeframe').val() || '5m';
const timezone = $('#timezone').val() || 'Asia/Shanghai';
const elementTimeframe = $('#elementTimeframe').val() || '1m';
// 确保时区参数有效
console.log('更新图表使用时区:', timezone);
// 如果symbol为空,不发送请求
if (!symbol) {
console.error('交易对不能为空');
$('#refreshLoadingSpinner').hide();
return;
}
console.log(`更新图表: symbol=${symbol}, timeframe=${timeframe}, elementTimeframe=${elementTimeframe}, timezone=${timezone}`);
// 获取开始和结束时间(如果已设置)
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;
// 检查和记录服务器返回的时区
console.log('服务器返回的时区:', data.timezone || '未指定');
// 刷新图表
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,
// 添加十字线工具提示本地化配置
horzLine: {
labelVisible: true,
},
vertLine: {
labelVisible: true,
// 自定义时间格式化
labelFormatter: (time) => {
const selectedTimezone = $('#timezone').val();
try {
return new Date(time * 1000).toLocaleString('zh-CN', {
timeZone: selectedTimezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
} catch (e) {
console.error('十字线时间格式化错误:', e);
return new Date(time * 1000).toLocaleString();
}
},
},
},
rightPriceScale: {
borderColor: '#ddd',
},
// 添加本地化选项,确保所有时间显示都使用选定的时区
localization: {
timeFormatter: (time) => {
const selectedTimezone = $('#timezone').val();
try {
return new Date(time * 1000).toLocaleString('zh-CN', {
timeZone: selectedTimezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
} catch (e) {
console.error('全局时间格式化错误:', e);
return new Date(time * 1000).toLocaleString();
}
}
},
timeScale: {
timeVisible: true,
secondsVisible: false,
visible: showTimeScale,
borderColor: '#ddd',
barSpacing: 10,
tickMarkFormatter: (time) => {
const selectedTimezone = $('#timezone').val();
try {
// 使用完整的配置确保时区正确应用
const date = new Date(time * 1000);
console.log('格式化时间:', time, '转换为:', date.toISOString(), '时区:', selectedTimezone);
return date.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 ? '小周期' : '主周期');
console.log('当前选择时区:', $('#timezone').val());
if (useElementPeriod) {
// 使用小周期K线数据
candles = currentData.element_kline_data.map((kline) => {
// 使用原始日期字符串创建Date对象
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) => {
// 使用原始日期字符串创建Date对象
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
});
// 在笔的末端添加macd_div值标记
if (bi.macd_div && bi.macd_div !== 0 && $('#showMainMacdDiv').is(':checked')) {
console.log(`添加macd_div标记: ${bi.macd_div.toFixed(2)}, 在时间点: ${endTime}`);
const macdDivLabel = mainChart.addLineSeries({
lastValueVisible: false,
priceLineVisible: false,
});
// 确定标记位置
const markerPosition = bi.direction === 1 ? 'aboveBar' : 'belowBar';
const labelValue = bi.direction === 1 ? endPrice + (endPrice * 0.005) : endPrice - (endPrice * 0.005);
const arrayShape = bi.direction === 1 ? 'arrowDown' : 'arrowUp';
// 简化标记显示
macdDivLabel.setData([
{ time: endTime, value: labelValue }
]);
// 使用不同方式添加文本标记
const textColor = bi.macd_div > 0 ? '#dc3545' : '#28a745';
const textSize = Math.min(14, Math.max(10, Math.abs(bi.macd_div) * 2));
macdDivLabel.setMarkers([
{
time: endTime,
position: markerPosition,
color: textColor,
shape: arrayShape,
text: bi.macd_div.toFixed(2),
}
]);
}
} 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
});
// 在笔的末端添加macd_div值标记
if (bi.macd_div && bi.macd_div !== 0 && $('#showElementMacdDiv').is(':checked')) {
console.log(`添加元素周期macd_div标记: ${bi.macd_div.toFixed(2)}, 在时间点: ${endTime}`);
const macdDivLabel = mainChart.addLineSeries({
lastValueVisible: false,
priceLineVisible: false,
});
// 确定标记位置
const markerPosition = bi.direction === 1 ? 'aboveBar' : 'belowBar';
const labelValue = bi.direction === 1 ? endPrice + (endPrice * 0.005) : endPrice - (endPrice * 0.005);
const arrayShape = bi.direction === 1 ? 'arrowDown' : 'arrowUp';
// 简化标记显示
macdDivLabel.setData([
{ time: endTime, value: labelValue }
]);
// 使用不同方式添加文本标记
const textColor = bi.macd_div > 0 ? '#9c27b0' : '#673ab7';
const textSize = Math.min(14, Math.max(10, Math.abs(bi.macd_div) * 2));
macdDivLabel.setMarkers([
{
time: endTime,
position: markerPosition,
color: textColor,
shape: arrayShape,
text: bi.macd_div.toFixed(2),
transparent: true,
}
]);
}
} 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() {
// 调试变量
window.debugMode = true;
// 添加买卖点悬浮提示元素
const tooltipElement = document.createElement('div');
tooltipElement.className = 'point-tooltip';
document.body.appendChild(tooltipElement);
// 添加自定义十字线信息显示
const crosshairTooltip = document.createElement('div');
crosshairTooltip.className = 'crosshair-tooltip';
crosshairTooltip.style.position = 'absolute';
crosshairTooltip.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
crosshairTooltip.style.color = 'white';
crosshairTooltip.style.padding = '5px 10px';
crosshairTooltip.style.borderRadius = '4px';
crosshairTooltip.style.fontSize = '12px';
crosshairTooltip.style.zIndex = '1000';
crosshairTooltip.style.pointerEvents = 'none';
crosshairTooltip.style.display = 'none';
document.body.appendChild(crosshairTooltip);
// 添加鼠标悬停事件显示提示
mainChart.subscribeCrosshairMove(param => {
if (param.time && param.point) {
const timeStr = param.time;
const markers = [...buyMarkers, ...sellMarkers].filter(m => m.time === timeStr);
// 显示时区调试信息
if (window.debugMode) {
const timezone = $('#timezone').val();
const formattedTime = formatTimeWithTimezone(timeStr * 1000, timezone);
// 获取当前价格 - 通过param.seriesPrices获取
let priceInfo = '';
if (param.seriesPrices && param.seriesPrices.size > 0) {
// 尝试从蜡烛图系列获取价格
if (tvWidget.series.candleSeries && param.seriesPrices.get(tvWidget.series.candleSeries)) {
const price = param.seriesPrices.get(tvWidget.series.candleSeries);
priceInfo = `价格: ${price.toFixed(2)}`;
}
// 如果没有蜡烛图系列价格,尝试从线图系列获取
else if (tvWidget.series.lineSeries && param.seriesPrices.get(tvWidget.series.lineSeries)) {
const price = param.seriesPrices.get(tvWidget.series.lineSeries);
priceInfo = `价格: ${price.toFixed(2)}`;
}
}
// 仅记录最简短的调试信息
console.debug(`十字线: ${timeStr} -> ${formattedTime} (${timezone})`);
// 显示自定义时区工具提示,包含价格信息
crosshairTooltip.innerHTML = `<div style="font-weight:bold">时间: ${formattedTime}</div>` +
(priceInfo ? `<div>${priceInfo}</div>` : '');
crosshairTooltip.style.display = 'block';
crosshairTooltip.style.left = (param.point.x + 15) + 'px';
crosshairTooltip.style.top = (param.point.y - 30) + 'px';
}
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';
crosshairTooltip.style.display = 'none';
}
});
// 处理图表缩放、平移等事件,隐藏提示
mainChart.timeScale().subscribeVisibleTimeRangeChange(() => {
tooltipElement.style.display = 'none';
crosshairTooltip.style.display = 'none';
});
}
// 辅助函数:使用指定时区格式化时间戳
function formatTimeWithTimezone(timestamp, timezone) {
try {
return new Date(timestamp).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);
return new Date(timestamp).toLocaleString();
}
}
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 },
{ data: 'macd_div', render: formatMacdValue }
]
});
// 线段数据表 - 优先使用元素时间周期数据
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 {
// 设置默认值为BTC/USDT:USDT
$select.val('BTC/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() {
// 从本地存储中恢复时区设置
const savedTimezone = localStorage.getItem('selectedTimezone');
if (savedTimezone) {
$('#timezone').val(savedTimezone);
console.log('从本地存储恢复时区设置:', savedTimezone);
}
// 初始化交易对下拉菜单
$('#symbol').val('BTC/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();
// 确保在文档加载完成后初始化时区设置
// 默认设置为Shanghai时区
if (!$('#timezone').val()) {
$('#timezone').val('Asia/Shanghai');
}
// 记录当前时区设置
console.log('页面加载完成,当前时区设置:', $('#timezone').val());
// 添加自定义事件处理 - 让时区选择变更立即生效
$('#timezone').on('change', function() {
const newTimezone = $(this).val();
console.log('时区已更改为:', newTimezone);
// 保存到本地存储,下次访问时自动使用
localStorage.setItem('selectedTimezone', newTimezone);
// 如果已有数据,重新渲染图表和表格
if (currentData) {
// 先销毁现有图表实例
if (tvWidget.mainChart) {
try {
// 销毁主图表及其关联的线系列
tvWidget.mainChart = null;
tvWidget.volumeChart = null;
tvWidget.macdChart = null;
// 重置系列数据
tvWidget.series = {
candleSeries: null,
lineSeries: null,
volumeSeries: null,
macdLineSeries: null,
signalLineSeries: null,
histogramSeries: null,
mainBiSeries: [],
mainSegSeries: [],
mainZsSeries: [],
mainUncompletedZsSeries: [],
elementBiSeries: [],
elementSegSeries: [],
elementZsSeries: [],
elementUncompletedZsSeries: [],
tradePointSeries: []
};
} catch (e) {
console.error('销毁图表错误:', e);
}
}
// 使用新的时区重新初始化图表
initTradingView($('#symbol').val(), $('#timeframe').val());
// 重新渲染图表数据
renderChart();
// 更新表格
updateTables(currentData);
}
});
});
// 自动刷新相关变量
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 },
{ data: 'macd_div', render: formatMacdValue }
]
});
// 线段数据表更新
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);
}
function refreshChartOnly() {
// 仅使用当前数据刷新图表显示,不从服务器加载新数据
if (currentData) {
console.log('仅刷新图表显示,不重新获取数据');
refreshChart(currentData);
} else {
console.log('没有当前数据,无法刷新显示');
}
}
// 绑定MACD显示开关
$('#showMacd').change(function() {
updateChart();
});
// 绑定主周期MACD背离显示开关
$('#showMainMacdDiv').change(function() {
refreshChartOnly();
});
// 绑定次周期MACD背离显示开关
$('#showElementMacdDiv').change(function() {
refreshChartOnly();
});
// 绑定分型类型显示开关
$('#showKlcFxType').change(function() {
updateChart();
});
// 绑定小周期分型显示开关
$('#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)
})
// 数据回放相关变量
let replayTimer = null;
let replayData = null;
let currentReplayIndex = 0;
let totalReplaySteps = 0;
let isReplaying = false;
let replayStartTime = null;
let replayEndTime = null;
// 数据回放功能初始化
$(document).ready(function() {
// 开始回放按钮
$('#startReplay').click(function() {
startDataReplay();
});
// 暂停回放按钮
$('#pauseReplay').click(function() {
pauseDataReplay();
});
// 停止回放按钮
$('#stopReplay').click(function() {
stopDataReplay();
});
});
// 开始数据回放
function startDataReplay() {
// 如果自动刷新开启,需要先关闭
if ($('#autoRefresh').is(':checked')) {
$('#autoRefresh').prop('checked', false).change();
}
// 检查是否设置了时间范围
if (!$('#start_time').val() || !$('#end_time').val()) {
alert('请先设置开始时间和结束时间');
return;
}
// 如果已经在回放中,且是暂停状态,则继续回放
if (isReplaying && replayTimer === null) {
continueDataReplay();
return;
}
// 停止任何现有的回放
stopDataReplay();
// 获取回放参数
replayStartTime = new Date($('#start_time').val()).getTime();
replayEndTime = new Date($('#end_time').val()).getTime();
// 检查时间范围是否有效
if (replayStartTime >= replayEndTime) {
alert('开始时间必须早于结束时间');
return;
}
// 获取当前参数
const symbol = $('#symbol').val() || 'BTC/USDT:USDT';
const timeframe = $('#timeframe').val() || '5m';
const timezone = $('#timezone').val() || 'Asia/Shanghai';
const elementTimeframe = $('#elementTimeframe').val() || '1m';
// 显示回放状态
$('#replayStatus').text('正在加载数据...');
$('#replayProgress').show();
$('.progress-bar').css('width', '0%');
// 切换按钮状态
$('#startReplay').hide();
$('#stopReplay').show();
// 尝试使用当前已有数据
if (currentData && currentData.kline_data && currentData.kline_data.length > 0) {
const currentStart = new Date(currentData.kline_data[0].date).getTime();
const currentEnd = new Date(currentData.kline_data[currentData.kline_data.length - 1].date).getTime();
// 检查当前数据是否覆盖所需的时间范围
if (currentStart <= replayStartTime && currentEnd >= replayEndTime) {
console.log('使用当前数据进行回放,无需重新请求');
replayData = currentData;
initReplay();
return;
}
}
// 请求整个时间范围的数据
$.ajax({
url: '/api/analyze',
data: {
symbol: symbol,
timeframe: timeframe,
timezone: timezone,
element_timeframe: elementTimeframe,
start_time: replayStartTime,
end_time: replayEndTime,
elements_only: false
},
success: function(data) {
// 检查是否有有效数据
if (!data || !data.kline_data || data.kline_data.length === 0) {
$('#replayStatus').text('服务器返回的数据无效');
$('#replayProgress').hide();
$('#startReplay').show();
$('#stopReplay').hide();
console.error('服务器返回的数据无效:', data);
return;
}
// 保存完整数据
replayData = data;
// 初始化回放
initReplay();
},
error: function(jqXHR, textStatus, errorThrown) {
$('#replayStatus').text('加载数据失败');
$('#replayProgress').hide();
$('#startReplay').show();
$('#stopReplay').hide();
alert('加载数据失败: ' + (jqXHR.responseJSON?.error || errorThrown));
}
});
}
// 初始化回放
function initReplay() {
if (!replayData || !replayData.kline_data || replayData.kline_data.length === 0) {
$('#replayStatus').text('没有可回放的数据');
$('#replayProgress').hide();
$('#startReplay').show();
$('#stopReplay').hide();
return;
}
// 设置回放变量
isReplaying = true;
currentReplayIndex = 0;
totalReplaySteps = replayData.kline_data.length;
// 更新回放状态
$('#replayStatus').text(`准备回放 (0/${totalReplaySteps})`);
$('.progress-bar').css('width', '0%');
// 准备初始数据
prepareInitialReplayData();
// 开始回放
continueDataReplay();
}
// 准备初始回放数据
function prepareInitialReplayData() {
// 创建初始数据集,只包含第一根K线
const initialData = $.extend(true, {}, replayData);
// 确保第一根K线数据有效
if (!replayData.kline_data || replayData.kline_data.length === 0) {
console.error('无有效K线数据用于回放');
$('#replayStatus').text('无有效数据用于回放');
$('#replayProgress').hide();
return;
}
initialData.kline_data = [replayData.kline_data[0]];
// 确保初始K线数据的所有字段都有值
const firstKline = initialData.kline_data[0];
if (firstKline) {
// 确保K线数据的每个字段都是有效的数值
['open', 'high', 'low', 'close', 'volume'].forEach(field => {
if (firstKline[field] === null || firstKline[field] === undefined || isNaN(firstKline[field])) {
console.warn(`K线数据的${field}字段无效,设置为默认值`);
firstKline[field] = field === 'volume' ? 0 : firstKline.close || 0;
}
});
}
// 清空其他数据列表,因为还没有处理
initialData.bi_list = [];
initialData.seg_list = [];
initialData.zs_list = [];
initialData.uncompleted_zs_list = [];
initialData.element_bi_list = [];
initialData.element_seg_list = [];
initialData.element_zs_list = [];
initialData.element_uncompleted_zs_list = [];
initialData.macd_divergence = [];
initialData.klc_fx_type = [];
// 初始化MACD数据
if (initialData.macd && initialData.macd.length > 0) {
initialData.macd = [initialData.macd[0]];
// 确保MACD数据有效
const firstMacd = initialData.macd[0];
if (firstMacd) {
['dif', 'dea', 'macd'].forEach(field => {
if (firstMacd[field] === null || firstMacd[field] === undefined || isNaN(firstMacd[field])) {
console.warn(`MACD数据的${field}字段无效,设置为0`);
firstMacd[field] = 0;
}
});
}
}
// 更新图表,固定坐标轴范围
currentData = initialData;
// 清除当前图表
if (tvWidget.state.isInitialized) {
// 保存当前的图表可见范围以供后续使用
try {
tvWidget.state.visibleRange = tvWidget.mainChart.timeScale().getVisibleRange();
tvWidget.state.logicalRange = tvWidget.mainChart.timeScale().getVisibleLogicalRange();
} catch (e) {
console.error('保存图表范围失败', e);
}
}
// 刷新图表
refreshChart(initialData);
// 固定y轴范围
fixYAxisRange();
}
// 固定y轴范围,保持图表不因数据变化而变化
function fixYAxisRange() {
if (!tvWidget.mainChart) return;
// 获取全部数据的最高价和最低价,为了适当的范围
let minPrice = Infinity;
let maxPrice = -Infinity;
// 计算整个数据集的价格范围
replayData.kline_data.forEach(kline => {
const high = parseFloat(kline.high);
const low = parseFloat(kline.low);
if (high > maxPrice) maxPrice = high;
if (low < minPrice) minPrice = low;
});
// 添加一些边距(约5%
const priceRange = maxPrice - minPrice;
maxPrice += priceRange * 0.05;
minPrice -= priceRange * 0.05;
// 修复: 使用正确的API设置固定价格范围
try {
// 禁用自动缩放
tvWidget.mainChart.priceScale().applyOptions({
autoScale: false,
scaleMargins: {
top: 0.1,
bottom: 0.2
},
// 直接在options中设置最小和最大值
minimumValue: minPrice,
maximumValue: maxPrice
});
} catch (e) {
console.error('设置Y轴范围失败:', e);
}
}
// 继续回放
function continueDataReplay() {
if (!isReplaying || !replayData) return;
// 获取回放速度
const interval = parseFloat($('#replayInterval').val()) * 1000 || 1000;
// 切换按钮状态
$('#startReplay').hide();
$('#pauseReplay').show();
$('#stopReplay').show();
// 开始回放定时器
replayTimer = setInterval(replayNextStep, interval);
}
// 回放下一步
function replayNextStep() {
if (!isReplaying || currentReplayIndex >= totalReplaySteps) {
stopDataReplay();
return;
}
// 更新回放状态
currentReplayIndex++;
updateReplayStatus();
// 创建截止到当前索引的数据子集
const subsetData = createDataSubset(currentReplayIndex);
// 更新图表
currentData = subsetData;
refreshChart(subsetData);
// 固定Y轴范围
fixYAxisRange();
// 保持最新数据可见
keepLatestDataVisible();
// 如果到达最后一步,停止回放
if (currentReplayIndex >= totalReplaySteps) {
$('#replayStatus').text('回放完成');
pauseDataReplay();
}
}
// 保持最新数据可见
function keepLatestDataVisible() {
if (!tvWidget.mainChart) return;
try {
// 获取当前数据的最后一个时间点
if (currentData && currentData.kline_data && currentData.kline_data.length > 0) {
const lastBar = currentData.kline_data[currentData.kline_data.length - 1];
const lastTime = Math.floor(new Date(lastBar.date).getTime() / 1000);
// 计算时间范围
const barCount = Math.min(50, currentData.kline_data.length); // 显示最近的50根K线或所有K线
const firstVisibleIndex = Math.max(0, currentData.kline_data.length - barCount);
const firstVisibleBar = currentData.kline_data[firstVisibleIndex];
const firstTime = Math.floor(new Date(firstVisibleBar.date).getTime() / 1000);
// 设置时间轴范围
tvWidget.mainChart.timeScale().setVisibleRange({
from: firstTime,
to: lastTime
});
}
} catch (e) {
console.error('保持最新数据可见失败:', e);
}
}
// 创建数据子集
function createDataSubset(endIndex) {
// 创建数据副本
const subsetData = $.extend(true, {}, replayData);
// 截取K线数据
subsetData.kline_data = replayData.kline_data.slice(0, endIndex);
// 验证K线数据
subsetData.kline_data.forEach((kline, index) => {
['open', 'high', 'low', 'close', 'volume'].forEach(field => {
if (kline[field] === null || kline[field] === undefined || isNaN(kline[field])) {
console.warn(`K线数据[${index}]的${field}字段无效,使用替代值`);
// 对于价格字段,尝试使用其他价格字段,或者前一个K线的对应值
if (field !== 'volume') {
if (field === 'open' && kline.close !== null && !isNaN(kline.close)) {
kline[field] = kline.close;
} else if (index > 0) {
kline[field] = subsetData.kline_data[index-1][field] || 0;
} else {
kline[field] = 0;
}
} else {
kline[field] = 0; // 对于成交量,设为0
}
}
});
});
// 处理其他数据结构(笔、线段、中枢等)
// 我们需要根据当前K线的时间来过滤其他数据结构
try {
const currentEndTime = new Date(subsetData.kline_data[endIndex - 1].date).getTime();
// 过滤笔列表
if (subsetData.bi_list && subsetData.bi_list.length) {
subsetData.bi_list = filterDataByTime(replayData.bi_list, currentEndTime);
}
// 过滤线段列表
if (subsetData.seg_list && subsetData.seg_list.length) {
subsetData.seg_list = filterDataByTime(replayData.seg_list, currentEndTime);
}
// 过滤中枢列表
if (subsetData.zs_list && subsetData.zs_list.length) {
subsetData.zs_list = filterDataByTime(replayData.zs_list, currentEndTime);
}
// 过滤未完成中枢列表
if (subsetData.uncompleted_zs_list && subsetData.uncompleted_zs_list.length) {
subsetData.uncompleted_zs_list = filterDataByTime(replayData.uncompleted_zs_list, currentEndTime);
}
// 过滤小周期数据
if (subsetData.element_bi_list && subsetData.element_bi_list.length) {
subsetData.element_bi_list = filterDataByTime(replayData.element_bi_list, currentEndTime);
}
if (subsetData.element_seg_list && subsetData.element_seg_list.length) {
subsetData.element_seg_list = filterDataByTime(replayData.element_seg_list, currentEndTime);
}
if (subsetData.element_zs_list && subsetData.element_zs_list.length) {
subsetData.element_zs_list = filterDataByTime(replayData.element_zs_list, currentEndTime);
}
if (subsetData.element_uncompleted_zs_list && subsetData.element_uncompleted_zs_list.length) {
subsetData.element_uncompleted_zs_list = filterDataByTime(replayData.element_uncompleted_zs_list, currentEndTime);
}
// 过滤MACD
if (subsetData.macd && subsetData.macd.length) {
subsetData.macd = replayData.macd.slice(0, endIndex);
// 验证MACD数据
subsetData.macd.forEach((item, index) => {
['dif', 'dea', 'macd'].forEach(field => {
if (item[field] === null || item[field] === undefined || isNaN(item[field])) {
console.warn(`MACD数据[${index}]的${field}字段无效,设置为0`);
item[field] = 0;
}
});
});
}
// 过滤MACD背离
if (subsetData.macd_divergence && subsetData.macd_divergence.length) {
subsetData.macd_divergence = filterDataByTime(replayData.macd_divergence, currentEndTime);
}
// 过滤分型类型
if (subsetData.klc_fx_type && subsetData.klc_fx_type.length) {
subsetData.klc_fx_type = filterDataByTime(replayData.klc_fx_type, currentEndTime, 'time');
}
} catch (e) {
console.error('过滤数据时出错:', e);
}
return subsetData;
}
// 根据时间过滤数据
function filterDataByTime(dataList, endTime, timeField = 'end_time') {
if (!dataList || !Array.isArray(dataList)) return [];
return dataList.filter(item => {
const itemTime = new Date(item[timeField]).getTime();
return itemTime <= endTime;
});
}
// 更新回放状态显示
function updateReplayStatus() {
const percent = Math.floor((currentReplayIndex / totalReplaySteps) * 100);
$('#replayStatus').text(`回放中 (${currentReplayIndex}/${totalReplaySteps})`);
$('.progress-bar').css('width', `${percent}%`);
}
// 暂停回放
function pauseDataReplay() {
if (replayTimer) {
clearInterval(replayTimer);
replayTimer = null;
}
// 切换按钮状态
$('#pauseReplay').hide();
$('#startReplay').show();
if (currentReplayIndex >= totalReplaySteps) {
$('#replayStatus').text('回放完成');
} else {
$('#replayStatus').text(`已暂停 (${currentReplayIndex}/${totalReplaySteps})`);
}
}
// 停止回放
function stopDataReplay() {
// 清除定时器
if (replayTimer) {
clearInterval(replayTimer);
replayTimer = null;
}
// 重置回放状态
isReplaying = false;
currentReplayIndex = 0;
replayData = null;
// 更新UI
$('#replayStatus').text('');
$('#replayProgress').hide();
$('#pauseReplay').hide();
$('#stopReplay').hide();
$('#startReplay').show();
// 恢复图表自动缩放
if (tvWidget.mainChart) {
tvWidget.mainChart.priceScale().applyOptions({
autoScale: true
});
}
}
</script>
</body>
</html>