Files
chan.py/web/templates/index.html
T
2025-07-04 00:54:31 +08:00

6874 lines
326 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html>
<head>
<title>缠论分析系统</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css" rel="stylesheet">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.datatables.net/1.11.5/js/jquery.dataTables.min.js"></script>
<link href="https://cdn.datatables.net/1.11.5/css/jquery.dataTables.min.css" rel="stylesheet">
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- TradingView Widget BEGIN -->
<script src="https://cdn.jsdelivr.net/npm/lightweight-charts@4.0.1/dist/lightweight-charts.standalone.production.js"></script>
<!-- TradingView Widget END -->
<style>
body {
font-family: "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
margin: 0;
padding: 10px;
background-color: #f8f9fa;
color: #333;
}
.container {
max-width: 100%;
margin: 0 auto;
background-color: white;
padding: 5px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.header {
margin-bottom: 0px;
padding-bottom: 0px;
}
.controls {
margin-bottom: 10px;
padding: 10px;
background-color: #f8f9fa;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.form-label {
font-weight: 500;
margin-bottom: 6px;
color: #495057;
}
.chart-container {
width: 100%;
height: 700px;
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);
margin-bottom: 20px; /* 增加下边距,防止被下方数据面板遮挡 */
}
#tradingview_chart {
width: 100%;
height: 100%;
position: relative;
z-index: 1; /* 确保图表在数据面板之上 */
}
.chart-options {
position: absolute;
top: 10px;
right: 10px;
z-index: 100;
background-color: rgba(255, 255, 255, 0.9);
padding: 12px;
border-radius: 8px;
font-size: 13px;
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
}
.data-container {
margin-top: 30px; /* 增加上边距,避免与图表重叠 */
position: relative;
z-index: 10;
background-color: white;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
clear: both; /* 清除浮动 */
}
.nav-tabs {
margin-bottom: 10px;
position: relative;
z-index: 20;
background-color: white;
border-radius: 8px 8px 0 0;
padding: 10px 10px 0 10px;
}
.nav-tabs .nav-link {
border-radius: 6px 6px 0 0;
margin-right: 5px;
font-weight: 500;
transition: all 0.2s ease;
}
.nav-tabs .nav-link:hover {
background-color: #f8f9fa;
border-color: #dee2e6;
}
.nav-tabs .nav-link.active {
background-color: #0d6efd;
color: white;
border-color: #0d6efd;
}
/* 特别突出显示股票筛选tab */
#stock-filter-tab {
background-color: #28a745 !important;
color: white !important;
border-color: #28a745 !important;
font-weight: bold !important;
box-shadow: 0 2px 4px rgba(40, 167, 69, 0.3) !important;
}
#stock-filter-tab:hover {
background-color: #218838 !important;
border-color: #1e7e34 !important;
}
#stock-filter-tab.active {
background-color: #155724 !important;
border-color: #155724 !important;
}
.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;
}
/* 主次周期元素样式区分 */
/* 均线指标样式 */
.ma-indicators {
position: absolute;
top: 10px;
left: 0;
z-index: 100;
background-color: rgba(255, 255, 255, 0.95);
padding: 8px 12px;
border-radius: 0 6px 6px 0;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
max-width: calc(100% - 200px);
border-left: none;
}
.ma-indicators .d-flex {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.ma-indicators .d-flex .d-flex {
flex-direction: row;
align-items: center;
gap: 8px;
}
.ma-indicator-item {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
background-color: rgba(0,0,0,0.05);
border: 1px solid rgba(0,0,0,0.1);
width: 100%;
box-sizing: border-box;
}
.ma-indicator-color {
width: 12px;
height: 2px;
border-radius: 1px;
}
.ma-indicator-controls {
display: flex;
flex-direction: row;
margin-top: 2px;
gap: 2px;
opacity: 0.6;
transition: opacity 0.3s ease;
width: 100%;
justify-content: flex-end;
}
.ma-indicator-item:hover .ma-indicator-controls {
opacity: 1;
}
.ma-control-btn {
width: 16px;
height: 16px;
border: none;
background: transparent;
color: #666;
cursor: pointer;
border-radius: 2px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
}
.ma-control-btn:hover {
background-color: rgba(0,0,0,0.1);
color: #333;
}
.ma-indicator-value {
font-weight: bold;
}
</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-1">
<label for="dataSource" class="form-label">数据源:</label>
<select id="dataSource" class="form-select">
<option value="crypto" selected>加密货币</option>
<option value="a_stock">A股</option>
</select>
</div>
<div class="col-md-2" id="cryptoSymbolContainer">
<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" id="astockSymbolContainer" style="display:none;">
<label for="astockSymbol" class="form-label">A股股票:</label>
<select id="astockSymbol" class="form-select">
{% for stock in a_stock_symbols %}
<option value="{{ stock.symbol }}" {% if stock.symbol == '000001' %}selected{% endif %}>{{ stock.symbol }} - {{ stock.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-1">
<label for="timeframe" class="form-label">时间周期:</label>
<select id="timeframe" class="form-select">
<option value="1m">1分钟</option>
<option value="3m">3分钟</option>
<option value="5m" selected>5分钟</option>
<option value="15m">15分钟</option>
<option value="30m">30分钟</option>
<option value="1h">1小时</option>
<option value="2h">2小时</option>
<option value="4h">4小时</option>
<option value="6h">6小时</option>
<option value="8h">8小时</option>
<option value="12h">12小时</option>
<option value="1d">1日</option>
<option value="3d">3日</option>
<option value="1w">1周</option>
<option value="1M">1月</option>
</select>
</div>
<div class="col-md-1">
<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-3">
<label for="start_time" class="form-label">开始时间:</label>
<input type="datetime-local" id="start_time" class="form-control">
</div>
<div class="col-md-3">
<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()" style="padding: 8px 6px; font-size: 14px;">
分析
</button>
</div>
</div>
<div class="row mt-3">
<div class="col-md-8">
<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>
<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="showTradePoints" checked>
<label class="form-check-label" for="showTradePoints">买卖点</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showMainBollinger">
<label class="form-check-label" for="showMainBollinger">布林带</label>
</div>
<div class="d-inline-block ms-3">
<button type="button" class="btn btn-sm btn-outline-primary" id="addMovingAverageBtn" title="添加均线">
<i class="bi bi-graph-up"></i> 均线
</button>
<button type="button" class="btn btn-sm btn-outline-secondary ms-2" onclick="window.testMovingAverageSystem()" title="测试均线系统">
<i class="bi bi-bug"></i>
</button>
</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">
<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="showElementTradePoints">
<label class="form-check-label" for="showElementTradePoints">买卖点</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showElementBollinger">
<label class="form-check-label" for="showElementBollinger">布林带</label>
</div>
</div>
</div>
<div class="col-md-4">
<div class="d-flex align-items-center mb-2">
<label for="elementTimeframe" class="form-label me-2 mb-0">次级别时间周期:</label>
<select id="elementTimeframe" class="form-select form-select-sm me-2" style="width: 120px;">
<option value="1m" selected>1分钟</option>
<option value="3m">3分钟</option>
<option value="5m">5分钟</option>
<option value="15m">15分钟</option>
<option value="30m">30分钟</option>
<option value="1h">1小时</option>
<option value="2h">2小时</option>
<option value="4h">4小时</option>
<option value="6h">6小时</option>
<option value="8h">8小时</option>
<option value="12h">12小时</option>
<option value="1d">1日</option>
<option value="3d">3日</option>
<option value="1w">1周</option>
<option value="1M">1月</option>
</select>
</div>
<div class="d-flex align-items-center mb-2">
<label for="refreshInterval" class="form-label me-2 mb-0">自动刷新:</label>
<select id="refreshInterval" class="form-select form-select-sm me-2" style="width: 80px;">
<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 me-2">
<input class="form-check-input" type="checkbox" id="autoRefresh">
<label class="form-check-label" for="autoRefresh">启用</label>
</div>
<span id="nextRefreshTime" class="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">
<label for="replayInterval" class="form-label me-2 mb-0">数据回放:</label>
<select id="replayInterval" class="form-select form-select-sm me-2" style="width: 80px;">
<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="text-muted" style="font-size:0.85rem;"></span>
<div id="replayProgress" class="progress ms-2" style="width: 80px; 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="movingAverageIndicators" class="ma-indicators" style="display: none;">
<div class="d-flex flex-column gap-2 align-items-start">
<div id="maIndicatorsList" class="d-flex flex-column gap-2"></div>
</div>
</div>
<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>
<li class="nav-item" role="presentation">
<button class="nav-link" id="stock-filter-tab" data-bs-toggle="tab" data-bs-target="#stock-filter" 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>方向</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>
<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 class="tab-pane fade" id="stock-filter" role="tabpanel">
<div class="container-fluid">
<div class="row mb-3">
<div class="col-md-12">
<h5>A股强分型筛选</h5>
<p class="text-muted">筛选最近2个K线合并(KLC)中有一个满足分型强度≥1.0且分型类型不为UNKNOWN的A股股票</p>
</div>
</div>
<div class="row mb-3">
<div class="col-md-3">
<label for="filterStartTime" class="form-label">开始时间:</label>
<input type="datetime-local" id="filterStartTime" class="form-control">
</div>
<div class="col-md-3">
<label for="filterEndTime" class="form-label">结束时间:</label>
<input type="datetime-local" id="filterEndTime" class="form-control">
</div>
<div class="col-md-3">
<label for="filterTimeframe" class="form-label">时间周期:</label>
<select id="filterTimeframe" class="form-select">
<option value="1m">1分钟</option>
<option value="3m">3分钟</option>
<option value="5m">5分钟</option>
<option value="15m">15分钟</option>
<option value="30m">30分钟</option>
<option value="1h">1小时</option>
<option value="2h">2小时</option>
<option value="4h">4小时</option>
<option value="6h">6小时</option>
<option value="8h">8小时</option>
<option value="12h">12小时</option>
<option value="1d" selected>1日</option>
<option value="3d">3日</option>
<option value="1w">1周</option>
<option value="1M">1月</option>
</select>
</div>
<div class="col-md-3">
<label for="fxStrengthThreshold" class="form-label">分型强度阈值:</label>
<input type="number" id="fxStrengthThreshold" class="form-control" value="1.0" min="0" max="100" step="0.1">
</div>
</div>
<div class="row mb-3">
<div class="col-md-12">
<button class="btn btn-primary" onclick="filterStocks()">
<i class="bi bi-search"></i> 开始筛选
</button>
<button class="btn btn-secondary ms-2" onclick="exportFilterResults()">
<i class="bi bi-download"></i> 导出结果
</button>
<span id="filterProgress" class="ms-3" style="display:none;">
<i class="bi bi-hourglass-split"></i> 正在筛选中...
</span>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="table-container">
<table id="stockFilterTable" class="display compact" style="width:100%">
<thead>
<tr>
<th>股票代码</th>
<th>股票名称</th>
<th>分型时间</th>
<th>分型类型</th>
<th>分型强度</th>
<th>分型价格</th>
<th>当前价格</th>
<th>涨跌幅(%)</th>
<th>操作</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</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,
atrLineSeries: null,
mainBiSeries: [],
mainSegSeries: [],
mainZsSeries: [],
mainUncompletedZsSeries: [],
elementBiSeries: [],
elementSegSeries: [],
elementZsSeries: [],
elementUncompletedZsSeries: [],
tradePointSeries: [],
mainBollingerSeries: [],
elementBollingerSeries: [],
movingAverageSeries: []
},
state: {
isInitialized: false,
visibleRange: null,
logicalRange: null
}
};
// 均线系统相关变量
let movingAverages = [];
let maCounter = 0;
// 均线计算函数
function calculateMovingAverage(data, period, type = 'SMA') {
if (!data || data.length < period) return [];
const result = [];
if (type === 'SMA') {
// 简单移动平均线
for (let i = period - 1; i < data.length; i++) {
const sum = data.slice(i - period + 1, i + 1).reduce((acc, val) => acc + val.value, 0);
result.push({
time: data[i].time,
value: sum / period
});
}
} else if (type === 'EMA') {
// 指数移动平均线
const multiplier = 2 / (period + 1);
let ema = data.slice(0, period).reduce((acc, val) => acc + val.value, 0) / period;
result.push({
time: data[period - 1].time,
value: ema
});
for (let i = period; i < data.length; i++) {
ema = (data[i].value * multiplier) + (ema * (1 - multiplier));
result.push({
time: data[i].time,
value: ema
});
}
} else if (type === 'WMA') {
// 加权移动平均线
const weights = [];
let weightSum = 0;
for (let i = 1; i <= period; i++) {
weights.push(i);
weightSum += i;
}
for (let i = period - 1; i < data.length; i++) {
let weightedSum = 0;
for (let j = 0; j < period; j++) {
weightedSum += data[i - period + 1 + j].value * weights[j];
}
result.push({
time: data[i].time,
value: weightedSum / weightSum
});
}
}
return result;
}
// 获取价格数据源
function getPriceData(klineData, source) {
// 如果没有传入K线数据,使用当前数据源
if (!klineData) {
// 检查是否使用小周期K线数据
const useElementPeriod = $('#elementPeriodKline').is(':checked') &&
currentData.element_kline_data &&
Array.isArray(currentData.element_kline_data);
console.log('均线使用数据源:', useElementPeriod ? '小周期' : '主周期');
klineData = useElementPeriod ? currentData.element_kline_data : currentData.kline_data;
}
if (!klineData || !Array.isArray(klineData)) {
console.warn('K线数据无效:', klineData);
return [];
}
return klineData.map(kline => {
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
let value;
switch (source) {
case 'open':
value = parseFloat(kline.open);
break;
case 'high':
value = parseFloat(kline.high);
break;
case 'low':
value = parseFloat(kline.low);
break;
case 'close':
value = parseFloat(kline.close);
break;
case 'hl2':
value = (parseFloat(kline.high) + parseFloat(kline.low)) / 2;
break;
case 'hlc3':
value = (parseFloat(kline.high) + parseFloat(kline.low) + parseFloat(kline.close)) / 3;
break;
case 'ohlc4':
value = (parseFloat(kline.open) + parseFloat(kline.high) + parseFloat(kline.low) + parseFloat(kline.close)) / 4;
break;
default:
value = parseFloat(kline.close);
}
return { time: timestamp, value: value };
});
}
// 应用平滑处理
function applySmoothing(data, smoothingType, smoothingPeriod) {
if (smoothingType === 'none' || !smoothingPeriod || smoothingPeriod < 2) {
return data;
}
return calculateMovingAverage(data, smoothingPeriod, smoothingType);
}
// 添加均线到图表
function addMovingAverageToChart(config) {
const ma = {
id: `ma_${++maCounter}`,
...config,
series: null,
visible: true
};
if (!currentData) {
console.error('没有数据,无法添加均线');
return null;
}
// 获取价格数据(自动选择数据源)
const priceData = getPriceData(null, config.source);
// 计算均线
let maData = calculateMovingAverage(priceData, config.period, config.type);
// 应用平滑处理
if (config.smoothing !== 'none' && config.smoothingPeriod > 1) {
maData = applySmoothing(maData, config.smoothing, config.smoothingPeriod);
}
// 添加到图表
const lineSeries = tvWidget.mainChart.addLineSeries({
color: config.color,
lineWidth: config.lineWidth,
lineStyle: config.style === 'dashed' ? 1 : config.style === 'dotted' ? 3 : 0,
title: `${config.type}(${config.period})`,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: true
});
lineSeries.setData(maData);
ma.series = lineSeries;
ma.data = maData;
movingAverages.push(ma);
tvWidget.series.movingAverageSeries.push(lineSeries);
// 更新显示
updateMovingAverageIndicators();
return ma;
}
// 删除均线
function removeMovingAverage(maId) {
console.log('删除均线:', maId);
try {
const index = movingAverages.findIndex(ma => ma.id === maId);
if (index !== -1) {
const ma = movingAverages[index];
console.log('找到要删除的均线:', ma);
if (ma.series && tvWidget.mainChart) {
tvWidget.mainChart.removeSeries(ma.series);
// 从 tvWidget.series.movingAverageSeries 中移除
const seriesIndex = tvWidget.series.movingAverageSeries.indexOf(ma.series);
if (seriesIndex !== -1) {
tvWidget.series.movingAverageSeries.splice(seriesIndex, 1);
}
}
movingAverages.splice(index, 1);
updateMovingAverageIndicators();
console.log('均线删除成功');
} else {
console.warn('未找到要删除的均线:', maId);
}
} catch (error) {
console.error('删除均线时出错:', error);
}
}
// 切换均线显示/隐藏
function toggleMovingAverage(maId) {
console.log('切换均线显示状态:', maId);
try {
const ma = movingAverages.find(ma => ma.id === maId);
if (ma && ma.series) {
ma.visible = !ma.visible;
console.log('均线新状态:', ma.visible ? '显示' : '隐藏');
ma.series.applyOptions({
visible: ma.visible
});
updateMovingAverageIndicators();
console.log('均线状态切换成功');
} else {
console.warn('未找到均线或series不存在:', maId, ma);
}
} catch (error) {
console.error('切换均线显示状态时出错:', error);
}
}
// 更新均线指标显示
function updateMovingAverageIndicators() {
console.log('更新均线指标显示,当前均线数量:', movingAverages.length);
try {
const container = $('#movingAverageIndicators');
const list = $('#maIndicatorsList');
if (movingAverages.length === 0) {
container.hide();
console.log('没有均线,隐藏指标容器');
return;
}
list.empty();
movingAverages.forEach((ma, index) => {
console.log(`创建均线指标 ${index + 1}:`, ma.id, ma.type, ma.period);
const latestValue = ma.data && ma.data.length > 0 ? ma.data[ma.data.length - 1].value : 0;
const smoothingText = ma.smoothing !== 'none' ? `/${ma.smoothing}(${ma.smoothingPeriod})` : '';
const indicator = $(`
<div class="ma-indicator-item" data-ma-id="${ma.id}" style="opacity: ${ma.visible ? 1 : 0.5}">
<div class="ma-indicator-color" style="background-color: ${ma.color}"></div>
<span>${ma.type}(${ma.period})${smoothingText}</span>
<span class="ma-indicator-value" style="color: ${ma.color}">${latestValue.toFixed(2)}</span>
<div class="ma-indicator-controls">
<button class="ma-control-btn" onclick="window.toggleMovingAverage('${ma.id}')" title="${ma.visible ? '隐藏' : '显示'}">
<i class="bi ${ma.visible ? 'bi-eye-slash' : 'bi-eye'}"></i>
</button>
<button class="ma-control-btn" onclick="window.editMovingAverage('${ma.id}')" title="配置">
<i class="bi bi-gear"></i>
</button>
<button class="ma-control-btn" onclick="window.removeMovingAverage('${ma.id}')" title="删除">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
`);
list.append(indicator);
});
container.show();
console.log('均线指标显示更新完成');
} catch (error) {
console.error('更新均线指标显示时出错:', error);
}
}
// 编辑均线配置
function editMovingAverage(maId) {
console.log('编辑均线配置:', maId);
try {
const ma = movingAverages.find(ma => ma.id === maId);
if (!ma) {
console.warn('未找到要编辑的均线:', maId);
return;
}
console.log('找到均线配置:', ma);
// 填充表单
$('#maType').val(ma.type);
$('#maPeriod').val(ma.period);
$('#maSource').val(ma.source);
$('#maColor').val(ma.color);
$('#maSmoothing').val(ma.smoothing);
$('#maSmoothingPeriod').val(ma.smoothingPeriod);
$('#maLineWidth').val(ma.lineWidth);
$('#lineWidthDisplay').text(ma.lineWidth);
$('#maStyle').val(ma.style);
// 更新确认按钮
$('#addMovingAverageConfirm').text('更新均线').data('editId', maId);
// 启用/禁用平滑周期
$('#maSmoothingPeriod').prop('disabled', ma.smoothing === 'none');
// 显示对话框
const modal = new bootstrap.Modal(document.getElementById('movingAverageModal'));
modal.show();
console.log('编辑对话框已显示');
} catch (error) {
console.error('编辑均线配置时出错:', error);
}
}
// 更新所有均线数据
function updateAllMovingAverages() {
if (!currentData) return;
console.log('更新所有均线数据,均线数量:', movingAverages.length);
movingAverages.forEach(ma => {
// 自动选择数据源(主周期或次周期)
const priceData = getPriceData(null, ma.source);
if (priceData.length === 0) {
console.warn('均线数据为空,跳过:', ma.id);
return;
}
let maData = calculateMovingAverage(priceData, ma.period, ma.type);
if (ma.smoothing !== 'none' && ma.smoothingPeriod > 1) {
maData = applySmoothing(maData, ma.smoothing, ma.smoothingPeriod);
}
ma.data = maData;
// 如果序列不存在或无效,重新创建
if (!ma.series && tvWidget.mainChart) {
console.log('重新创建均线序列:', ma.type, ma.period);
try {
ma.series = tvWidget.mainChart.addLineSeries({
color: ma.color,
lineWidth: ma.lineWidth,
lineStyle: ma.style === 'solid' ? 0 :
ma.style === 'dotted' ? 1 :
ma.style === 'dashed' ? 2 : 0,
title: `${ma.type}(${ma.period})`,
visible: ma.visible !== false,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: true
});
// 添加到movingAverageSeries数组
if (!tvWidget.series.movingAverageSeries) {
tvWidget.series.movingAverageSeries = [];
}
tvWidget.series.movingAverageSeries.push(ma.series);
console.log('均线序列创建成功:', ma.id);
} catch (error) {
console.error('创建均线序列失败:', error);
return;
}
} else if (!tvWidget.mainChart) {
console.warn('主图表不存在,跳过均线更新:', ma.id);
return;
}
// 设置数据
if (ma.series) {
ma.series.setData(maData);
console.log(`均线 ${ma.type}(${ma.period}) 数据已更新,数据点数:`, maData.length);
}
});
updateMovingAverageIndicators();
}
// 确保函数在全局作用域中可用
window.removeMovingAverage = removeMovingAverage;
window.toggleMovingAverage = toggleMovingAverage;
window.editMovingAverage = editMovingAverage;
// 测试函数 - 用于验证均线系统是否工作
window.testMovingAverageSystem = function() {
console.log('=== 均线系统测试 ===');
console.log('当前均线数量:', movingAverages.length);
console.log('均线列表:', movingAverages);
console.log('图表对象:', tvWidget);
console.log('主图表:', tvWidget.mainChart);
console.log('=================');
if (movingAverages.length === 0) {
alert('当前没有添加任何均线。请先添加一个均线来测试。');
} else {
alert(`均线系统正常,当前有 ${movingAverages.length} 条均线。请检查浏览器控制台查看详细信息。`);
}
};
// 添加默认EMA线
function addDefaultEMALines() {
if (!tvWidget.mainChart || !currentData || !currentData.kline_data) {
console.log('图表或数据未准备好,跳过默认EMA添加');
return;
}
console.log('开始检查和添加默认EMA线');
// 默认EMA配置
const defaultEMAConfigs = [
{ period: 5, color: '#FF0000', name: 'EMA5' }, // 红色
{ period: 10, color: '#0000FF', name: 'EMA10' }, // 蓝色
{ period: 26, color: '#006400', name: 'EMA26' }, // 深绿色
{ period: 52, color: '#800080', name: 'EMA52' } // 紫色
];
// 延迟添加EMA,确保图表完全准备好
setTimeout(() => {
defaultEMAConfigs.forEach(config => {
// 检查是否已经存在相同周期的EMA
const existingEMA = movingAverages.find(ma =>
ma.type === 'EMA' &&
ma.period === config.period &&
ma.source === 'close'
);
if (existingEMA) {
console.log(`${config.name}已存在,跳过添加`);
return;
}
try {
const emaConfig = {
type: 'EMA',
period: config.period,
source: 'close',
color: config.color,
smoothing: 'none',
smoothingPeriod: 3,
lineWidth: 1,
style: 'solid'
};
addMovingAverageToChart(emaConfig);
console.log(`已添加默认${config.name},颜色:${config.color}`);
} catch (error) {
console.error(`添加${config.name}时出错:`, error);
}
});
console.log('默认EMA线检查和添加完成');
}, 100);
}
// 初始化均线系统事件处理程序
function initMovingAverageEvents() {
// 均线按钮点击事件
$('#addMovingAverageBtn').click(function() {
// 重置表单
$('#movingAverageForm')[0].reset();
$('#maType').val('SMA');
$('#maPeriod').val(20);
$('#maSource').val('close');
$('#maColor').val('#2962FF');
$('#maSmoothing').val('none');
$('#maSmoothingPeriod').val(3).prop('disabled', true);
$('#maLineWidth').val(2);
$('#lineWidthDisplay').text('2');
$('#maStyle').val('solid');
// 重置确认按钮
$('#addMovingAverageConfirm').text('添加均线').removeData('editId');
// 显示对话框
const modal = new bootstrap.Modal(document.getElementById('movingAverageModal'));
modal.show();
});
// 平滑算法变更事件
$('#maSmoothing').change(function() {
const smoothing = $(this).val();
$('#maSmoothingPeriod').prop('disabled', smoothing === 'none');
});
// 线宽滑块变更事件
$('#maLineWidth').on('input', function() {
$('#lineWidthDisplay').text($(this).val());
});
// 确认添加/更新均线
$('#addMovingAverageConfirm').click(function() {
const form = $('#movingAverageForm')[0];
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const editId = $(this).data('editId');
const config = {
type: $('#maType').val(),
period: parseInt($('#maPeriod').val()),
source: $('#maSource').val(),
color: $('#maColor').val(),
smoothing: $('#maSmoothing').val(),
smoothingPeriod: parseInt($('#maSmoothingPeriod').val()) || 3,
lineWidth: parseInt($('#maLineWidth').val()),
style: $('#maStyle').val()
};
if (editId) {
// 更新现有均线
const ma = movingAverages.find(ma => ma.id === editId);
if (ma) {
// 删除旧系列
if (ma.series) {
tvWidget.mainChart.removeSeries(ma.series);
const seriesIndex = tvWidget.series.movingAverageSeries.indexOf(ma.series);
if (seriesIndex !== -1) {
tvWidget.series.movingAverageSeries.splice(seriesIndex, 1);
}
}
// 更新配置
Object.assign(ma, config);
// 重新计算和添加
const klineData = currentData.kline_data;
const priceData = getPriceData(klineData, config.source);
let maData = calculateMovingAverage(priceData, config.period, config.type);
if (config.smoothing !== 'none' && config.smoothingPeriod > 1) {
maData = applySmoothing(maData, config.smoothing, config.smoothingPeriod);
}
const lineSeries = tvWidget.mainChart.addLineSeries({
color: config.color,
lineWidth: config.lineWidth,
lineStyle: config.style === 'dashed' ? 1 : config.style === 'dotted' ? 3 : 0,
title: `${config.type}(${config.period})`,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: true
});
lineSeries.setData(maData);
ma.series = lineSeries;
ma.data = maData;
tvWidget.series.movingAverageSeries.push(lineSeries);
updateMovingAverageIndicators();
}
} else {
// 添加新均线
addMovingAverageToChart(config);
}
// 关闭对话框
const modal = bootstrap.Modal.getInstance(document.getElementById('movingAverageModal'));
modal.hide();
});
}
// 买卖点样式定义 - 根据desc字段直接显示
function getTradePointStyle(point) {
const isBuy = point.type > 0;
const desc = point.desc || ''; // 使用后端返回的描述,如 "T1", "T2", "T1P" 等
// 直接使用描述作为显示文本,加上买卖前缀
const text = isBuy ? `B${desc}` : `S${desc}`;
// 根据买卖和描述设置颜色
let color;
if (isBuy) {
// 买点用红色系
if (desc.includes('1')) {
color = '#FF1744'; // 一类买点(包括1P
} else if (desc.includes('2')) {
color = '#F50057'; // 二类买点(包括2S
} else if (desc.includes('3')) {
color = '#D500F9'; // 三类买点(包括3A、3B
} else {
color = '#FF5722'; // 其他买点
}
} else {
// 卖点用绿色系
if (desc.includes('1')) {
color = '#00E676'; // 一类卖点(包括1P
} else if (desc.includes('2')) {
color = '#00B0FF'; // 二类卖点(包括2S
} else if (desc.includes('3')) {
color = '#FFEA00'; // 三类卖点(包括3A、3B
} else {
color = '#4CAF50'; // 其他卖点
}
}
return {
color: color,
shape: isBuy ? 'arrowUp' : 'arrowDown', // 恢复shape属性
text: text,
size: 3
};
}
// 同一时间点的标记堆叠间距系数
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);
// 添加自定义十字线信息显示
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);
// 助手函数:转换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);
});
// 添加小周期买卖点复选框变更事件
$('#showElementTradePoints').change(function() {
refreshChart(currentData);
});
// 添加分型类型复选框变更事件
$('#showKlcFxType').change(function() {
refreshChartOnly();
});
// 添加布林带显示变更事件
$('#showMainBollinger').change(function() {
updateChartDisplay();
});
$('#showElementBollinger').change(function() {
updateChartDisplay();
});
// 添加K线周期切换事件监听器
$('input[name="klinePeriod"]').change(function() {
console.log('K线周期切换:', $(this).attr('id'), $(this).is(':checked'));
updateChartDisplay();
// 更新数据源信息
if (currentData) {
setupDataSourceInfo(currentData);
}
});
// 当选择不同的元素时间周期时
$('#elementTimeframe').change(function() {
const elementTimeframe = $(this).val();
const mainTimeframe = $('#timeframe').val();
// 检查选择的元素时间周期是否小于等于主周期
if (compareTimeframes(elementTimeframe, mainTimeframe) > 0) {
alert('元素时间周期必须小于或等于主图表时间周期。');
setSmallerOrEqualTimeframe(); // 重置为最大的小于等于时间周期
return;
}
console.log(`当前选择的元素时间周期: ${elementTimeframe},需要点击分析按钮来应用更改`);
});
// 比较两个时间周期的大小
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:', true);
console.log('- 显示成交量:', false);
console.log('- 显示分型类型:', $('#showKlcFxType').is(':checked'));
console.log('- 显示小周期分型:', $('#showElementKlcFxType').is(':checked'));
console.log('- 显示KLU分型:', $('#showKluFxType').is(':checked'));
console.log('- 显示小周期KLU分型:', $('#showElementKluFxType').is(':checked'));
console.log('- 显示买卖点:', $('#showTradePoints').is(':checked'));
console.log('- 显示原始K线:', $('#showOriginalKline').is(':checked'));
console.log('- 显示主周期布林带:', $('#showMainBollinger').is(':checked'));
console.log('- 显示次周期布林带:', $('#showElementBollinger').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 formatConfirmTime(timeStr) {
if (!timeStr || timeStr === null || timeStr === 'null' || timeStr === '') {
return '<span class="text-muted">未确认</span>';
}
return formatTime(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 formatTradePointType(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>';
}
function updateChart() {
// 只显示旋转加载图标
$('#refreshLoadingSpinner').show();
// 在开始更新前保存当前所有图表的视图状态
let savedViewState = null;
if (tvWidget && tvWidget.state && tvWidget.state.isInitialized) {
savedViewState = {
visibleRange: null,
logicalRange: null
};
// 保存主图表的视图状态
if (tvWidget.mainChart && tvWidget.mainChart.timeScale) {
try {
savedViewState.visibleRange = tvWidget.mainChart.timeScale().getVisibleRange();
savedViewState.logicalRange = tvWidget.mainChart.timeScale().getVisibleLogicalRange();
console.log('已保存图表视图状态:', savedViewState);
} catch (e) {
console.warn('保存图表视图状态时出错:', e);
}
}
}
// 获取参数
const dataSource = $('#dataSource').val() || 'crypto';
let symbol;
if (dataSource === 'crypto') {
symbol = $('#symbol').val() || 'BTC/USDT:USDT';
} else {
symbol = $('#astockSymbol').val() || '000001';
}
const timeframe = $('#timeframe').val() || '5m';
const timezone = $('#timezone').val() || 'Asia/Shanghai';
const elementTimeframe = $('#elementTimeframe').val() || '1m';
// 确保时区参数有效
console.log('更新图表使用时区:', timezone);
console.log('数据源:', dataSource, '交易对/股票:', symbol);
// 如果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;
// 将保存的视图状态传递给刷新函数
if (savedViewState) {
data._savedViewState = savedViewState;
}
// 检查和记录服务器返回的时区
console.log('服务器返回的时区:', data.timezone || '未指定');
// 刷新图表
refreshChart(data);
// 更新均线数据
setTimeout(() => {
updateAllMovingAverages();
}, 100);
},
error: function(jqXHR, textStatus, errorThrown) {
// 隐藏加载图标
$('#refreshLoadingSpinner').hide();
// 显示错误信息
console.error('加载数据失败:', errorThrown);
alert('加载数据失败: ' + (jqXHR.responseJSON?.error || errorThrown));
}
});
}
// 初始化图表
function initTradingView(symbol, timeframe) {
try {
console.log('初始化TradingView图表:', symbol, timeframe);
// 获取当前交易对的配置
const symbolConfig = getSymbolConfig(symbol);
console.log('交易对配置:', symbolConfig);
// 检查数据是否存在
if (!currentData || !currentData.kline_data) {
console.error('数据加载失败或不存在');
return;
}
// 检查是否使用小周期K线数据
const useElementPeriod = $('#elementPeriodKline').is(':checked') &&
currentData.element_kline_data &&
Array.isArray(currentData.element_kline_data);
// 输出K线周期选择状态
console.log('K线周期选择:', useElementPeriod ? '小周期' : '主周期');
console.log('当前选择时区:', $('#timezone').val());
console.log('交易对类型:', symbolConfig.type);
let candles = [];
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线数据 - 检查数据是否存在
if (!currentData.kline_data || !Array.isArray(currentData.kline_data)) {
console.error('主周期K线数据不存在或不是数组:', currentData.kline_data);
return;
}
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 (symbolConfig.type === 'a_stock' && timeframe.includes('m')) {
// 对于A股分钟级数据,过滤非交易时间
const originalLength = candles.length;
candles = filterTradingHours(candles, symbolConfig);
console.log(`A股数据过滤: ${originalLength} -> ${candles.length} 条记录`);
}
// 清除图表容器
document.getElementById('tradingview_chart').innerHTML = '';
// 重置图表对象(保留均线配置,但清空序列引用)
// 清空所有均线的序列引用(图表重新初始化后序列无效)
movingAverages.forEach(ma => {
ma.series = null;
});
console.log('已清空均线序列引用,配置保留:', movingAverages.length);
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: [],
mainBollingerSeries: [],
elementBollingerSeries: [],
movingAverageSeries: []
},
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 = true;
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和ATR容器
let macdChartContainer = null;
let atrChartContainer = null;
if (showMacd) {
// 设置各图表高度 - 为四个图表分配合理比例
mainChartContainer.style.height = '45%'; // 主图占45%(约315px
volumeChartContainer.style.top = '45%';
volumeChartContainer.style.height = '18.5%'; // 成交量图占18.5%(约129.5px
// 创建MACD容器
macdChartContainer = document.createElement('div');
macdChartContainer.style.width = '100%';
macdChartContainer.style.height = '18.5%'; // MACD图占18.5%(约129.5px
macdChartContainer.style.position = 'absolute';
macdChartContainer.style.top = '63.5%'; // 从63.5%位置开始
macdChartContainer.style.left = '0';
macdChartContainer.style.right = '0';
macdChartContainer.style.borderTop = '1px solid #e0e0e0';
// 创建ATR容器
atrChartContainer = document.createElement('div');
atrChartContainer.style.width = '100%';
atrChartContainer.style.height = '18%'; // ATR图占18%(约126px
atrChartContainer.style.position = 'absolute';
atrChartContainer.style.top = '82%'; // 从82%位置开始
atrChartContainer.style.left = '0';
atrChartContainer.style.right = '0';
atrChartContainer.style.borderTop = '1px solid #e0e0e0';
} else {
// 不显示MACD时的高度 - 主图和成交量图分配
mainChartContainer.style.height = '72%'; // 主图占72%(约504px
volumeChartContainer.style.top = '72%';
volumeChartContainer.style.height = '28%'; // 成交量图占28%(约196px
}
container.appendChild(mainChartContainer);
container.appendChild(volumeChartContainer);
if (showMacd) {
container.appendChild(macdChartContainer);
container.appendChild(atrChartContainer);
}
// 防止同步过程中的无限循环
let syncInProgress = false;
// 创建统一的图表选项
const createChartOptions = (showTimeScale = true, chartType = 'main') => {
// 根据图表类型确定高度
let chartHeight;
if (chartType === 'main') {
chartHeight = mainChartContainer.clientHeight;
} else if (chartType === 'volume') {
chartHeight = volumeChartContainer.clientHeight;
} else if (chartType === 'macd') {
chartHeight = macdChartContainer ? macdChartContainer.clientHeight : 0;
} else if (chartType === 'atr') {
chartHeight = atrChartContainer ? atrChartContainer.clientHeight : 0;
} else {
chartHeight = mainChartContainer.clientHeight;
}
const baseOptions = {
width: mainChartContainer.clientWidth,
height: chartHeight,
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 {
const date = new Date(time * 1000);
if (symbolConfig.type === 'a_stock') {
// A股使用中国时区格式
return date.toLocaleString('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
} else {
return date.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',
scaleMargins: {
top: 0.1,
bottom: 0.1,
},
// 为标签留出更多空间,防止遮挡
minimumWidth: 80,
},
// 添加左边距配置
leftPriceScale: {
visible: false,
},
// 添加本地化选项,确保所有时间显示都使用选定的时区
localization: {
timeFormatter: (time) => {
const selectedTimezone = $('#timezone').val();
try {
const date = new Date(time * 1000);
if (symbolConfig.type === 'a_stock') {
// A股使用中国时区格式
return date.toLocaleString('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
} else {
return date.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: symbolConfig.type === 'a_stock' ? 6 : 10,
// 移除可能影响拖动的固定边缘设置
// fixLeftEdge: true,
// fixRightEdge: true,
lockVisibleTimeRangeOnResize: true,
tickMarkFormatter: (time) => {
const selectedTimezone = symbolConfig.type === 'a_stock' ? 'Asia/Shanghai' : $('#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();
}
},
},
};
// 根据交易对类型调整配置
return adjustChartForSymbolType(baseOptions, symbolConfig);
};
// 创建主图表
const mainChart = LightweightCharts.createChart(mainChartContainer, createChartOptions(true, 'main'));
// 创建成交量图表 - 只显示底部的时间轴
const volumeChart = LightweightCharts.createChart(volumeChartContainer, createChartOptions(false, 'volume'));
// 创建MACD图表(如果需要)
let macdChart = null;
let atrChart = null;
if (showMacd) {
macdChart = LightweightCharts.createChart(macdChartContainer, createChartOptions(false, 'macd'));
atrChart = LightweightCharts.createChart(atrChartContainer, createChartOptions(false, 'atr'));
// 强制所有图表使用相同的时间刻度配置,确保时间对齐
const timeScaleOptions = {
borderVisible: false,
timeVisible: true,
secondsVisible: false,
tickMarkMaxCharacterLength: 8,
};
// 应用统一的时间刻度到所有图表
mainChart.timeScale().applyOptions(timeScaleOptions);
volumeChart.timeScale().applyOptions(timeScaleOptions);
macdChart.timeScale().applyOptions(timeScaleOptions);
atrChart.timeScale().applyOptions(timeScaleOptions);
mainChart.timeScale().applyOptions(timeScaleOptions);
volumeChart.timeScale().applyOptions(timeScaleOptions);
macdChart.timeScale().applyOptions(timeScaleOptions);
atrChart.timeScale().applyOptions(timeScaleOptions);
}
// 创建蜡烛图系列并设置数据
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;
}
// 转换成交量数据 - 根据周期选择使用对应数据源
let volumes = [];
// 使用已定义的useElementPeriod变量
const volumeDataSource = useElementPeriod ? currentData.element_kline_data : currentData.kline_data;
const dataSourceType = useElementPeriod ? '小周期' : '主周期';
console.log('成交量数据源:', dataSourceType);
console.log('成交量数据长度:', volumeDataSource.length);
if (volumeDataSource && Array.isArray(volumeDataSource)) {
volumes = volumeDataSource.map(kline => {
// 使用与K线和MACD完全相同的时间戳计算方式
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)',
};
});
console.log('处理后的成交量数据点数:', volumes.length);
}
// 添加成交量图表
const volumeSeries = volumeChart.addHistogramSeries({
color: '#26a69a',
priceFormat: {
type: 'volume',
},
title: '成交量',
});
volumeSeries.setData(volumes);
tvWidget.series.volumeSeries = volumeSeries;
// 添加MACD图表 - 根据周期选择使用对应数据源
const macdDataExists = useElementPeriod ?
(currentData.element_macd && currentData.element_kline_data && Array.isArray(currentData.element_kline_data)) :
(currentData.macd && currentData.kline_data && Array.isArray(currentData.kline_data));
if (showMacd && macdDataExists) {
// 创建MACD线
const macdLineSeries = macdChart.addLineSeries({
color: '#2962FF',
lineWidth: 1,
title: 'MACD',
lastValueVisible: false, // 禁用最后值标签,防止遮挡
priceLineVisible: false, // 禁用价格线
});
// 创建信号线
const signalLineSeries = macdChart.addLineSeries({
color: '#FF6B6B',
lineWidth: 1,
title: 'Signal',
lastValueVisible: false, // 禁用最后值标签,防止遮挡
priceLineVisible: false, // 禁用价格线
});
// 创建直方图
const histogramSeries = macdChart.addHistogramSeries({
color: '#26a69a',
title: 'Histogram',
priceFormat: {
type: 'price',
precision: 4,
},
});
// 提取MACD数据 - 使用和K线数据相同的时间处理逻辑
const macdData = [];
const signalData = [];
const histogramData = [];
// 根据周期选择使用对应数据源
const klineDataSource = useElementPeriod ? currentData.element_kline_data : currentData.kline_data;
const macdDataSource = useElementPeriod ? currentData.element_macd : currentData.macd;
const macdSourceType = useElementPeriod ? '小周期' : '主周期';
console.log('MACD数据源:', macdSourceType);
console.log('K线数据长度:', klineDataSource.length);
console.log('MACD数据:', macdDataSource);
for (let i = 0; i < klineDataSource.length; i++) {
const kline = klineDataSource[i];
// 使用与K线完全相同的时间戳计算方式
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
if (macdDataSource && macdDataSource.macd && macdDataSource.macd[i] !== undefined) {
macdData.push({
time: timestamp,
value: macdDataSource.macd[i]
});
signalData.push({
time: timestamp,
value: macdDataSource.signal[i]
});
// 设置直方图颜色
const histValue = macdDataSource.histogram[i];
histogramData.push({
time: timestamp,
value: histValue,
color: histValue >= 0 ? 'rgba(220, 53, 69, 0.5)' : 'rgba(40, 167, 69, 0.5)'
});
}
}
console.log('处理后的MACD数据点数:', macdData.length);
macdLineSeries.setData(macdData);
signalLineSeries.setData(signalData);
histogramSeries.setData(histogramData);
tvWidget.series.macdLineSeries = macdLineSeries;
tvWidget.series.signalLineSeries = signalLineSeries;
tvWidget.series.histogramSeries = histogramSeries;
}
// 添加ATR图表 - 根据周期选择使用对应数据源
const atrDataExists = useElementPeriod ?
(currentData.element_kline_data && Array.isArray(currentData.element_kline_data)) :
(currentData.kline_data && Array.isArray(currentData.kline_data));
if (showMacd && atrDataExists) {
// 创建ATR线
const atrLineSeries = atrChart.addLineSeries({
color: '#FF9800',
lineWidth: 2,
title: 'ATR',
lastValueVisible: false,
priceLineVisible: false,
baseLineVisible: false,
crosshairMarkerVisible: false,
pointMarkersVisible: false,
});
// 提取ATR数据
const atrData = [];
// 根据周期选择使用对应数据源
const klineDataSource = useElementPeriod ? currentData.element_kline_data : currentData.kline_data;
const atrSourceType = useElementPeriod ? '小周期' : '主周期';
console.log('ATR数据源:', atrSourceType);
console.log('ATR K线数据长度:', klineDataSource.length);
// 创建一个隐藏的基础数据系列来保持时间轴对齐
const baseLineSeries = atrChart.addLineSeries({
color: 'transparent',
lineWidth: 0,
lastValueVisible: false,
priceLineVisible: false,
baseLineVisible: false,
crosshairMarkerVisible: false,
pointMarkersVisible: false,
visible: false
});
// 添加完整的时间数据(透明)来保持时间轴连续
const baseData = [];
for (let i = 0; i < klineDataSource.length; i++) {
const kline = klineDataSource[i];
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
baseData.push({
time: timestamp,
value: 0.1 // 使用很小的值,不会显示
});
// 只添加ATR>0的有效数据到可见系列
if (kline.atr && kline.atr > 0) {
atrData.push({
time: timestamp,
value: kline.atr
});
}
}
// 先设置基础数据保持时间轴
baseLineSeries.setData(baseData);
// 再设置ATR可见数据
atrLineSeries.setData(atrData);
console.log('ATR基础数据点数:', baseData.length);
console.log('ATR可见数据点数:', atrData.length);
tvWidget.series.atrLineSeries = atrLineSeries;
}
// 实现三图联动滚动
// 同步图表的时间范围
function syncCharts(sourceChart, sourceContainer) {
// 防止无限循环 - 使用更精确的检查
if (syncInProgress) {
console.log('🔄 同步正在进行中,跳过此次同步');
return;
}
syncInProgress = true;
console.log('🚀 开始同步图表,来源:',
sourceChart === mainChart ? '主图' :
sourceChart === volumeChart ? '成交量图' :
sourceChart === macdChart ? 'MACD图' : 'ATR图');
try {
if (sourceChart && sourceChart.timeScale) {
const logicalRange = sourceChart.timeScale().getVisibleLogicalRange();
if (logicalRange && logicalRange.from !== undefined && logicalRange.to !== undefined) {
console.log('📊 同步时间范围:', logicalRange);
// 同步主图
if (sourceChart !== mainChart && mainChart && mainChart.timeScale) {
try {
mainChart.timeScale().setVisibleLogicalRange(logicalRange);
console.log('✅ 主图同步完成');
} catch (e) {
console.error('❌ 主图同步失败:', e);
}
}
// 同步成交量图
if (sourceChart !== volumeChart && volumeChart && volumeChart.timeScale) {
try {
volumeChart.timeScale().setVisibleLogicalRange(logicalRange);
console.log('✅ 成交量图同步完成');
} catch (e) {
console.error('❌ 成交量图同步失败:', e);
}
}
// 同步MACD图
if (showMacd && macdChart && sourceChart !== macdChart && macdChart.timeScale) {
try {
macdChart.timeScale().setVisibleLogicalRange(logicalRange);
console.log('✅ MACD图同步完成');
} catch (e) {
console.error('❌ MACD图同步失败:', e);
}
}
// 同步ATR图
if (showMacd && atrChart && sourceChart !== atrChart && atrChart.timeScale) {
try {
atrChart.timeScale().setVisibleLogicalRange(logicalRange);
console.log('✅ ATR图同步完成');
} catch (e) {
console.error('❌ ATR图同步失败:', e);
}
}
// 保存当前的可见范围到全局状态
if (tvWidget && tvWidget.state) {
tvWidget.state.logicalRange = logicalRange;
}
} else {
console.warn('⚠️ 无效的逻辑范围:', logicalRange);
}
} else {
console.warn('⚠️ 无效的源图表或时间刻度');
}
} catch (e) {
console.error('💥 同步图表出错:', e);
}
// 立即重置同步标志,提高响应速度
setTimeout(() => {
syncInProgress = false;
console.log('🔓 同步标志已重置');
}, 1);
}
// 用于跟踪所有图表的拖动状态
let localDragStates = {
main: false,
volume: false,
macd: false,
atr: false
};
// 全局鼠标抬起事件(只添加一次)
document.addEventListener('mouseup', () => {
// 重置所有拖动状态
Object.keys(localDragStates).forEach(key => {
if (localDragStates[key]) {
console.log(`全局鼠标抬起,重置${key}图表拖动状态`);
localDragStates[key] = false;
}
});
});
// 为每个图表添加事件监听
const addChartSyncEvents = (chartContainer, chart) => {
console.log('为图表添加同步事件监听:',
chart === mainChart ? '主图' :
chart === volumeChart ? '成交量图' :
chart === macdChart ? 'MACD图' : 'ATR图');
// 确定当前图表类型
const chartType = chart === mainChart ? 'main' :
chart === volumeChart ? 'volume' :
chart === macdChart ? 'macd' : 'atr';
// 使用LightweightCharts内置的时间范围变化事件(这是最可靠的方法)
chart.timeScale().subscribeVisibleTimeRangeChange(() => {
// 使用图表特定的同步标志防止递归
if (!syncInProgress) {
console.log('✅ 检测到时间范围变化,触发同步:', chartType, '当前范围:', chart.timeScale().getVisibleLogicalRange());
syncCharts(chart, chartContainer);
} else {
console.log('⏸️ 同步进行中,跳过时间范围变化事件:', chartType);
}
});
// 备用的DOM事件监听(用于调试和额外保障)
let isScrolling = false;
// 鼠标按下事件
chartContainer.addEventListener('mousedown', (e) => {
localDragStates[chartType] = true;
console.log('鼠标按下开始拖动:', chartType);
});
// 鼠标抬起事件
chartContainer.addEventListener('mouseup', (e) => {
if (localDragStates[chartType]) {
localDragStates[chartType] = false;
console.log('鼠标抬起,结束拖动:', chartType);
}
});
// 鼠标离开事件
chartContainer.addEventListener('mouseleave', (e) => {
if (localDragStates[chartType]) {
localDragStates[chartType] = false;
console.log('鼠标离开容器,结束拖动:', chartType);
}
});
// 滚轮缩放事件(保持原有逻辑)
chartContainer.addEventListener('wheel', (e) => {
if (!isScrolling) {
isScrolling = true;
console.log('滚轮缩放:', chartType);
setTimeout(() => {
if (!syncInProgress) {
syncCharts(chart, chartContainer);
}
isScrolling = false;
}, 50);
}
});
};
// 添加事件监听
addChartSyncEvents(mainChartContainer, mainChart);
addChartSyncEvents(volumeChartContainer, volumeChart);
if (showMacd && macdChart) {
addChartSyncEvents(macdChartContainer, macdChart);
}
if (showMacd && atrChart) {
addChartSyncEvents(atrChartContainer, atrChart);
}
// 窗口大小变化时重绘图表
window.addEventListener('resize', () => {
// 调整主图大小
mainChart.applyOptions({
width: mainChartContainer.clientWidth,
height: mainChartContainer.clientHeight
});
// 调整成交量图大小
volumeChart.applyOptions({
width: volumeChartContainer.clientWidth,
height: volumeChartContainer.clientHeight
});
// 调整MACD图大小
if (showMacd && macdChart && macdChartContainer) {
macdChart.applyOptions({
width: macdChartContainer.clientWidth,
height: macdChartContainer.clientHeight
});
}
// 调整ATR图大小
if (showMacd && atrChart && atrChartContainer) {
atrChart.applyOptions({
width: atrChartContainer.clientWidth,
height: atrChartContainer.clientHeight
});
}
// 重新同步 - 使用主图作为同步源
setTimeout(() => {
if (mainChart) {
syncCharts(mainChart, mainChartContainer);
}
}, 200);
});
// 显示笔的绘制 - 分别处理主周期和次周期
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,
color: 'transparent', // 设置为透明色
lineWidth: 0, // 线宽为0
});
// 添加一个透明的数据点用于承载标记
macdDivLabel.setData([
{ time: endTime, value: endPrice }
]);
// 主周期MACD背离标记根据笔方向显示,远离K线避免与分型重叠
const markerPosition = bi.direction === 1 ? 'aboveBar' : 'belowBar';
const textColor = bi.macd_div > 0 ? '#dc3545' : '#28a745';
// 只使用标记,不添加数据点
macdDivLabel.setMarkers([
{
time: endTime,
position: markerPosition,
color: textColor,
text: `${bi.macd_div.toFixed(2)}`, // 添加M前缀区分
size: 0.6, // 更小的尺寸,远离分型标记
}
]);
}
} 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,
color: 'transparent', // 设置为透明色
lineWidth: 0, // 线宽为0
});
// 添加一个透明的数据点用于承载标记
macdDivLabel.setData([
{ time: endTime, value: endPrice }
]);
// 次周期MACD背离标记使用不同位置,进一步避免重叠
const markerPosition = bi.direction === 1 ? 'aboveBar' : 'belowBar';
const textColor = bi.macd_div > 0 ? '#9c27b0' : '#673ab7';
// 只使用标记,不添加数据点
macdDivLabel.setMarkers([
{
time: endTime,
position: markerPosition,
color: textColor,
text: `${bi.macd_div.toFixed(2)}`, // 添加E前缀区分次周期
size: 0.4, // 更小的尺寸,让分型标记有更多空间
}
]);
}
} 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 ($('#showMainBollinger').is(':checked') || $('#showElementBollinger').is(':checked')) {
console.log('绘制布林带 - 已启用');
// 主周期布林带
if ($('#showMainBollinger').is(':checked') && currentData.bollinger && currentData.bollinger.upper && currentData.bollinger.lower && currentData.bollinger.middle) {
console.log(`绘制主周期布林带数据,共${currentData.bollinger.upper.length}条`);
// 准备布林带数据
const upperBandData = [];
const lowerBandData = [];
const middleBandData = [];
// 主周期布林带始终使用主周期K线数据作为时间源
const mainKlineData = currentData.kline_data;
for (let i = 0; i < mainKlineData.length && i < currentData.bollinger.upper.length; i++) {
const kline = mainKlineData[i];
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
// 只添加非0的有效数据点
if (currentData.bollinger.upper[i] && currentData.bollinger.upper[i] !== 0) {
upperBandData.push({
time: timestamp,
value: currentData.bollinger.upper[i]
});
}
if (currentData.bollinger.lower[i] && currentData.bollinger.lower[i] !== 0) {
lowerBandData.push({
time: timestamp,
value: currentData.bollinger.lower[i]
});
}
if (currentData.bollinger.middle[i] && currentData.bollinger.middle[i] !== 0) {
middleBandData.push({
time: timestamp,
value: currentData.bollinger.middle[i]
});
}
}
// 创建布林带上轨
const upperBandSeries = mainChart.addLineSeries({
color: '#2196F3',
lineWidth: 1,
lineStyle: 2, // 虚线
lastValueVisible: false,
priceLineVisible: false,
title: '布林上轨'
});
upperBandSeries.setData(upperBandData);
// 创建布林带下轨
const lowerBandSeries = mainChart.addLineSeries({
color: '#2196F3',
lineWidth: 1,
lineStyle: 2, // 虚线
lastValueVisible: false,
priceLineVisible: false,
title: '布林下轨'
});
lowerBandSeries.setData(lowerBandData);
// 创建布林带中轨(移动平均线)
const middleBandSeries = mainChart.addLineSeries({
color: '#FF9800',
lineWidth: 1,
lastValueVisible: false,
priceLineVisible: false,
title: '布林中轨'
});
middleBandSeries.setData(middleBandData);
// 保存到tvWidget.series对象
tvWidget.series.mainBollingerSeries.push(upperBandSeries);
tvWidget.series.mainBollingerSeries.push(lowerBandSeries);
tvWidget.series.mainBollingerSeries.push(middleBandSeries);
console.log('主周期布林带绘制完成');
}
// 次周期布林带
if ($('#showElementBollinger').is(':checked') && currentData.element_bollinger && currentData.element_bollinger.upper && currentData.element_bollinger.lower && currentData.element_bollinger.middle) {
console.log(`绘制次周期布林带数据,共${currentData.element_bollinger.upper.length}条`);
// 准备次周期布林带数据
const elementUpperBandData = [];
const elementLowerBandData = [];
const elementMiddleBandData = [];
// 使用次周期K线数据
const elementKlineData = currentData.element_kline_data || currentData.kline_data;
for (let i = 0; i < elementKlineData.length && i < currentData.element_bollinger.upper.length; i++) {
const kline = elementKlineData[i];
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
// 只添加非0的有效数据点
if (currentData.element_bollinger.upper[i] && currentData.element_bollinger.upper[i] !== 0) {
elementUpperBandData.push({
time: timestamp,
value: currentData.element_bollinger.upper[i]
});
}
if (currentData.element_bollinger.lower[i] && currentData.element_bollinger.lower[i] !== 0) {
elementLowerBandData.push({
time: timestamp,
value: currentData.element_bollinger.lower[i]
});
}
if (currentData.element_bollinger.middle[i] && currentData.element_bollinger.middle[i] !== 0) {
elementMiddleBandData.push({
time: timestamp,
value: currentData.element_bollinger.middle[i]
});
}
}
// 创建次周期布林带上轨
const elementUpperBandSeries = mainChart.addLineSeries({
color: '#9C27B0',
lineWidth: 1,
lineStyle: 2, // 虚线
lastValueVisible: false,
priceLineVisible: false,
title: '次周期布林上轨'
});
elementUpperBandSeries.setData(elementUpperBandData);
// 创建次周期布林带下轨
const elementLowerBandSeries = mainChart.addLineSeries({
color: '#9C27B0',
lineWidth: 1,
lineStyle: 2, // 虚线
lastValueVisible: false,
priceLineVisible: false,
title: '次周期布林下轨'
});
elementLowerBandSeries.setData(elementLowerBandData);
// 创建次周期布林带中轨
const elementMiddleBandSeries = mainChart.addLineSeries({
color: '#E91E63',
lineWidth: 1,
lastValueVisible: false,
priceLineVisible: false,
title: '次周期布林中轨'
});
elementMiddleBandSeries.setData(elementMiddleBandData);
// 保存到tvWidget.series对象
tvWidget.series.elementBollingerSeries.push(elementUpperBandSeries);
tvWidget.series.elementBollingerSeries.push(elementLowerBandSeries);
tvWidget.series.elementBollingerSeries.push(elementMiddleBandSeries);
console.log('次周期布林带绘制完成');
}
} else {
console.log('绘制布林带 - 已禁用');
}
// 绘制买卖点标记
console.log('=== 开始检查买卖点显示条件 ===');
console.log('showTradePoints勾选状态:', $('#showTradePoints').is(':checked'));
console.log('currentData.trade_points存在:', !!currentData.trade_points);
console.log('currentData.trade_points长度:', currentData.trade_points ? currentData.trade_points.length : 'undefined');
if (currentData.trade_points && currentData.trade_points.length > 0) {
console.log('前3个买卖点数据样本:', currentData.trade_points.slice(0, 3));
}
// 收集所有主周期买卖点标记
const allMainTradePointMarkers = [];
const mainTradePointMarkers = []; // 用于tooltip支持
// 处理主周期买卖点
if ($('#showTradePoints').is(':checked') && currentData.trade_points && currentData.trade_points.length > 0) {
console.log(`绘制主周期买卖点标记,共${currentData.trade_points.length}个`);
currentData.trade_points.forEach(function(tradePoint, index) {
try {
// 直接使用UTC时间戳(秒)
const timestamp = Math.floor(new Date(tradePoint.time).getTime() / 1000);
const price = parseFloat(tradePoint.price);
// 添加调试信息(只打印前3个)
if (index < 3) {
console.log(`主周期买卖点${index+1}: 原始时间=${tradePoint.time}, 转换时间戳=${timestamp}, 价格=${price}, 类型=${tradePoint.type}`);
}
if (isNaN(timestamp) || isNaN(price)) {
console.error('主周期买卖点时间或价格转换错误:', tradePoint.time, tradePoint.price);
return;
}
// 根据买卖点类型确定样式
let style = getTradePointStyle(tradePoint);
// 添加标记配置
const markerConfig = {
time: timestamp,
position: tradePoint.type > 0 ? 'belowBar' : 'aboveBar',
color: style.color,
//shape: style.shape,
text: style.text,
size: style.size
};
allMainTradePointMarkers.push(markerConfig);
// 创建买卖点标记对象,包含tooltip信息
const tradePointMarker = {
time: timestamp,
tooltip: `<div style="color: ${style.color}; font-weight: bold;">
${tradePoint.type > 0 ? '买点' : '卖点'}: ${tradePoint.desc}<br>
价格: ${price.toFixed(4)}<br>
时间: ${tradePoint.time}
</div>`
};
mainTradePointMarkers.push(tradePointMarker);
} catch (e) {
console.error('绘制主周期买卖点标记出错:', e);
}
});
}
// 暂存主周期买卖点标记
window.mainTradePointMarkers = allMainTradePointMarkers;
// 重置全局markers,准备重新构建
window.tradePointMarkers = mainTradePointMarkers;
// 检查是否有任何主周期买卖点数据
const hasMainTradePointData = $('#showTradePoints').is(':checked') && currentData.trade_points && currentData.trade_points.length > 0;
if (!hasMainTradePointData) {
console.log('绘制主周期买卖点标记 - 已禁用或无数据');
// 清空主周期买卖点标记
window.mainTradePointMarkers = [];
window.tradePointMarkers = [];
}
// 绘制小周期买卖点标记
if ($('#showElementTradePoints').is(':checked') && currentData.element_trade_points && currentData.element_trade_points.length > 0) {
// 收集所有小周期买卖点标记
const allElementTradePointMarkers = [];
const elementTradePointMarkers = []; // 用于tooltip支持
console.log(`绘制小周期买卖点标记,共${currentData.element_trade_points.length}个`);
currentData.element_trade_points.forEach(function(tradePoint) {
try {
// 直接使用UTC时间戳(秒)
const timestamp = Math.floor(new Date(tradePoint.time).getTime() / 1000);
const price = parseFloat(tradePoint.price);
if (isNaN(timestamp) || isNaN(price)) {
console.error('小周期买卖点时间或价格转换错误:', tradePoint.time, tradePoint.price);
return;
}
// 根据买卖点类型确定样式(小周期使用稍小的尺寸)
let style = getTradePointStyle(tradePoint);
style = {...style, size: style.size * 0.75}; // 小周期标记稍小一些
// 小周期买卖点标记配置
const markerConfig = {
time: timestamp,
position: tradePoint.type > 0 ? 'belowBar' : 'aboveBar',
color: style.color,
//shape: style.shape,
text: style.text,
size: 1
};
allElementTradePointMarkers.push(markerConfig);
// 创建小周期买卖点标记对象,包含tooltip信息
const elementTradePointMarker = {
time: timestamp,
tooltip: `<div style="color: ${style.color}; font-weight: bold;">
小周期${tradePoint.type > 0 ? '买点' : '卖点'}: ${tradePoint.desc}<br>
价格: ${price.toFixed(4)}<br>
时间: ${tradePoint.time}
</div>`
};
elementTradePointMarkers.push(elementTradePointMarker);
} catch (e) {
console.error('绘制小周期买卖点标记出错:', e);
}
});
// 将小周期买卖点标记添加到全局markers中以支持tooltip功能
//window.tradePointMarkers = [...(window.tradePointMarkers || []), ...elementTradePointMarkers];
// 合并主周期和小周期买卖点标记,统一设置到K线数据系列
const combinedMarkers = [...(window.mainTradePointMarkers || []), ...allElementTradePointMarkers];
if (combinedMarkers.length > 0) {
console.log('合并设置', combinedMarkers.length, '个买卖点标记(主周期:', (window.mainTradePointMarkers || []).length, '个,小周期:', allElementTradePointMarkers.length, '个)');
// 尝试在K线系列上设置合并后的标记
if (showOriginalKline && tvWidget.series.candleSeries) {
tvWidget.series.candleSeries.setMarkers(combinedMarkers);
console.log('合并标记已设置到蜡烛图系列');
} else if (!showOriginalKline && tvWidget.series.lineSeries) {
tvWidget.series.lineSeries.setMarkers(combinedMarkers);
console.log('合并标记已设置到线图系列');
} else {
console.log('未找到主数据系列,无法设置标记');
}
}
} else {
console.log('绘制小周期买卖点标记 - 已禁用或无数据');
// 只设置主周期买卖点标记
if (window.mainTradePointMarkers && window.mainTradePointMarkers.length > 0) {
console.log('仅设置', window.mainTradePointMarkers.length, '个主周期买卖点标记');
// 尝试在K线系列上设置标记
if (showOriginalKline && tvWidget.series.candleSeries) {
tvWidget.series.candleSeries.setMarkers(window.mainTradePointMarkers);
console.log('主周期标记已设置到蜡烛图系列');
} else if (!showOriginalKline && tvWidget.series.lineSeries) {
tvWidget.series.lineSeries.setMarkers(window.mainTradePointMarkers);
console.log('主周期标记已设置到线图系列');
} else {
console.log('未找到主数据系列,无法设置标记');
}
} else {
console.log('没有买卖点标记需要显示,清空图表标记');
// 清空图表上的所有标记
if (showOriginalKline && tvWidget.series.candleSeries) {
tvWidget.series.candleSeries.setMarkers([]);
} else if (!showOriginalKline && tvWidget.series.lineSeries) {
tvWidget.series.lineSeries.setMarkers([]);
}
}
}
// 调整所有图表以适应数据
mainChart.timeScale().fitContent();
volumeChart.timeScale().fitContent();
if (showMacd && macdChart) {
macdChart.timeScale().fitContent();
}
if (showMacd && atrChart) {
atrChart.timeScale().fitContent();
}
// 保存图表对象
tvWidget.mainChart = mainChart;
tvWidget.volumeChart = volumeChart;
tvWidget.macdChart = macdChart;
tvWidget.atrChart = atrChart;
tvWidget.state.isInitialized = true;
// 绑定同步事件
bindSyncEvents(mainChartContainer, volumeChartContainer, macdChartContainer, atrChartContainer, mainChart, volumeChart, macdChart, atrChart, showMacd);
// 强制同步所有图表时间范围(确保时间对齐)
setTimeout(() => {
if (mainChart && mainChart.timeScale) {
// 先让所有图表自适应内容
mainChart.timeScale().fitContent();
if (volumeChart) volumeChart.timeScale().fitContent();
if (showMacd && macdChart) macdChart.timeScale().fitContent();
if (showMacd && atrChart) atrChart.timeScale().fitContent();
// 等待适应完成后强制同步时间范围
setTimeout(() => {
const mainRange = mainChart.timeScale().getVisibleLogicalRange();
console.log('主图时间范围:', mainRange);
if (mainRange && volumeChart && volumeChart.timeScale) {
volumeChart.timeScale().setVisibleLogicalRange(mainRange);
console.log('成交量图时间范围已同步');
}
if (mainRange && showMacd && macdChart && macdChart.timeScale) {
macdChart.timeScale().setVisibleLogicalRange(mainRange);
console.log('MACD图时间范围已同步');
}
if (mainRange && showMacd && atrChart && atrChart.timeScale) {
atrChart.timeScale().setVisibleLogicalRange(mainRange);
console.log('ATR图时间范围已同步');
}
console.log('所有图表时间范围已强制同步完成');
}, 50);
}
}, 200);
// 只有在时间输入框都为空时才设置图表默认时间范围
if (!$('#start_time').val() && !$('#end_time').val()) {
setDefaultTimeRange();
}
// 添加买卖点提示
setupTooltip(mainChart, [], [], mainChartContainer, volumeChartContainer, macdChartContainer, atrChartContainer, volumeChart, macdChart, atrChart, showMacd);
// 添加默认EMA线
addDefaultEMALines();
console.log('图表初始化完成');
} catch (e) {
console.error('图表初始化错误:', e);
}
}
// 增量更新图表数据
function updateTradingViewData() {
try {
console.log('增量更新图表数据');
// 检查 currentData 是否存在
if (!currentData) {
console.error('currentData为空,无法更新图表');
return;
}
// 保存当前的可视范围
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 &&
Array.isArray(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 if (currentData.kline_data && Array.isArray(currentData.kline_data)) {
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);
}
// 更新成交量数据 - 根据周期选择使用对应数据源
let volumes = [];
const volumeDataSource = useElementPeriod ? currentData.element_kline_data : currentData.kline_data;
if (volumeDataSource && Array.isArray(volumeDataSource)) {
volumes = volumeDataSource.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数据 - 根据周期选择使用对应数据源
const macdDataSource = useElementPeriod ? currentData.element_macd : currentData.macd;
const macdKlineSource = useElementPeriod ? currentData.element_kline_data : currentData.kline_data;
if (true && macdDataSource && macdKlineSource && Array.isArray(macdKlineSource) && tvWidget.series.macdLineSeries) {
// 提取MACD数据
const macdData = [];
const signalData = [];
const histogramData = [];
for (let i = 0; i < macdKlineSource.length; i++) {
const kline = macdKlineSource[i];
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
if (macdDataSource && macdDataSource.macd && macdDataSource.macd[i] !== undefined) {
macdData.push({
time: timestamp,
value: macdDataSource.macd[i]
});
signalData.push({
time: timestamp,
value: macdDataSource.signal[i]
});
// 设置直方图颜色
const histValue = macdDataSource.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);
}
// 更新ATR数据 - 根据周期选择使用对应数据源
const atrKlineSource = useElementPeriod ? currentData.element_kline_data : currentData.kline_data;
if (atrKlineSource && Array.isArray(atrKlineSource) && tvWidget.series.atrLineSeries) {
const atrData = [];
for (let i = 0; i < atrKlineSource.length; i++) {
const kline = atrKlineSource[i];
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
// 只添加ATR>0的有效数据,不显示ATR=0的点
if (kline.atr && kline.atr > 0) {
atrData.push({
time: timestamp,
value: kline.atr
});
}
}
tvWidget.series.atrLineSeries.setData(atrData);
}
// 先恢复可视范围,避免先显示默认范围再跳转的跳跃效果
if (tvWidget.mainChart && (tvWidget.state.logicalRange || tvWidget.state.visibleRange)) {
console.log('检测到保存的视图状态,准备恢复所有图表范围');
// 在回放模式下,图表已锁定,不设置范围
if (isReplaying) {
// 回放模式下跳过范围设置,因为图表已锁定
console.log('回放模式:跳过范围设置,图表已锁定');
} else {
// 非回放模式下延迟应用,确保所有图表都能正确恢复
setTimeout(() => {
try {
if (tvWidget.state.logicalRange) {
console.log('开始恢复所有图表的logicalRange:', tvWidget.state.logicalRange);
// 恢复主图表
tvWidget.mainChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange);
console.log('✅ 主图表范围已恢复');
// 恢复成交量图表
if (tvWidget.volumeChart && tvWidget.volumeChart.timeScale) {
tvWidget.volumeChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange);
console.log('✅ 成交量图表范围已恢复');
}
// 恢复MACD图表
if (tvWidget.macdChart && tvWidget.macdChart.timeScale) {
tvWidget.macdChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange);
console.log('✅ MACD图表范围已恢复');
}
// 恢复ATR图表
if (tvWidget.atrChart && tvWidget.atrChart.timeScale) {
tvWidget.atrChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange);
console.log('✅ ATR图表范围已恢复');
}
} else if (tvWidget.state.visibleRange) {
console.log('开始恢复所有图表的visibleRange:', tvWidget.state.visibleRange);
// 恢复主图表
tvWidget.mainChart.timeScale().setVisibleRange(tvWidget.state.visibleRange);
console.log('✅ 主图表范围已恢复');
// 恢复成交量图表
if (tvWidget.volumeChart && tvWidget.volumeChart.timeScale) {
tvWidget.volumeChart.timeScale().setVisibleRange(tvWidget.state.visibleRange);
console.log('✅ 成交量图表范围已恢复');
}
// 恢复MACD图表
if (tvWidget.macdChart && tvWidget.macdChart.timeScale) {
tvWidget.macdChart.timeScale().setVisibleRange(tvWidget.state.visibleRange);
console.log('✅ MACD图表范围已恢复');
}
// 恢复ATR图表
if (tvWidget.atrChart && tvWidget.atrChart.timeScale) {
tvWidget.atrChart.timeScale().setVisibleRange(tvWidget.state.visibleRange);
console.log('✅ ATR图表范围已恢复');
}
}
console.log('🎉 所有图表视图范围恢复完成');
} catch (e) {
console.error('❌ 恢复图表视图范围时出错:', e);
}
}, 50); // 减少延迟到50ms以提高用户体验
}
}
// 重新显示笔、线段和中枢等图形
if (isReplaying) {
// 回放模式下,直接redraw,图表已锁定
redrawFractalElements();
} else {
redrawFractalElements();
}
console.log('增量更新图表完成');
} catch (e) {
console.error('增量更新图表错误,回退到完全重绘:', e);
// 出错时回退到完全重绘
initTradingView($('#symbol').val(), $('#timeframe').val());
}
}
function bindSyncEvents(mainChartContainer, volumeChartContainer, macdChartContainer, atrChartContainer, mainChart, volumeChart, macdChart, atrChart, showMacd) {
// 防止同步过程中的无限循环
let syncInProgress = false;
// 用于跟踪所有图表的拖动状态 - 在函数内部定义以确保作用域正确
let localDragStates = {
main: false,
volume: false,
macd: false,
atr: false
};
// 同步图表的时间范围
function syncCharts(sourceChart, sourceContainer) {
// 防止无限循环 - 使用更精确的检查
if (syncInProgress) {
console.log('🔄 同步正在进行中,跳过此次同步');
return;
}
syncInProgress = true;
console.log('🚀 开始同步图表,来源:',
sourceChart === mainChart ? '主图' :
sourceChart === volumeChart ? '成交量图' :
sourceChart === macdChart ? 'MACD图' : 'ATR图');
try {
if (sourceChart && sourceChart.timeScale) {
const logicalRange = sourceChart.timeScale().getVisibleLogicalRange();
if (logicalRange && logicalRange.from !== undefined && logicalRange.to !== undefined) {
console.log('📊 同步时间范围:', logicalRange);
// 同步主图
if (sourceChart !== mainChart && mainChart && mainChart.timeScale) {
try {
mainChart.timeScale().setVisibleLogicalRange(logicalRange);
console.log('✅ 主图同步完成');
} catch (e) {
console.error('❌ 主图同步失败:', e);
}
}
// 同步成交量图
if (sourceChart !== volumeChart && volumeChart && volumeChart.timeScale) {
try {
volumeChart.timeScale().setVisibleLogicalRange(logicalRange);
console.log('✅ 成交量图同步完成');
} catch (e) {
console.error('❌ 成交量图同步失败:', e);
}
}
// 同步MACD图
if (showMacd && macdChart && sourceChart !== macdChart && macdChart.timeScale) {
try {
macdChart.timeScale().setVisibleLogicalRange(logicalRange);
console.log('✅ MACD图同步完成');
} catch (e) {
console.error('❌ MACD图同步失败:', e);
}
}
// 同步ATR图
if (showMacd && atrChart && sourceChart !== atrChart && atrChart.timeScale) {
try {
atrChart.timeScale().setVisibleLogicalRange(logicalRange);
console.log('✅ ATR图同步完成');
} catch (e) {
console.error('❌ ATR图同步失败:', e);
}
}
// 保存当前的可见范围到全局状态,确保ATR图表状态也被保存
if (tvWidget && tvWidget.state) {
tvWidget.state.logicalRange = logicalRange;
// 同时尝试保存visibleRange作为备份
try {
if (sourceChart && sourceChart.timeScale) {
tvWidget.state.visibleRange = sourceChart.timeScale().getVisibleRange();
}
} catch (e) {
console.debug('保存visibleRange时出错:', e);
}
console.log('📝 图表状态已保存,包括ATR图表');
}
} else {
console.warn('⚠️ 无效的逻辑范围:', logicalRange);
}
} else {
console.warn('⚠️ 无效的源图表或时间刻度');
}
} catch (e) {
console.error('💥 同步图表出错:', e);
}
// 立即重置同步标志,提高响应速度
setTimeout(() => {
syncInProgress = false;
console.log('🔓 同步标志已重置');
}, 1);
}
// 为每个图表添加事件监听
const addChartSyncEvents = (chartContainer, chart) => {
console.log('为图表添加同步事件监听:',
chart === mainChart ? '主图' :
chart === volumeChart ? '成交量图' :
chart === macdChart ? 'MACD图' : 'ATR图');
// 确定当前图表类型
const chartType = chart === mainChart ? 'main' :
chart === volumeChart ? 'volume' :
chart === macdChart ? 'macd' : 'atr';
// 使用LightweightCharts内置的时间范围变化事件(这是最可靠的方法)
chart.timeScale().subscribeVisibleTimeRangeChange(() => {
// 使用图表特定的同步标志防止递归
if (!syncInProgress) {
console.log('✅ 检测到时间范围变化,触发同步:', chartType, '当前范围:', chart.timeScale().getVisibleLogicalRange());
syncCharts(chart, chartContainer);
} else {
console.log('⏸️ 同步进行中,跳过时间范围变化事件:', chartType);
}
});
// 备用的DOM事件监听(用于调试和额外保障)
let isScrolling = false;
// 鼠标按下事件
chartContainer.addEventListener('mousedown', (e) => {
localDragStates[chartType] = true;
console.log('鼠标按下开始拖动:', chartType);
});
// 鼠标抬起事件
chartContainer.addEventListener('mouseup', (e) => {
if (localDragStates[chartType]) {
localDragStates[chartType] = false;
console.log('鼠标抬起,结束拖动:', chartType);
}
});
// 鼠标离开事件
chartContainer.addEventListener('mouseleave', (e) => {
if (localDragStates[chartType]) {
localDragStates[chartType] = false;
console.log('鼠标离开容器,结束拖动:', chartType);
}
});
// 滚轮缩放事件(保持原有逻辑)
chartContainer.addEventListener('wheel', (e) => {
if (!isScrolling) {
isScrolling = true;
console.log('滚轮缩放:', chartType);
setTimeout(() => {
if (!syncInProgress) {
syncCharts(chart, chartContainer);
}
isScrolling = false;
}, 50);
}
});
};
// 添加事件监听
if (mainChartContainer && mainChart) {
addChartSyncEvents(mainChartContainer, mainChart);
}
if (volumeChartContainer && volumeChart) {
addChartSyncEvents(volumeChartContainer, volumeChart);
}
if (showMacd && macdChartContainer && macdChart) {
addChartSyncEvents(macdChartContainer, macdChart);
}
if (showMacd && atrChartContainer && atrChart) {
addChartSyncEvents(atrChartContainer, atrChart);
}
// 如果有均线配置,用新数据重新计算和显示均线
if (movingAverages.length > 0) {
console.log('图表重新初始化后,更新均线数据,均线数量:', movingAverages.length);
setTimeout(() => {
updateAllMovingAverages();
}, 200);
}
// 窗口大小变化时重绘图表
window.addEventListener('resize', () => {
// 调整主图大小
if (mainChart && mainChartContainer) {
mainChart.applyOptions({
width: mainChartContainer.clientWidth,
height: mainChartContainer.clientHeight
});
}
// 调整成交量图大小
if (volumeChart && volumeChartContainer) {
volumeChart.applyOptions({
width: volumeChartContainer.clientWidth,
height: volumeChartContainer.clientHeight
});
}
// 调整MACD图大小
if (showMacd && macdChart && macdChartContainer) {
macdChart.applyOptions({
width: macdChartContainer.clientWidth,
height: macdChartContainer.clientHeight
});
}
// 调整ATR图大小
if (showMacd && atrChart && atrChartContainer) {
atrChart.applyOptions({
width: atrChartContainer.clientWidth,
height: atrChartContainer.clientHeight
});
}
// 重新同步 - 使用主图作为同步源
setTimeout(() => {
if (mainChart) {
syncCharts(mainChart, mainChartContainer);
}
}, 200);
});
}
function setupTooltip(mainChart, buyMarkers = [], sellMarkers = [], mainChartContainer, volumeChartContainer, macdChartContainer, atrChartContainer, volumeChart, macdChart, atrChart, showMacd) {
// 调试变量
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);
// 添加鼠标悬停事件显示提示
if (mainChart) {
mainChart.subscribeCrosshairMove(param => {
// 十字线同步到其他图表 - 通过DOM元素绘制垂直线实现虚线延长效果
if (param.time && param.point && volumeChart) {
try {
// 清除之前的十字线标记
const existingVolumeLines = document.querySelectorAll('.volume-crosshair-line');
existingVolumeLines.forEach(line => line.remove());
const existingMacdLines = document.querySelectorAll('.macd-crosshair-line');
existingMacdLines.forEach(line => line.remove());
const existingAtrLines = document.querySelectorAll('.atr-crosshair-line');
existingAtrLines.forEach(line => line.remove());
// 获取时间对应的坐标位置
const mainTimeCoordinate = mainChart.timeScale().timeToCoordinate(param.time);
if (mainTimeCoordinate !== null) {
// 获取主图容器的位置
const mainChartRect = mainChartContainer.getBoundingClientRect();
// 在交易量图上绘制垂直线
const volumeTimeCoordinate = volumeChart.timeScale().timeToCoordinate(param.time);
if (volumeTimeCoordinate !== null) {
const volumeChartRect = volumeChartContainer.getBoundingClientRect();
const volumeLine = document.createElement('div');
volumeLine.className = 'volume-crosshair-line';
volumeLine.style.position = 'fixed'; // 改为fixed定位
volumeLine.style.left = (volumeChartRect.left + volumeTimeCoordinate) + 'px';
volumeLine.style.top = volumeChartRect.top + 'px';
volumeLine.style.width = '1px';
volumeLine.style.height = volumeChartRect.height + 'px';
volumeLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)';
volumeLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)';
volumeLine.style.pointerEvents = 'none';
volumeLine.style.zIndex = '1000';
document.body.appendChild(volumeLine);
}
// 如果有MACD图,也在MACD图上绘制垂直线
if (showMacd && macdChart && macdChartContainer) {
const macdTimeCoordinate = macdChart.timeScale().timeToCoordinate(param.time);
if (macdTimeCoordinate !== null) {
const macdChartRect = macdChartContainer.getBoundingClientRect();
const macdLine = document.createElement('div');
macdLine.className = 'macd-crosshair-line';
macdLine.style.position = 'fixed'; // 改为fixed定位
macdLine.style.left = (macdChartRect.left + macdTimeCoordinate) + 'px';
macdLine.style.top = macdChartRect.top + 'px';
macdLine.style.width = '1px';
macdLine.style.height = macdChartRect.height + 'px';
macdLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)';
macdLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)';
macdLine.style.pointerEvents = 'none';
macdLine.style.zIndex = '1000';
document.body.appendChild(macdLine);
}
}
// 如果有ATR图,也在ATR图上绘制垂直线
if (showMacd && atrChart && atrChartContainer) {
const atrTimeCoordinate = atrChart.timeScale().timeToCoordinate(param.time);
if (atrTimeCoordinate !== null) {
const atrChartRect = atrChartContainer.getBoundingClientRect();
const atrLine = document.createElement('div');
atrLine.className = 'atr-crosshair-line';
atrLine.style.position = 'fixed'; // 改为fixed定位
atrLine.style.left = (atrChartRect.left + atrTimeCoordinate) + 'px';
atrLine.style.top = atrChartRect.top + 'px';
atrLine.style.width = '1px';
atrLine.style.height = atrChartRect.height + 'px';
atrLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)';
atrLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)';
atrLine.style.pointerEvents = 'none';
atrLine.style.zIndex = '1000';
document.body.appendChild(atrLine);
}
}
}
} catch (e) {
console.debug('十字线同步出错:', e);
}
} else {
// 当十字线离开时,清除垂直线
try {
const existingVolumeLines = document.querySelectorAll('.volume-crosshair-line');
existingVolumeLines.forEach(line => line.remove());
const existingMacdLines = document.querySelectorAll('.macd-crosshair-line');
existingMacdLines.forEach(line => line.remove());
const existingAtrLines = document.querySelectorAll('.atr-crosshair-line');
existingAtrLines.forEach(line => line.remove());
} catch (e) {
console.debug('清除十字线时出错:', e);
}
}
if (param.time && param.point) {
const timeStr = param.time;
const markers = [...buyMarkers, ...sellMarkers].filter(m => m.time === timeStr);
// 同时检查分型标记
const fxMarkers = (window.fxMarkers || []).filter(m => m.time === timeStr);
const allMarkers = [...markers, ...fxMarkers];
// 显示时区调试信息
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 (allMarkers.length > 0) {
// 有买卖点或分型标记,显示自定义提示
const tooltips = allMarkers.map(m => m.tooltip).join('<br><hr style="margin: 5px 0;">');
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 updateTables(currentData) {
// 检查数据有效性
if (!currentData) {
console.error('updateTables: 传入的数据为空');
return;
}
const data = currentData;
// 检查用户选择的是主周期还是小周期数据
const useElementPeriod = $('#elementPeriodKline').is(':checked');
console.log('数据表显示周期选择:', useElementPeriod ? '小周期' : '主周期');
// 笔数据表更新
if (tables.bi) {
tables.bi.clear().destroy();
}
// 根据用户选择决定使用哪个周期的数据
let biData, biSource;
if (useElementPeriod && data.element_bi_list && data.element_bi_list.length > 0) {
biData = data.element_bi_list;
biSource = '小周期';
} else {
biData = data.bi_list;
biSource = '主周期';
}
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: 'sure_time', render: formatConfirmTime },
{ 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();
}
// 根据用户选择决定使用哪个周期的数据
let segData, segSource;
if (useElementPeriod && data.element_seg_list && data.element_seg_list.length > 0) {
segData = data.element_seg_list;
segSource = '小周期';
} else {
segData = data.seg_list;
segSource = '主周期';
}
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: 'sure_time', render: formatConfirmTime },
{ data: 'start_price', render: formatPrice },
{ data: 'end_price', render: formatPrice },
{ data: 'direction', render: formatDirection }
]
});
// 中枢数据表更新
if (tables.zs) {
tables.zs.clear().destroy();
}
// 根据用户选择决定使用哪个周期的数据
let zsData, zsSource;
if (useElementPeriod && data.element_zs_list && data.element_zs_list.length > 0) {
zsData = data.element_zs_list;
zsSource = '小周期';
} else {
zsData = data.zs_list;
zsSource = '主周期';
}
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.tradePoints) {
tables.tradePoints.clear().destroy();
}
// 根据用户选择决定使用哪个周期的数据
let tradePointsData, tradePointsSource;
if (useElementPeriod && data.element_trade_points && data.element_trade_points.length > 0) {
tradePointsData = data.element_trade_points;
tradePointsSource = '小周期';
} else {
tradePointsData = data.trade_points;
tradePointsSource = '主周期';
}
console.log(`表格显示${tradePointsSource}买卖点数据,共${tradePointsData ? tradePointsData.length : 0}条`);
tables.tradePoints = $('#tradePointsTable').DataTable({
data: tradePointsData || [],
order: [[0, 'desc']],
pageLength: 25,
columns: [
{ data: 'time', render: formatTime },
{ data: 'price', render: formatPrice },
{ data: 'type', render: formatTradePointType },
{ data: 'desc' }
]
});
// 更新数据源信息显示
const selectedPeriod = useElementPeriod ? '小周期' : '主周期';
const timeframe = useElementPeriod && data.element_timeframe ? data.element_timeframe : $('#timeframe').val();
$('#dataSourceText').html(`当前显示的是<strong>${selectedPeriod} (${timeframe})</strong> 数据`);
// K线数据表更新
if (tables.kline) {
tables.kline.clear().destroy();
}
// 根据用户选择决定使用哪个周期的K线数据
let klineData, klineSource;
if (useElementPeriod && data.element_kline_data && data.element_kline_data.length > 0) {
klineData = data.element_kline_data;
klineSource = '小周期';
} else {
klineData = data.kline_data;
klineSource = '主周期';
}
console.log(`表格显示${klineSource}K线数据,共${klineData ? klineData.length : 0}条`);
tables.kline = $('#klineTable').DataTable({
data: klineData || [],
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.uncompletedZs) {
tables.uncompletedZs.clear().destroy();
}
// 根据用户选择决定使用哪个周期的数据
let uncompletedZsData, uncompletedZsSource;
if (useElementPeriod && data.element_uncompleted_zs_list && data.element_uncompleted_zs_list.length > 0) {
uncompletedZsData = data.element_uncompleted_zs_list;
uncompletedZsSource = '小周期';
} else {
uncompletedZsData = data.uncompleted_zs_list;
uncompletedZsSource = '主周期';
}
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数据表更新
if (tables.macd) {
tables.macd.clear().destroy();
}
// 根据用户选择决定使用哪个周期的MACD数据
let macdDisplayData = [];
let macdSource;
if (useElementPeriod && data.element_kline_data && data.element_macd) {
// 使用小周期数据
macdSource = '小周期';
macdDisplayData = data.element_kline_data.map((item, index) => {
return {
time: item.date,
close: item.close,
macd: data.element_macd.macd[index],
signal: data.element_macd.signal[index],
histogram: data.element_macd.histogram[index]
};
});
} else if (data.kline_data && data.macd) {
// 使用主周期数据
macdSource = '主周期';
macdDisplayData = 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]
};
});
}
console.log(`表格显示${macdSource}MACD数据,共${macdDisplayData.length}条`);
tables.macd = $('#macdTable').DataTable({
data: macdDisplayData,
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 useElementPeriod = $('#elementPeriodKline').is(':checked');
const mainTimeframe = $('#timeframe').val();
const elementTimeframe = data.element_timeframe || mainTimeframe;
// 添加标签点击事件
$('#kline-tab, #macd-tab').off('click').on('click', function() {
$('.data-source-info').show();
if (useElementPeriod && data.element_kline_data && data.element_kline_data.length > 0) {
$('#dataSourceText').html(`当前显示的是<strong>小周期 (${elementTimeframe})</strong> 数据`);
} else {
$('#dataSourceText').html(`当前显示的是<strong>主周期 (${mainTimeframe})</strong> 数据`);
}
});
$('#bi-tab').off('click').on('click', function() {
$('.data-source-info').show();
if (useElementPeriod && data.element_bi_list && data.element_bi_list.length > 0) {
$('#dataSourceText').html(`当前显示的是<strong>小周期 (${elementTimeframe})</strong> 笔数据`);
} else {
$('#dataSourceText').html(`当前显示的是<strong>主周期 (${mainTimeframe})</strong> 笔数据`);
}
});
$('#seg-tab').off('click').on('click', function() {
$('.data-source-info').show();
if (useElementPeriod && data.element_seg_list && data.element_seg_list.length > 0) {
$('#dataSourceText').html(`当前显示的是<strong>小周期 (${elementTimeframe})</strong> 线段数据`);
} else {
$('#dataSourceText').html(`当前显示的是<strong>主周期 (${mainTimeframe})</strong> 线段数据`);
}
});
$('#zs-tab').off('click').on('click', function() {
$('.data-source-info').show();
if (useElementPeriod && data.element_zs_list && data.element_zs_list.length > 0) {
$('#dataSourceText').html(`当前显示的是<strong>小周期 (${elementTimeframe})</strong> 中枢数据`);
} else {
$('#dataSourceText').html(`当前显示的是<strong>主周期 (${mainTimeframe})</strong> 中枢数据`);
}
});
$('#trade-points-tab').off('click').on('click', function() {
$('.data-source-info').show();
if (useElementPeriod && data.element_trade_points && data.element_trade_points.length > 0) {
$('#dataSourceText').html(`当前显示的是<strong>小周期 (${elementTimeframe})</strong> 买卖点数据`);
} else {
$('#dataSourceText').html(`当前显示的是<strong>主周期 (${mainTimeframe})</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);
}
// 初始化数据源切换
$('#dataSource').on('change', function() {
const dataSource = $(this).val();
if (dataSource === 'crypto') {
$('#cryptoSymbolContainer').show();
$('#astockSymbolContainer').hide();
// 停止A股状态更新器
if (window.astockStatusInterval) {
clearInterval(window.astockStatusInterval);
window.astockStatusInterval = null;
}
} else if (dataSource === 'a_stock') {
$('#cryptoSymbolContainer').hide();
$('#astockSymbolContainer').show();
// 加载A股数据时,如果还没有加载股票列表,可以在这里触发加载
loadAStockSymbols();
// 启动A股交易时间状态更新器
startAStockStatusUpdater();
}
});
// 检查初始数据源设置
const initialDataSource = $('#dataSource').val();
if (initialDataSource === 'a_stock') {
startAStockStatusUpdater();
}
// 初始化交易对下拉菜单
$('#symbol').val('BTC/USDT:USDT');
$('#astockSymbol').val('000001');
$('#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.atrChart = null;
// 重置系列数据
tvWidget.series = {
candleSeries: null,
lineSeries: null,
volumeSeries: null,
macdLineSeries: null,
signalLineSeries: null,
histogramSeries: null,
atrLineSeries: null,
mainBiSeries: [],
mainSegSeries: [],
mainZsSeries: [],
mainUncompletedZsSeries: [],
elementBiSeries: [],
elementSegSeries: [],
elementZsSeries: [],
elementUncompletedZsSeries: [],
tradePointSeries: [],
mainBollingerSeries: [],
elementBollingerSeries: []
};
} catch (e) {
console.error('销毁图表错误:', e);
}
}
// 使用新的时区重新初始化图表
initTradingView($('#symbol').val(), $('#timeframe').val());
// 重新渲染图表数据
renderChart();
// 更新表格
updateTables(currentData);
}
});
// 页面加载完成后初始化
$(document).ready(function() {
// 设置默认的筛选时间(最近7天)
const now = new Date();
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
$('#filterEndTime').val(now.toISOString().slice(0, 16));
$('#filterStartTime').val(weekAgo.toISOString().slice(0, 16));
// 初始化股票筛选表格
initStockFilterTable();
// 添加页面滚动事件监听器,清除十字线延长线
$(window).on('scroll', function() {
try {
// 清除所有十字线延长线,防止它们跟着页面滚动
const existingVolumeLines = document.querySelectorAll('.volume-crosshair-line');
existingVolumeLines.forEach(line => line.remove());
const existingMacdLines = document.querySelectorAll('.macd-crosshair-line');
existingMacdLines.forEach(line => line.remove());
} catch (e) {
console.debug('清除滚动中的十字线时出错:', e);
}
});
// 突出显示股票筛选tab (已禁用)
setTimeout(function() {
const stockFilterTab = $('#stock-filter-tab');
if (false && stockFilterTab.length > 0) {
console.log('股票筛选tab已找到,开始突出显示');
// 添加闪烁效果来吸引注意
stockFilterTab.addClass('animate__animated animate__pulse');
// 滚动到tab区域
$('html, body').animate({
scrollTop: $('.data-container').offset().top - 100
}, 1000);
// 添加提示信息
const alertDiv = $(`
<div class="alert alert-info alert-dismissible fade show" role="alert" style="position: fixed; top: 20px; right: 20px; z-index: 9999; max-width: 400px;">
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`);
$('body').append(alertDiv);
// 10秒后自动隐藏提示
setTimeout(() => {
alertDiv.alert('close');
}, 10000);
}
// 已禁用突出显示功能,无需错误日志
}, 2000);
});
});
// 自动刷新相关变量
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 = true;
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());
// 在回放模式下立即恢复范围,避免跳跃效果
if (isReplaying) {
// 回放模式下立即设置范围,减少延迟
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('图表可见范围已恢复(回放模式)');
}
}, 50); // 回放模式下减少延迟到50ms
} else {
// 非回放模式下保持原有的延迟
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: 'sure_time', render: formatConfirmTime },
{ 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: 'sure_time', render: formatConfirmTime },
{ 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;
// 提取保存的视图状态
const savedViewState = data._savedViewState;
if (savedViewState) {
console.log('检测到保存的视图状态,将在图表更新后恢复:', savedViewState);
}
// 如果图表未初始化,则创建图表,否则更新图表数据
if (!tvWidget.state.isInitialized) {
console.log('图表尚未初始化,创建新图表');
initTradingView($('#symbol').val(), $('#timeframe').val());
// 如果有保存的视图状态,在图表初始化后恢复
if (savedViewState) {
setTimeout(() => {
restoreViewState(savedViewState);
}, 500); // 给图表足够时间完成初始化
}
} else {
console.log('图表已初始化,进行增量更新');
// 如果有保存的视图状态,先将其存储到tvWidget.state中
if (savedViewState) {
tvWidget.state.visibleRange = savedViewState.visibleRange;
tvWidget.state.logicalRange = savedViewState.logicalRange;
console.log('已将保存的视图状态存储到tvWidget.state');
}
updateTradingViewData();
}
// 更新表格数据
updateTables(data);
}
function refreshChartOnly() {
// 仅使用当前数据刷新图表显示,不从服务器加载新数据
if (currentData) {
console.log('仅刷新图表显示,不重新获取数据');
refreshChart(currentData);
} else {
console.log('没有当前数据,无法刷新显示');
}
}
// 恢复所有图表的视图状态,包括ATR图表
function restoreViewState(viewState) {
if (!viewState || !tvWidget) {
console.warn('无效的视图状态或图表对象,跳过恢复');
return;
}
console.log('开始恢复所有图表的视图状态:', viewState);
try {
// 恢复主图表
if (tvWidget.mainChart && tvWidget.mainChart.timeScale) {
if (viewState.logicalRange) {
tvWidget.mainChart.timeScale().setVisibleLogicalRange(viewState.logicalRange);
console.log('✅ 主图表视图状态已恢复 (logicalRange)');
} else if (viewState.visibleRange) {
tvWidget.mainChart.timeScale().setVisibleRange(viewState.visibleRange);
console.log('✅ 主图表视图状态已恢复 (visibleRange)');
}
}
// 恢复成交量图表
if (tvWidget.volumeChart && tvWidget.volumeChart.timeScale) {
if (viewState.logicalRange) {
tvWidget.volumeChart.timeScale().setVisibleLogicalRange(viewState.logicalRange);
console.log('✅ 成交量图表视图状态已恢复');
} else if (viewState.visibleRange) {
tvWidget.volumeChart.timeScale().setVisibleRange(viewState.visibleRange);
console.log('✅ 成交量图表视图状态已恢复');
}
}
// 恢复MACD图表
if (tvWidget.macdChart && tvWidget.macdChart.timeScale) {
if (viewState.logicalRange) {
tvWidget.macdChart.timeScale().setVisibleLogicalRange(viewState.logicalRange);
console.log('✅ MACD图表视图状态已恢复');
} else if (viewState.visibleRange) {
tvWidget.macdChart.timeScale().setVisibleRange(viewState.visibleRange);
console.log('✅ MACD图表视图状态已恢复');
}
}
// 恢复ATR图表
if (tvWidget.atrChart && tvWidget.atrChart.timeScale) {
if (viewState.logicalRange) {
tvWidget.atrChart.timeScale().setVisibleLogicalRange(viewState.logicalRange);
console.log('✅ ATR图表视图状态已恢复 (logicalRange)');
} else if (viewState.visibleRange) {
tvWidget.atrChart.timeScale().setVisibleRange(viewState.visibleRange);
console.log('✅ ATR图表视图状态已恢复 (visibleRange)');
}
}
// 同时更新tvWidget.state以保持状态同步
if (tvWidget.state) {
tvWidget.state.visibleRange = viewState.visibleRange;
tvWidget.state.logicalRange = viewState.logicalRange;
}
console.log('🎉 所有图表视图状态恢复完成');
} catch (e) {
console.error('❌ 恢复图表视图状态时出错:', e);
}
}
// 绑定主周期MACD背离显示开关
$('#showMainMacdDiv').change(function() {
refreshChartOnly();
});
// 绑定次周期MACD背离显示开关
$('#showElementMacdDiv').change(function() {
refreshChartOnly();
});
// 绑定分型类型显示开关
$('#showKlcFxType').change(function() {
refreshChartOnly();
});
// 绑定小周期分型显示开关
$('#showElementKlcFxType').change(function() {
refreshChart(currentData);
});
// 绑定KLU分型显示开关
$('#showKluFxType').change(function() {
refreshChartOnly();
});
// 绑定小周期KLU分型显示开关
$('#showElementKluFxType').change(function() {
refreshChart(currentData);
});
// 绑定买卖点显示开关
$('#showTradePoints').change(function() {
refreshChart(currentData);
});
// 绑定布林带显示变更事件
$('#showMainBollinger').change(function() {
updateChartDisplay();
});
$('#showElementBollinger').change(function() {
updateChartDisplay();
});
// 绑定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': false,
'showMacd': true,
'showKlcFxType': $('#showKlcFxType').is(':checked'),
'showKluFxType': $('#showKluFxType').is(':checked'),
'showElementKlcFxType': $('#showElementKlcFxType').is(':checked'),
'showElementKluFxType': $('#showElementKluFxType').is(':checked'),
'showTradePoints': $('#showTradePoints').is(':checked'),
'showMainBollinger': $('#showMainBollinger').is(':checked'),
'showElementBollinger': $('#showElementBollinger').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();
// 使用新的回放数据API
$.ajax({
url: '/api/replay_data',
data: {
symbol: symbol,
timeframe: timeframe,
timezone: timezone,
element_timeframe: elementTimeframe,
start_time: replayStartTime,
end_time: replayEndTime
},
success: function(data) {
// 检查是否有有效数据
if (!data || !data.replay_data || Object.keys(data.replay_data).length === 0) {
$('#replayStatus').text('服务器返回的回放数据无效');
$('#replayProgress').hide();
$('#startReplay').show();
$('#stopReplay').hide();
console.error('服务器返回的回放数据无效:', data);
return;
}
// 保存回放数据
replayData = data.replay_data;
totalReplaySteps = data.total_length;
console.log('回放数据加载完成,总步骤数:', totalReplaySteps);
console.log('回放数据已从后台预计算完成,确保不会使用未来数据');
// 初始化回放
initReplay();
},
error: function(jqXHR, textStatus, errorThrown) {
$('#replayStatus').text('生成回放数据失败');
$('#replayProgress').hide();
$('#startReplay').show();
$('#stopReplay').hide();
alert('生成回放数据失败: ' + (jqXHR.responseJSON?.error || errorThrown));
}
});
}
// 初始化回放
function initReplay() {
if (!replayData || Object.keys(replayData).length === 0) {
$('#replayStatus').text('没有可回放的数据');
$('#replayProgress').hide();
$('#startReplay').show();
$('#stopReplay').hide();
return;
}
// 设置回放变量
isReplaying = true;
currentReplayIndex = 0;
// totalReplaySteps已经在startDataReplay中设置了
// 更新回放状态
$('#replayStatus').text(`准备回放 (0/${totalReplaySteps})`);
$('.progress-bar').css('width', '0%');
// 预先锁定图表,防止任何自动调整
lockChartsForReplay();
// 准备初始数据(显示第一个时间点的数据)
if (replayData[0]) {
currentData = replayData[0];
updateTradingViewData();
updateTables(currentData);
}
// 开始回放
continueDataReplay();
}
// 为回放优化图表设置,减少跳跃效果
function optimizeChartForReplay() {
if (!tvWidget || !tvWidget.mainChart) return;
try {
console.log('优化图表设置以减少回放跳跃...');
// 临时禁用动画,提高性能
if (tvWidget.mainChart.applyOptions) {
tvWidget.mainChart.applyOptions({
timeScale: {
rightOffset: 5, // 减少右侧偏移
barSpacing: 6, // 固定K线间距
fixLeftEdge: false,
fixRightEdge: false,
lockVisibleTimeRangeOnResize: true // 窗口大小改变时锁定可视范围
}
});
}
if (tvWidget.volumeChart && tvWidget.volumeChart.applyOptions) {
tvWidget.volumeChart.applyOptions({
timeScale: {
rightOffset: 5,
barSpacing: 6,
lockVisibleTimeRangeOnResize: true
}
});
}
if (tvWidget.macdChart && tvWidget.macdChart.applyOptions) {
tvWidget.macdChart.applyOptions({
timeScale: {
rightOffset: 5,
barSpacing: 6,
lockVisibleTimeRangeOnResize: true
}
});
}
console.log('图表优化设置完成');
} catch (e) {
console.error('优化图表设置失败:', e);
}
}
// 锁定图表,阻止所有自动调整行为
function lockChartsForReplay() {
try {
if (tvWidget && tvWidget.mainChart) {
// 完全禁用自动缩放和范围调整
tvWidget.mainChart.applyOptions({
handleScroll: false,
handleScale: false,
timeScale: {
rightOffset: 0,
fixLeftEdge: true,
fixRightEdge: true,
lockVisibleTimeRangeOnResize: true,
borderVisible: false
},
priceScale: {
autoScale: false,
borderVisible: false
}
});
if (tvWidget.volumeChart) {
tvWidget.volumeChart.applyOptions({
handleScroll: false,
handleScale: false,
timeScale: {
rightOffset: 0,
fixLeftEdge: true,
fixRightEdge: true,
lockVisibleTimeRangeOnResize: true,
borderVisible: false
},
priceScale: {
autoScale: false,
borderVisible: false
}
});
}
if (tvWidget.macdChart) {
tvWidget.macdChart.applyOptions({
handleScroll: false,
handleScale: false,
timeScale: {
rightOffset: 0,
fixLeftEdge: true,
fixRightEdge: true,
lockVisibleTimeRangeOnResize: true,
borderVisible: false
},
priceScale: {
autoScale: false,
borderVisible: false
}
});
}
console.log('图表已锁定,禁用所有自动调整');
}
} catch (e) {
console.error('锁定图表失败:', e);
}
}
// 解锁图表,恢复正常交互
function unlockChartsAfterReplay() {
try {
if (tvWidget && tvWidget.mainChart) {
// 恢复正常的交互和自动调整
tvWidget.mainChart.applyOptions({
handleScroll: true,
handleScale: true,
timeScale: {
rightOffset: 12,
fixLeftEdge: false,
fixRightEdge: false,
lockVisibleTimeRangeOnResize: false,
borderVisible: true
},
priceScale: {
autoScale: true,
borderVisible: true
}
});
if (tvWidget.volumeChart) {
tvWidget.volumeChart.applyOptions({
handleScroll: true,
handleScale: true,
timeScale: {
rightOffset: 12,
fixLeftEdge: false,
fixRightEdge: false,
lockVisibleTimeRangeOnResize: false,
borderVisible: true
},
priceScale: {
autoScale: true,
borderVisible: true
}
});
}
if (tvWidget.macdChart) {
tvWidget.macdChart.applyOptions({
handleScroll: true,
handleScale: true,
timeScale: {
rightOffset: 12,
fixLeftEdge: false,
fixRightEdge: false,
lockVisibleTimeRangeOnResize: false,
borderVisible: true
},
priceScale: {
autoScale: true,
borderVisible: true
}
});
}
console.log('图表已解锁,恢复正常交互');
}
} catch (e) {
console.error('解锁图表失败:', e);
}
}
// 固定y轴范围,保持图表不因数据变化而变化
function fixYAxisRange() {
if (!tvWidget.mainChart || !replayData || !replayData.kline_data || replayData.kline_data.length === 0) {
console.log('无法固定Y轴范围:图表或数据不可用');
return;
}
// 获取全部数据的最高价和最低价,为了适当的范围
let minPrice = Infinity;
let maxPrice = -Infinity;
// 计算整个数据集的价格范围
replayData.kline_data.forEach(kline => {
const high = parseFloat(kline.high);
const low = parseFloat(kline.low);
if (!isNaN(high) && high > maxPrice) maxPrice = high;
if (!isNaN(low) && low < minPrice) minPrice = low;
});
// 检查是否获得了有效的价格范围
if (minPrice === Infinity || maxPrice === -Infinity) {
console.warn('无法获得有效的价格范围,跳过Y轴固定');
return;
}
// 添加一些边距(约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
});
console.log(`Y轴范围已固定: ${minPrice.toFixed(2)} - ${maxPrice.toFixed(2)}`);
} 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 currentStepData = replayData[currentReplayIndex - 1]; // 索引从0开始
if (!currentStepData) {
console.error(`回放步骤 ${currentReplayIndex} 没有数据`);
return;
}
// 更新当前数据
currentData = currentStepData;
// 直接更新数据,图表已锁定不会跳跃
console.log(`回放步骤 ${currentReplayIndex}/${totalReplaySteps}:使用预计算数据`);
try {
updateTradingViewData();
// 表格更新频率降低,每20步更新一次
if (currentReplayIndex % 20 === 0 || currentReplayIndex === totalReplaySteps) {
updateTables(currentData);
}
// 不再需要频繁的Y轴调整和视图调整,因为图表已锁定
} catch (e) {
console.error('回放步骤更新失败:', e);
}
// 如果到达最后一步,停止回放
if (currentReplayIndex >= totalReplaySteps) {
$('#replayStatus').text('回放完成');
// 回放完成时解锁图表
unlockChartsAfterReplay();
pauseDataReplay();
}
}
// 保持最新数据可见
function keepLatestDataVisible() {
if (!tvWidget.mainChart || !currentData || !currentData.kline_data || currentData.kline_data.length === 0) return;
try {
// 在回放模式下,更平滑地调整视图范围
const lastBar = currentData.kline_data[currentData.kline_data.length - 1];
const lastTime = Math.floor(new Date(lastBar.date).getTime() / 1000);
// 如果已经有保存的视图范围,则基于它进行微调,避免大幅跳跃
if (tvWidget.state.logicalRange) {
const currentLogicalRange = tvWidget.mainChart.timeScale().getVisibleLogicalRange();
const currentTimeRange = tvWidget.mainChart.timeScale().getVisibleRange();
if (currentTimeRange && currentTimeRange.to) {
// 检查最新数据是否已经在可视范围内
const timeDiff = lastTime - currentTimeRange.to;
// 只有当最新数据超出当前可视范围的一定程度时才调整
if (timeDiff > 0) {
const barCount = Math.min(50, currentData.kline_data.length);
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);
// 平滑地扩展视图范围,而不是突然跳跃
const newRange = {
from: Math.min(currentTimeRange.from || firstTime, firstTime),
to: lastTime
};
tvWidget.mainChart.timeScale().setVisibleRange(newRange);
if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleRange(newRange);
if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleRange(newRange);
console.log('平滑调整视图范围:', newRange);
}
}
} else {
// 初始情况,设置默认的视图范围
const barCount = Math.min(50, currentData.kline_data.length);
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);
const newRange = {
from: firstTime,
to: lastTime
};
tvWidget.mainChart.timeScale().setVisibleRange(newRange);
if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleRange(newRange);
if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleRange(newRange);
console.log('设置初始视图范围:', newRange);
}
} catch (e) {
console.error('保持最新数据可见失败:', e);
}
}
// 更新回放状态显示
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;
// 清空当前数据,确保下次回放是干净状态
currentData = null;
// 更新UI
$('#replayStatus').text('');
$('#replayProgress').hide();
$('#pauseReplay').hide();
$('#stopReplay').hide();
$('#startReplay').show();
// 恢复图表自动缩放和正常设置
if (tvWidget.mainChart) {
tvWidget.mainChart.priceScale().applyOptions({
autoScale: true
});
// 恢复图表的正常设置
tvWidget.mainChart.applyOptions({
timeScale: {
rightOffset: 12, // 恢复默认右侧偏移
barSpacing: 6,
fixLeftEdge: false,
fixRightEdge: false,
lockVisibleTimeRangeOnResize: false // 恢复窗口调整时的自动适应
}
});
if (tvWidget.volumeChart) {
tvWidget.volumeChart.applyOptions({
timeScale: {
rightOffset: 12,
barSpacing: 6,
lockVisibleTimeRangeOnResize: false
}
});
}
if (tvWidget.macdChart) {
tvWidget.macdChart.applyOptions({
timeScale: {
rightOffset: 12,
barSpacing: 6,
lockVisibleTimeRangeOnResize: false
}
});
}
}
// 解锁图表,恢复正常交互
unlockChartsAfterReplay();
console.log('回放已停止,数据已清空,图表已解锁');
}
// 获取A股股票列表
function loadAStockSymbols() {
$.get('/api/popular_a_stocks', function(data) {
if (Array.isArray(data)) {
const $select = $('#astockSymbol');
const currentSymbol = $select.val(); // 保存当前选中的值
$select.empty();
data.forEach(function(stock) {
$select.append($('<option>', {
value: stock.symbol,
text: stock.symbol + ' - ' + stock.name
}));
});
// 如果有保存的选中值,恢复它
if (currentSymbol && data.some(stock => stock.symbol === currentSymbol)) {
$select.val(currentSymbol);
} else {
// 设置默认值为平安银行
$select.val('000001');
}
}
}).fail(function() {
console.error('加载A股股票列表失败');
});
}
// 检测交易对类型并返回相应的配置
function getSymbolConfig(symbol) {
const isAStock = symbol && symbol.length === 6 && /^\d+$/.test(symbol);
if (isAStock) {
return {
type: 'a_stock',
displayName: symbol,
tradingSessions: [
// A股交易时间配置
{ start: '09:30', end: '11:30' }, // 上午
{ start: '13:00', end: '15:00' } // 下午
],
timezone: 'Asia/Shanghai',
// A股的交易日配置(周一到周五,除节假日)
tradingDays: [1, 2, 3, 4, 5] // 1=周一, 7=周日
};
} else {
return {
type: 'crypto',
displayName: symbol,
tradingSessions: [
{ start: '00:00', end: '23:59' } // 24小时交易
],
timezone: 'UTC',
tradingDays: [1, 2, 3, 4, 5, 6, 7] // 7天交易
};
}
}
// 根据交易对类型调整图表配置
function adjustChartForSymbolType(chartOptions, symbolConfig) {
if (symbolConfig.type === 'a_stock') {
// A股特殊配置
chartOptions.timeScale = {
...chartOptions.timeScale,
// 禁用非交易时间的显示
borderVisible: true,
borderColor: '#ddd',
// 自定义时间格式化,只显示交易时间
timeVisible: true,
// 添加A股特定的时间范围限制
rightOffset: 12,
barSpacing: 6,
minBarSpacing: 3,
};
// 添加A股交易时间提示
chartOptions.layout = {
...chartOptions.layout,
fontSize: 12,
fontFamily: 'Arial, sans-serif'
};
}
return chartOptions;
}
// 过滤非交易时间的数据(仅用于显示优化)
function filterTradingHours(data, symbolConfig) {
if (symbolConfig.type !== 'a_stock') {
return data; // 非A股数据不需要过滤
}
return data.filter(item => {
const date = new Date(item.time * 1000);
const hour = date.getHours();
const minute = date.getMinutes();
const timeStr = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
// 检查是否在交易时间内
return symbolConfig.tradingSessions.some(session => {
return timeStr >= session.start && timeStr <= session.end;
});
});
}
// 更新A股交易时间状态
function updateAStockTradingStatus() {
const now = new Date();
const chinaTime = new Date(now.toLocaleString("en-US", {timeZone: "Asia/Shanghai"}));
const hour = chinaTime.getHours();
const minute = chinaTime.getMinutes();
const dayOfWeek = chinaTime.getDay(); // 0=周日, 1=周一, ..., 6=周六
const statusElement = document.getElementById('tradingTimeStatus');
if (!statusElement) return;
// 检查是否为交易日(周一到周五)
const isTradingDay = dayOfWeek >= 1 && dayOfWeek <= 5;
if (!isTradingDay) {
statusElement.className = 'badge bg-secondary';
statusElement.textContent = '非交易日';
return;
}
// 检查是否在交易时间内
const currentTime = hour * 60 + minute; // 转换为分钟
const morningStart = 9 * 60 + 30; // 09:30
const morningEnd = 11 * 60 + 30; // 11:30
const afternoonStart = 13 * 60; // 13:00
const afternoonEnd = 15 * 60; // 15:00
let status = '';
let className = '';
if (currentTime >= morningStart && currentTime <= morningEnd) {
status = '上午交易中';
className = 'badge bg-success';
} else if (currentTime >= afternoonStart && currentTime <= afternoonEnd) {
status = '下午交易中';
className = 'badge bg-success';
} else if (currentTime > morningEnd && currentTime < afternoonStart) {
status = '午间休市';
className = 'badge bg-warning';
} else if (currentTime < morningStart) {
status = '开盘前';
className = 'badge bg-info';
} else if (currentTime > afternoonEnd) {
status = '收盘后';
className = 'badge bg-dark';
} else {
status = '非交易时间';
className = 'badge bg-secondary';
}
statusElement.className = className;
statusElement.textContent = status;
}
// 启动A股交易时间状态更新
function startAStockStatusUpdater() {
// 如果已经有定时器在运行,先清除
if (window.astockStatusInterval) {
clearInterval(window.astockStatusInterval);
}
// 立即更新一次
updateAStockTradingStatus();
// 每30秒更新一次
window.astockStatusInterval = setInterval(updateAStockTradingStatus, 30000);
console.log('A股交易时间状态更新器已启动');
}
// 股票筛选相关函数
let stockFilterTable = null;
// 初始化股票筛选表格
function initStockFilterTable() {
// 简单的表格初始化,不使用DataTable
console.log('初始化股票筛选表格');
}
// 筛选股票
function filterStocks() {
const startTime = $('#filterStartTime').val();
const endTime = $('#filterEndTime').val();
const timeframe = $('#filterTimeframe').val();
const threshold = parseFloat($('#fxStrengthThreshold').val());
if (!startTime || !endTime) {
alert('请选择开始时间和结束时间');
return;
}
if (new Date(startTime) >= new Date(endTime)) {
alert('开始时间必须早于结束时间');
return;
}
// 显示进度指示器
$('#filterProgress').show();
$('#stockFilterTable tbody').empty();
// 发送筛选请求
$.ajax({
url: '/api/filter_stocks',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({
start_time: startTime,
end_time: endTime,
timeframe: timeframe,
fx_strength_threshold: threshold
}),
timeout: 300000, // 5分钟超时
success: function(response) {
$('#filterProgress').hide();
if (response.error) {
// 根据错误类型显示不同的错误信息
if (response.error_type === 'network_error') {
showNetworkErrorAlert(response.error, response.suggestion);
} else {
alert('筛选失败: ' + response.error);
}
return;
}
// 更新表格数据
updateStockFilterTable(response.results);
// 显示统计信息
const totalCount = response.results.length;
const processedCount = response.total_processed || 0;
const failedCount = response.failed_count || 0;
const dataSource = response.data_source || '未知';
let message = `筛选完成!使用${dataSource},共处理 ${processedCount} 只股票,找到 ${totalCount} 只满足条件的股票`;
if (failedCount > 0) {
message += `${failedCount} 只股票数据获取失败`;
}
// 显示成功提示
showSuccessAlert(message);
},
error: function(xhr, status, error) {
$('#filterProgress').hide();
console.error('筛选请求失败:', error, status, xhr);
// 根据错误类型显示不同的错误信息
if (status === 'timeout') {
showNetworkErrorAlert(
'请求超时,可能是网络连接不稳定或数据量较大',
'请检查网络连接,或尝试缩小时间范围后重试'
);
} else if (xhr.responseJSON && xhr.responseJSON.error_type === 'network_error') {
showNetworkErrorAlert(xhr.responseJSON.error, xhr.responseJSON.suggestion);
} else {
alert('筛选请求失败: ' + (xhr.responseJSON?.error || error || '未知错误'));
}
}
});
}
// 显示网络错误提示
function showNetworkErrorAlert(errorMessage, suggestion) {
const alertDiv = $(`
<div class="alert alert-warning alert-dismissible fade show" role="alert">
<h6><i class="fas fa-exclamation-triangle"></i> 网络连接问题</h6>
<p><strong>错误信息:</strong>${errorMessage}</p>
<p><strong>建议:</strong>${suggestion}</p>
<hr>
<p class="mb-0">
<small>
<i class="fas fa-info-circle"></i>
如果问题持续存在,请检查网络连接或联系管理员
</small>
</p>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`);
$('#stock-filter .container-fluid').prepend(alertDiv);
// 10秒后自动隐藏提示
setTimeout(() => {
alertDiv.alert('close');
}, 10000);
}
// 显示成功提示
function showSuccessAlert(message) {
const alertDiv = $(`
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="fas fa-check-circle"></i> ${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`);
$('#stock-filter .container-fluid').prepend(alertDiv);
// 5秒后自动隐藏提示
setTimeout(() => {
alertDiv.alert('close');
}, 5000);
}
// 更新股票筛选表格
function updateStockFilterTable(results) {
const tbody = $('#stockFilterTable tbody');
tbody.empty();
if (!results || results.length === 0) {
tbody.append('<tr><td colspan="9" class="text-center">没有找到满足条件的股票</td></tr>');
return;
}
// 按分型强度降序排列
results.sort((a, b) => b.fx_strength - a.fx_strength);
// 添加新数据
results.forEach(function(stock) {
const strengthClass = getStrengthClass(stock.fx_strength);
const changeClass = stock.change_percent >= 0 ? 'text-danger' : 'text-success';
const changeSign = stock.change_percent >= 0 ? '+' : '';
const row = `
<tr>
<td>${stock.symbol}</td>
<td>${stock.name}</td>
<td>${stock.fx_time}</td>
<td>${stock.fx_type}</td>
<td><span class="${strengthClass}">${stock.fx_strength.toFixed(2)}</span></td>
<td>${stock.fx_price.toFixed(2)}</td>
<td>${stock.current_price ? stock.current_price.toFixed(2) : '-'}</td>
<td><span class="${changeClass}">${changeSign}${(stock.change_percent || 0).toFixed(2)}%</span></td>
<td><button class="btn btn-sm btn-outline-primary" onclick="analyzeStock('${stock.symbol}')">分析</button></td>
</tr>
`;
tbody.append(row);
});
}
// 获取分型强度对应的CSS类
function getStrengthClass(strength) {
if (strength >= 2.0) return 'text-danger fw-bold';
else if (strength >= 1.5) return 'text-warning fw-bold';
else if (strength >= 1.0) return 'text-info';
else return '';
}
// 分析特定股票
function analyzeStock(symbol) {
// 切换到主分析页面
$('#stock-filter-tab').removeClass('active');
$('#kline-tab').addClass('active');
$('#stock-filter').removeClass('show active');
$('#kline').addClass('show active');
// 切换数据源为A股
$('#dataSource').val('a_stock');
$('#dataSource').trigger('change');
// 等待数据源切换完成后设置股票代码
setTimeout(() => {
$('#astockSymbol').val(symbol);
// 触发分析
updateChart();
}, 500);
}
// 导出筛选结果
function exportFilterResults() {
const tbody = $('#stockFilterTable tbody tr');
if (tbody.length === 0 || (tbody.length === 1 && tbody.find('td').length === 1)) {
alert('没有可导出的数据');
return;
}
// 创建CSV内容
const headers = ['股票代码', '股票名称', '分型时间', '分型类型', '分型强度', '分型价格', '当前价格', '涨跌幅(%)'];
let csvContent = headers.join(',') + '\n';
tbody.each(function() {
const cells = $(this).find('td');
if (cells.length > 1) { // 排除"没有数据"的行
const row = [];
cells.slice(0, 8).each(function() { // 只取前8列,排除操作列
row.push($(this).text().trim());
});
csvContent += row.join(',') + '\n';
}
});
// 创建下载链接
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', `股票筛选结果_${new Date().toISOString().slice(0, 10)}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
// 页面加载完成后初始化
$(document).ready(function() {
// 设置默认的筛选时间(最近7天)
const now = new Date();
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
$('#filterEndTime').val(now.toISOString().slice(0, 16));
$('#filterStartTime').val(weekAgo.toISOString().slice(0, 16));
// 初始化股票筛选表格
initStockFilterTable();
// 添加页面滚动事件监听器,清除十字线延长线
$(window).on('scroll', function() {
try {
// 清除所有十字线延长线,防止它们跟着页面滚动
const existingVolumeLines = document.querySelectorAll('.volume-crosshair-line');
existingVolumeLines.forEach(line => line.remove());
const existingMacdLines = document.querySelectorAll('.macd-crosshair-line');
existingMacdLines.forEach(line => line.remove());
} catch (e) {
console.debug('清除滚动中的十字线时出错:', e);
}
});
// 突出显示股票筛选tab (已禁用)
setTimeout(function() {
const stockFilterTab = $('#stock-filter-tab');
if (false && stockFilterTab.length > 0) {
console.log('股票筛选tab已找到,开始突出显示');
// 添加闪烁效果来吸引注意
stockFilterTab.addClass('animate__animated animate__pulse');
// 滚动到tab区域
$('html, body').animate({
scrollTop: $('.data-container').offset().top - 100
}, 1000);
// 添加提示信息
const alertDiv = $(`
<div class="alert alert-info alert-dismissible fade show" role="alert" style="position: fixed; top: 20px; right: 20px; z-index: 9999; max-width: 400px;">
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`);
$('body').append(alertDiv);
// 10秒后自动隐藏提示
setTimeout(() => {
alertDiv.alert('close');
}, 10000);
}
// 已禁用突出显示功能,无需错误日志
}, 2000);
});
// 初始化均线系统
$(document).ready(function() {
initMovingAverageEvents();
});
</script>
<!-- 均线配置对话框 -->
<div class="modal fade" id="movingAverageModal" tabindex="-1" aria-labelledby="movingAverageModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="movingAverageModalLabel">配置均线</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="movingAverageForm">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="maType" class="form-label">类型</label>
<select class="form-select" id="maType" required>
<option value="SMA">SMA (简单移动平均线)</option>
<option value="EMA">EMA (指数移动平均线)</option>
<option value="WMA">WMA (加权移动平均线)</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="maPeriod" class="form-label">周期长度</label>
<input type="number" class="form-control" id="maPeriod" min="1" value="20" required>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="maSource" class="form-label">价格来源</label>
<select class="form-select" id="maSource" required>
<option value="close">收盘价</option>
<option value="open">开盘价</option>
<option value="high">最高价</option>
<option value="low">最低价</option>
<option value="hl2">高低平均价 (HL2)</option>
<option value="hlc3">高低收平均价 (HLC3)</option>
<option value="ohlc4">开高低收平均价 (OHLC4)</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="maColor" class="form-label">颜色</label>
<input type="color" class="form-control form-control-color" id="maColor" value="#2962FF" title="选择均线颜色">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="maSmoothing" class="form-label">平滑算法</label>
<select class="form-select" id="maSmoothing">
<option value="none">无平滑</option>
<option value="SMA">SMA</option>
<option value="EMA">EMA</option>
<option value="WMA">WMA</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="maSmoothingPeriod" class="form-label">平滑周期</label>
<input type="number" class="form-control" id="maSmoothingPeriod" min="1" value="3" disabled>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="maLineWidth" class="form-label">线宽</label>
<input type="range" class="form-range" id="maLineWidth" min="1" max="5" value="2">
<span id="lineWidthDisplay">2</span>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="maStyle" class="form-label">线型</label>
<select class="form-select" id="maStyle">
<option value="solid">实线</option>
<option value="dashed">虚线</option>
<option value="dotted">点线</option>
</select>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="addMovingAverageConfirm">添加均线</button>
</div>
</div>
</div>
</div>
</body>
</html>