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