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