5526 lines
262 KiB
HTML
5526 lines
262 KiB
HTML
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>缠论分析系统</title>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css" rel="stylesheet">
|
||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||
<script src="https://cdn.datatables.net/1.11.5/js/jquery.dataTables.min.js"></script>
|
||
<link href="https://cdn.datatables.net/1.11.5/css/jquery.dataTables.min.css" rel="stylesheet">
|
||
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||
<!-- TradingView Widget BEGIN -->
|
||
<script src="https://cdn.jsdelivr.net/npm/lightweight-charts@4.0.1/dist/lightweight-charts.standalone.production.js"></script>
|
||
<!-- TradingView Widget END -->
|
||
<style>
|
||
body {
|
||
font-family: "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||
margin: 0;
|
||
padding: 10px;
|
||
background-color: #f8f9fa;
|
||
color: #333;
|
||
}
|
||
.container {
|
||
max-width: 100%;
|
||
margin: 0 auto;
|
||
background-color: white;
|
||
padding: 5px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||
}
|
||
.header {
|
||
margin-bottom: 0px;
|
||
padding-bottom: 0px;
|
||
}
|
||
.controls {
|
||
margin-bottom: 10px;
|
||
padding: 10px;
|
||
background-color: #f8f9fa;
|
||
border-radius: 8px;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||
}
|
||
.form-label {
|
||
font-weight: 500;
|
||
margin-bottom: 6px;
|
||
color: #495057;
|
||
}
|
||
.chart-container {
|
||
width: 100%;
|
||
height: 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;
|
||
}
|
||
|
||
/* 主次周期元素样式区分 */
|
||
</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">
|
||
{% for value, label in timeframes.items() %}
|
||
<option value="{{ value }}" {% if value == '5m' %}selected{% endif %}>{{ label }}</option>
|
||
{% endfor %}
|
||
</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>
|
||
<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;">
|
||
{% for value, label in timeframes.items() %}
|
||
<option value="{{ value }}" {% if value == '1m' %}selected{% endif %}>{{ label }}</option>
|
||
{% endfor %}
|
||
</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="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="5m">5分钟</option>
|
||
<option value="15m">15分钟</option>
|
||
<option value="30m">30分钟</option>
|
||
<option value="1h">1小时</option>
|
||
<option value="4h">4小时</option>
|
||
<option value="1d" selected>1日</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,
|
||
mainBiSeries: [],
|
||
mainSegSeries: [],
|
||
mainZsSeries: [],
|
||
mainUncompletedZsSeries: [],
|
||
elementBiSeries: [],
|
||
elementSegSeries: [],
|
||
elementZsSeries: [],
|
||
elementUncompletedZsSeries: [],
|
||
tradePointSeries: [],
|
||
mainBollingerSeries: [],
|
||
elementBollingerSeries: []
|
||
},
|
||
state: {
|
||
isInitialized: false,
|
||
visibleRange: null,
|
||
logicalRange: null
|
||
}
|
||
};
|
||
|
||
// 买卖点样式定义 - 根据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();
|
||
|
||
// 获取参数
|
||
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;
|
||
|
||
// 检查和记录服务器返回的时区
|
||
console.log('服务器返回的时区:', data.timezone || '未指定');
|
||
|
||
// 刷新图表
|
||
refreshChart(data);
|
||
},
|
||
error: function(jqXHR, textStatus, errorThrown) {
|
||
// 隐藏加载图标
|
||
$('#refreshLoadingSpinner').hide();
|
||
|
||
// 显示错误信息
|
||
console.error('加载数据失败:', errorThrown);
|
||
alert('加载数据失败: ' + (jqXHR.responseJSON?.error || errorThrown));
|
||
}
|
||
});
|
||
}
|
||
|
||
// 初始化图表
|
||
function initTradingView(symbol, timeframe) {
|
||
try {
|
||
console.log('初始化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 = '';
|
||
|
||
// 重置图表对象
|
||
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: []
|
||
},
|
||
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容器
|
||
let macdChartContainer = null;
|
||
if (showMacd) {
|
||
// 设置各图表高度 - 为三个图表分配合理比例,主图表适度增加高度
|
||
mainChartContainer.style.height = '55%'; // 主图占55%(约385px)
|
||
volumeChartContainer.style.top = '55%';
|
||
volumeChartContainer.style.height = '22.5%'; // 成交量图占22.5%(约157.5px)
|
||
|
||
macdChartContainer = document.createElement('div');
|
||
macdChartContainer.style.width = '100%';
|
||
macdChartContainer.style.height = '22.5%'; // MACD图占22.5%(约157.5px)
|
||
macdChartContainer.style.position = 'absolute';
|
||
macdChartContainer.style.top = '77.5%'; // 从77.5%位置开始
|
||
macdChartContainer.style.left = '0';
|
||
macdChartContainer.style.right = '0';
|
||
macdChartContainer.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);
|
||
|
||
// 防止同步过程中的无限循环
|
||
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 {
|
||
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;
|
||
if (showMacd) {
|
||
macdChart = LightweightCharts.createChart(macdChartContainer, createChartOptions(false, 'macd'));
|
||
}
|
||
|
||
// 创建蜡烛图系列并设置数据
|
||
if (showOriginalKline) {
|
||
const candleSeries = mainChart.addCandlestickSeries({
|
||
upColor: '#dc3545',
|
||
downColor: '#28a745',
|
||
borderVisible: false,
|
||
wickUpColor: '#dc3545',
|
||
wickDownColor: '#28a745',
|
||
});
|
||
candleSeries.setData(candles);
|
||
tvWidget.series.candleSeries = candleSeries;
|
||
} else {
|
||
// 如果不显示原始K线,只显示线图
|
||
const lineSeries = mainChart.addLineSeries({
|
||
color: '#2962FF',
|
||
lineWidth: 2,
|
||
crosshairMarkerVisible: true,
|
||
lastValueVisible: true,
|
||
priceLineVisible: true,
|
||
});
|
||
|
||
// 提取收盘价数据
|
||
const lineData = candles.map(candle => ({
|
||
time: candle.time,
|
||
value: candle.close
|
||
}));
|
||
|
||
lineSeries.setData(lineData);
|
||
tvWidget.series.lineSeries = lineSeries;
|
||
}
|
||
|
||
// 转换成交量数据 - 始终使用主K线周期数据
|
||
let volumes = [];
|
||
// 使用与K线和MACD相同的数据源选择逻辑
|
||
const volumeDataSource = useElementPeriod ? currentData.element_kline_data : currentData.kline_data;
|
||
|
||
console.log('成交量数据源选择:', useElementPeriod ? '次周期' : '主周期');
|
||
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图表 - 始终使用主K线周期的MACD数据
|
||
if (showMacd && currentData.macd && currentData.kline_data && Array.isArray(currentData.kline_data)) {
|
||
// 创建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 = [];
|
||
|
||
// 使用与K线数据相同的数据源来确保时间对齐
|
||
const klineDataSource = useElementPeriod ? currentData.element_kline_data : currentData.kline_data;
|
||
const macdDataSource = useElementPeriod ?
|
||
(currentData.element_macd || currentData.macd) : // 如果有次周期MACD数据则使用,否则使用主周期
|
||
currentData.macd; // 主周期使用主周期MACD数据
|
||
|
||
console.log('MACD数据源选择:', useElementPeriod ? '次周期' : '主周期');
|
||
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;
|
||
}
|
||
|
||
// 实现三图联动滚动
|
||
|
||
// 同步图表的时间范围
|
||
function syncCharts(sourceChart, sourceContainer) {
|
||
// 防止无限循环 - 使用更精确的检查
|
||
if (syncInProgress) {
|
||
console.log('🔄 同步正在进行中,跳过此次同步');
|
||
return;
|
||
}
|
||
|
||
syncInProgress = true;
|
||
console.log('🚀 开始同步图表,来源:',
|
||
sourceChart === mainChart ? '主图' :
|
||
sourceChart === volumeChart ? '成交量图' : 'MACD图');
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
// 保存当前的可见范围到全局状态
|
||
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
|
||
};
|
||
|
||
// 全局鼠标抬起事件(只添加一次)
|
||
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 ? '成交量图' : 'MACD图');
|
||
|
||
// 确定当前图表类型
|
||
const chartType = chart === mainChart ? 'main' : chart === volumeChart ? 'volume' : 'macd';
|
||
|
||
// 使用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);
|
||
}
|
||
|
||
// 窗口大小变化时重绘图表
|
||
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
|
||
});
|
||
}
|
||
|
||
// 重新同步 - 使用主图作为同步源
|
||
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();
|
||
}
|
||
|
||
// 保存图表对象
|
||
tvWidget.mainChart = mainChart;
|
||
tvWidget.volumeChart = volumeChart;
|
||
tvWidget.macdChart = macdChart;
|
||
tvWidget.state.isInitialized = true;
|
||
|
||
// 绑定同步事件
|
||
bindSyncEvents(mainChartContainer, volumeChartContainer, macdChartContainer, mainChart, volumeChart, macdChart, showMacd);
|
||
|
||
// 只有在时间输入框都为空时才设置图表默认时间范围
|
||
if (!$('#start_time').val() && !$('#end_time').val()) {
|
||
setDefaultTimeRange();
|
||
}
|
||
|
||
// 添加买卖点提示
|
||
setupTooltip(mainChart, [], [], mainChartContainer, volumeChartContainer, macdChartContainer, volumeChart, macdChart, showMacd);
|
||
|
||
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 = [];
|
||
if (useElementPeriod && currentData.element_kline_data && Array.isArray(currentData.element_kline_data)) {
|
||
volumes = currentData.element_kline_data.map(kline => {
|
||
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
|
||
return {
|
||
time: timestamp,
|
||
value: parseFloat(kline.volume),
|
||
color: parseFloat(kline.close) >= parseFloat(kline.open) ? 'rgba(220, 53, 69, 0.5)' : 'rgba(40, 167, 69, 0.5)',
|
||
};
|
||
});
|
||
} else if (currentData.kline_data && Array.isArray(currentData.kline_data)) {
|
||
volumes = currentData.kline_data.map(kline => {
|
||
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
|
||
return {
|
||
time: timestamp,
|
||
value: parseFloat(kline.volume),
|
||
color: parseFloat(kline.close) >= parseFloat(kline.open) ? 'rgba(220, 53, 69, 0.5)' : 'rgba(40, 167, 69, 0.5)',
|
||
};
|
||
});
|
||
}
|
||
|
||
if (tvWidget.series.volumeSeries) {
|
||
tvWidget.series.volumeSeries.setData(volumes);
|
||
}
|
||
|
||
// 更新MACD数据
|
||
if (true && currentData.macd && currentData.kline_data && Array.isArray(currentData.kline_data) && tvWidget.series.macdLineSeries) {
|
||
// 提取MACD数据
|
||
const macdData = [];
|
||
const signalData = [];
|
||
const histogramData = [];
|
||
|
||
for (let i = 0; i < currentData.kline_data.length; i++) {
|
||
const kline = currentData.kline_data[i];
|
||
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
|
||
|
||
if (currentData.macd && currentData.macd.macd && currentData.macd.macd[i] !== undefined) {
|
||
macdData.push({
|
||
time: timestamp,
|
||
value: currentData.macd.macd[i]
|
||
});
|
||
|
||
signalData.push({
|
||
time: timestamp,
|
||
value: currentData.macd.signal[i]
|
||
});
|
||
|
||
// 设置直方图颜色
|
||
const histValue = currentData.macd.histogram[i];
|
||
histogramData.push({
|
||
time: timestamp,
|
||
value: histValue,
|
||
color: histValue >= 0 ? 'rgba(220, 53, 69, 0.5)' : 'rgba(40, 167, 69, 0.5)'
|
||
});
|
||
}
|
||
}
|
||
|
||
tvWidget.series.macdLineSeries.setData(macdData);
|
||
tvWidget.series.signalLineSeries.setData(signalData);
|
||
tvWidget.series.histogramSeries.setData(histogramData);
|
||
}
|
||
|
||
// 重新显示笔、线段和中枢等图形
|
||
redrawFractalElements();
|
||
|
||
// 恢复之前的可视范围
|
||
if (tvWidget.mainChart) {
|
||
if (tvWidget.state.logicalRange) {
|
||
tvWidget.mainChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange);
|
||
if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange);
|
||
if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange);
|
||
} else if (tvWidget.state.visibleRange) {
|
||
tvWidget.mainChart.timeScale().setVisibleRange(tvWidget.state.visibleRange);
|
||
if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleRange(tvWidget.state.visibleRange);
|
||
if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleRange(tvWidget.state.visibleRange);
|
||
}
|
||
}
|
||
|
||
console.log('增量更新图表完成');
|
||
} catch (e) {
|
||
console.error('增量更新图表错误,回退到完全重绘:', e);
|
||
// 出错时回退到完全重绘
|
||
initTradingView($('#symbol').val(), $('#timeframe').val());
|
||
}
|
||
}
|
||
|
||
function bindSyncEvents(mainChartContainer, volumeChartContainer, macdChartContainer, mainChart, volumeChart, macdChart, showMacd) {
|
||
// 防止同步过程中的无限循环
|
||
let syncInProgress = false;
|
||
|
||
// 用于跟踪所有图表的拖动状态 - 在函数内部定义以确保作用域正确
|
||
let localDragStates = {
|
||
main: false,
|
||
volume: false,
|
||
macd: false
|
||
};
|
||
|
||
// 同步图表的时间范围
|
||
function syncCharts(sourceChart, sourceContainer) {
|
||
// 防止无限循环 - 使用更精确的检查
|
||
if (syncInProgress) {
|
||
console.log('🔄 同步正在进行中,跳过此次同步');
|
||
return;
|
||
}
|
||
|
||
syncInProgress = true;
|
||
console.log('🚀 开始同步图表,来源:',
|
||
sourceChart === mainChart ? '主图' :
|
||
sourceChart === volumeChart ? '成交量图' : 'MACD图');
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
// 保存当前的可见范围到全局状态
|
||
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);
|
||
}
|
||
|
||
// 为每个图表添加事件监听
|
||
const addChartSyncEvents = (chartContainer, chart) => {
|
||
console.log('为图表添加同步事件监听:', chart === mainChart ? '主图' : chart === volumeChart ? '成交量图' : 'MACD图');
|
||
|
||
// 确定当前图表类型
|
||
const chartType = chart === mainChart ? 'main' : chart === volumeChart ? 'volume' : 'macd';
|
||
|
||
// 使用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);
|
||
}
|
||
|
||
// 窗口大小变化时重绘图表
|
||
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
|
||
});
|
||
}
|
||
|
||
// 重新同步 - 使用主图作为同步源
|
||
setTimeout(() => {
|
||
if (mainChart) {
|
||
syncCharts(mainChart, mainChartContainer);
|
||
}
|
||
}, 200);
|
||
});
|
||
}
|
||
|
||
function setupTooltip(mainChart, buyMarkers = [], sellMarkers = [], mainChartContainer, volumeChartContainer, macdChartContainer, volumeChart, macdChart, 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 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);
|
||
}
|
||
}
|
||
}
|
||
} 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());
|
||
} 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.series = {
|
||
candleSeries: null,
|
||
lineSeries: null,
|
||
volumeSeries: null,
|
||
macdLineSeries: null,
|
||
signalLineSeries: null,
|
||
histogramSeries: 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());
|
||
|
||
// 恢复原始可见范围
|
||
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;
|
||
|
||
// 如果图表未初始化,则创建图表,否则更新图表数据
|
||
if (!tvWidget.state.isInitialized) {
|
||
console.log('图表尚未初始化,创建新图表');
|
||
initTradingView($('#symbol').val(), $('#timeframe').val());
|
||
} else {
|
||
console.log('图表已初始化,进行增量更新');
|
||
updateTradingViewData();
|
||
}
|
||
|
||
// 更新表格数据
|
||
updateTables(data);
|
||
}
|
||
|
||
function refreshChartOnly() {
|
||
// 仅使用当前数据刷新图表显示,不从服务器加载新数据
|
||
if (currentData) {
|
||
console.log('仅刷新图表显示,不重新获取数据');
|
||
refreshChart(currentData);
|
||
} else {
|
||
console.log('没有当前数据,无法刷新显示');
|
||
}
|
||
}
|
||
|
||
// 绑定主周期MACD背离显示开关
|
||
$('#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();
|
||
|
||
// 尝试使用当前已有数据
|
||
if (currentData && currentData.kline_data && currentData.kline_data.length > 0) {
|
||
const currentStart = new Date(currentData.kline_data[0].date).getTime();
|
||
const currentEnd = new Date(currentData.kline_data[currentData.kline_data.length - 1].date).getTime();
|
||
|
||
// 检查当前数据是否覆盖所需的时间范围
|
||
if (currentStart <= replayStartTime && currentEnd >= replayEndTime) {
|
||
console.log('使用当前数据进行回放,无需重新请求');
|
||
replayData = currentData;
|
||
initReplay();
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 请求整个时间范围的数据
|
||
$.ajax({
|
||
url: '/api/analyze',
|
||
data: {
|
||
symbol: symbol,
|
||
timeframe: timeframe,
|
||
timezone: timezone,
|
||
element_timeframe: elementTimeframe,
|
||
start_time: replayStartTime,
|
||
end_time: replayEndTime,
|
||
elements_only: false
|
||
},
|
||
success: function(data) {
|
||
// 检查是否有有效数据
|
||
if (!data || !data.kline_data || data.kline_data.length === 0) {
|
||
$('#replayStatus').text('服务器返回的数据无效');
|
||
$('#replayProgress').hide();
|
||
$('#startReplay').show();
|
||
$('#stopReplay').hide();
|
||
console.error('服务器返回的数据无效:', data);
|
||
return;
|
||
}
|
||
|
||
// 保存完整数据
|
||
replayData = data;
|
||
|
||
// 初始化回放
|
||
initReplay();
|
||
},
|
||
error: function(jqXHR, textStatus, errorThrown) {
|
||
$('#replayStatus').text('加载数据失败');
|
||
$('#replayProgress').hide();
|
||
$('#startReplay').show();
|
||
$('#stopReplay').hide();
|
||
alert('加载数据失败: ' + (jqXHR.responseJSON?.error || errorThrown));
|
||
}
|
||
});
|
||
}
|
||
|
||
// 初始化回放
|
||
function initReplay() {
|
||
if (!replayData || !replayData.kline_data || replayData.kline_data.length === 0) {
|
||
$('#replayStatus').text('没有可回放的数据');
|
||
$('#replayProgress').hide();
|
||
$('#startReplay').show();
|
||
$('#stopReplay').hide();
|
||
return;
|
||
}
|
||
|
||
// 设置回放变量
|
||
isReplaying = true;
|
||
currentReplayIndex = 0;
|
||
totalReplaySteps = replayData.kline_data.length;
|
||
|
||
// 更新回放状态
|
||
$('#replayStatus').text(`准备回放 (0/${totalReplaySteps})`);
|
||
$('.progress-bar').css('width', '0%');
|
||
|
||
// 准备初始数据
|
||
prepareInitialReplayData();
|
||
|
||
// 开始回放
|
||
continueDataReplay();
|
||
}
|
||
|
||
// 准备初始回放数据
|
||
function prepareInitialReplayData() {
|
||
// 创建初始数据集,只包含第一根K线
|
||
const initialData = $.extend(true, {}, replayData);
|
||
|
||
// 确保第一根K线数据有效
|
||
if (!replayData.kline_data || replayData.kline_data.length === 0) {
|
||
console.error('无有效K线数据用于回放');
|
||
$('#replayStatus').text('无有效数据用于回放');
|
||
$('#replayProgress').hide();
|
||
return;
|
||
}
|
||
|
||
initialData.kline_data = [replayData.kline_data[0]];
|
||
|
||
// 确保初始K线数据的所有字段都有值
|
||
const firstKline = initialData.kline_data[0];
|
||
if (firstKline) {
|
||
// 确保K线数据的每个字段都是有效的数值
|
||
['open', 'high', 'low', 'close', 'volume'].forEach(field => {
|
||
if (firstKline[field] === null || firstKline[field] === undefined || isNaN(firstKline[field])) {
|
||
console.warn(`K线数据的${field}字段无效,设置为默认值`);
|
||
firstKline[field] = field === 'volume' ? 0 : firstKline.close || 0;
|
||
}
|
||
});
|
||
}
|
||
|
||
// 清空其他数据列表,因为还没有处理
|
||
initialData.bi_list = [];
|
||
initialData.seg_list = [];
|
||
initialData.zs_list = [];
|
||
initialData.uncompleted_zs_list = [];
|
||
initialData.element_bi_list = [];
|
||
initialData.element_seg_list = [];
|
||
initialData.element_zs_list = [];
|
||
initialData.element_uncompleted_zs_list = [];
|
||
initialData.macd_divergence = [];
|
||
initialData.klc_fx_type = [];
|
||
|
||
// 初始化MACD数据
|
||
if (initialData.macd && initialData.macd.length > 0) {
|
||
initialData.macd = [initialData.macd[0]];
|
||
|
||
// 确保MACD数据有效
|
||
const firstMacd = initialData.macd[0];
|
||
if (firstMacd) {
|
||
['dif', 'dea', 'macd'].forEach(field => {
|
||
if (firstMacd[field] === null || firstMacd[field] === undefined || isNaN(firstMacd[field])) {
|
||
console.warn(`MACD数据的${field}字段无效,设置为0`);
|
||
firstMacd[field] = 0;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// 更新图表,固定坐标轴范围
|
||
currentData = initialData;
|
||
|
||
// 清除当前图表
|
||
if (tvWidget.state.isInitialized) {
|
||
// 保存当前的图表可见范围以供后续使用
|
||
try {
|
||
tvWidget.state.visibleRange = tvWidget.mainChart.timeScale().getVisibleRange();
|
||
tvWidget.state.logicalRange = tvWidget.mainChart.timeScale().getVisibleLogicalRange();
|
||
} catch (e) {
|
||
console.error('保存图表范围失败', e);
|
||
}
|
||
}
|
||
|
||
// 刷新图表
|
||
refreshChart(initialData);
|
||
|
||
// 固定y轴范围
|
||
fixYAxisRange();
|
||
}
|
||
|
||
// 固定y轴范围,保持图表不因数据变化而变化
|
||
function fixYAxisRange() {
|
||
if (!tvWidget.mainChart) return;
|
||
|
||
// 获取全部数据的最高价和最低价,为了适当的范围
|
||
let minPrice = Infinity;
|
||
let maxPrice = -Infinity;
|
||
|
||
// 计算整个数据集的价格范围
|
||
replayData.kline_data.forEach(kline => {
|
||
const high = parseFloat(kline.high);
|
||
const low = parseFloat(kline.low);
|
||
|
||
if (high > maxPrice) maxPrice = high;
|
||
if (low < minPrice) minPrice = low;
|
||
});
|
||
|
||
// 添加一些边距(约5%)
|
||
const priceRange = maxPrice - minPrice;
|
||
maxPrice += priceRange * 0.05;
|
||
minPrice -= priceRange * 0.05;
|
||
|
||
// 修复: 使用正确的API设置固定价格范围
|
||
try {
|
||
// 禁用自动缩放
|
||
tvWidget.mainChart.priceScale().applyOptions({
|
||
autoScale: false,
|
||
scaleMargins: {
|
||
top: 0.1,
|
||
bottom: 0.2
|
||
},
|
||
// 直接在options中设置最小和最大值
|
||
minimumValue: minPrice,
|
||
maximumValue: maxPrice
|
||
});
|
||
} catch (e) {
|
||
console.error('设置Y轴范围失败:', e);
|
||
}
|
||
}
|
||
|
||
// 继续回放
|
||
function continueDataReplay() {
|
||
if (!isReplaying || !replayData) return;
|
||
|
||
// 获取回放速度
|
||
const interval = parseFloat($('#replayInterval').val()) * 1000 || 1000;
|
||
|
||
// 切换按钮状态
|
||
$('#startReplay').hide();
|
||
$('#pauseReplay').show();
|
||
$('#stopReplay').show();
|
||
|
||
// 开始回放定时器
|
||
replayTimer = setInterval(replayNextStep, interval);
|
||
}
|
||
|
||
// 回放下一步
|
||
function replayNextStep() {
|
||
if (!isReplaying || currentReplayIndex >= totalReplaySteps) {
|
||
stopDataReplay();
|
||
return;
|
||
}
|
||
|
||
// 更新回放状态
|
||
currentReplayIndex++;
|
||
updateReplayStatus();
|
||
|
||
// 创建截止到当前索引的数据子集
|
||
const subsetData = createDataSubset(currentReplayIndex);
|
||
|
||
// 更新图表
|
||
currentData = subsetData;
|
||
refreshChart(subsetData);
|
||
|
||
// 固定Y轴范围
|
||
fixYAxisRange();
|
||
|
||
// 保持最新数据可见
|
||
keepLatestDataVisible();
|
||
|
||
// 如果到达最后一步,停止回放
|
||
if (currentReplayIndex >= totalReplaySteps) {
|
||
$('#replayStatus').text('回放完成');
|
||
pauseDataReplay();
|
||
}
|
||
}
|
||
|
||
// 保持最新数据可见
|
||
function keepLatestDataVisible() {
|
||
if (!tvWidget.mainChart) return;
|
||
|
||
try {
|
||
// 获取当前数据的最后一个时间点
|
||
if (currentData && currentData.kline_data && currentData.kline_data.length > 0) {
|
||
const lastBar = currentData.kline_data[currentData.kline_data.length - 1];
|
||
const lastTime = Math.floor(new Date(lastBar.date).getTime() / 1000);
|
||
|
||
// 计算时间范围
|
||
const barCount = Math.min(50, currentData.kline_data.length); // 显示最近的50根K线或所有K线
|
||
const firstVisibleIndex = Math.max(0, currentData.kline_data.length - barCount);
|
||
const firstVisibleBar = currentData.kline_data[firstVisibleIndex];
|
||
const firstTime = Math.floor(new Date(firstVisibleBar.date).getTime() / 1000);
|
||
|
||
// 设置时间轴范围
|
||
tvWidget.mainChart.timeScale().setVisibleRange({
|
||
from: firstTime,
|
||
to: lastTime
|
||
});
|
||
}
|
||
} catch (e) {
|
||
console.error('保持最新数据可见失败:', e);
|
||
}
|
||
}
|
||
|
||
// 创建数据子集
|
||
function createDataSubset(endIndex) {
|
||
// 创建数据副本
|
||
const subsetData = $.extend(true, {}, replayData);
|
||
|
||
// 截取K线数据
|
||
subsetData.kline_data = replayData.kline_data.slice(0, endIndex);
|
||
|
||
// 验证K线数据
|
||
subsetData.kline_data.forEach((kline, index) => {
|
||
['open', 'high', 'low', 'close', 'volume'].forEach(field => {
|
||
if (kline[field] === null || kline[field] === undefined || isNaN(kline[field])) {
|
||
console.warn(`K线数据[${index}]的${field}字段无效,使用替代值`);
|
||
// 对于价格字段,尝试使用其他价格字段,或者前一个K线的对应值
|
||
if (field !== 'volume') {
|
||
if (field === 'open' && kline.close !== null && !isNaN(kline.close)) {
|
||
kline[field] = kline.close;
|
||
} else if (index > 0) {
|
||
kline[field] = subsetData.kline_data[index-1][field] || 0;
|
||
} else {
|
||
kline[field] = 0;
|
||
}
|
||
} else {
|
||
kline[field] = 0; // 对于成交量,设为0
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
// 处理其他数据结构(笔、线段、中枢等)
|
||
// 我们需要根据当前K线的时间来过滤其他数据结构
|
||
try {
|
||
const currentEndTime = new Date(subsetData.kline_data[endIndex - 1].date).getTime();
|
||
|
||
// 过滤笔列表
|
||
if (subsetData.bi_list && subsetData.bi_list.length) {
|
||
subsetData.bi_list = filterDataByTime(replayData.bi_list, currentEndTime);
|
||
}
|
||
|
||
// 过滤线段列表
|
||
if (subsetData.seg_list && subsetData.seg_list.length) {
|
||
subsetData.seg_list = filterDataByTime(replayData.seg_list, currentEndTime);
|
||
}
|
||
|
||
// 过滤中枢列表
|
||
if (subsetData.zs_list && subsetData.zs_list.length) {
|
||
subsetData.zs_list = filterDataByTime(replayData.zs_list, currentEndTime);
|
||
}
|
||
|
||
// 过滤未完成中枢列表
|
||
if (subsetData.uncompleted_zs_list && subsetData.uncompleted_zs_list.length) {
|
||
subsetData.uncompleted_zs_list = filterDataByTime(replayData.uncompleted_zs_list, currentEndTime);
|
||
}
|
||
|
||
// 过滤小周期数据
|
||
if (subsetData.element_bi_list && subsetData.element_bi_list.length) {
|
||
subsetData.element_bi_list = filterDataByTime(replayData.element_bi_list, currentEndTime);
|
||
}
|
||
|
||
if (subsetData.element_seg_list && subsetData.element_seg_list.length) {
|
||
subsetData.element_seg_list = filterDataByTime(replayData.element_seg_list, currentEndTime);
|
||
}
|
||
|
||
if (subsetData.element_zs_list && subsetData.element_zs_list.length) {
|
||
subsetData.element_zs_list = filterDataByTime(replayData.element_zs_list, currentEndTime);
|
||
}
|
||
|
||
if (subsetData.element_uncompleted_zs_list && subsetData.element_uncompleted_zs_list.length) {
|
||
subsetData.element_uncompleted_zs_list = filterDataByTime(replayData.element_uncompleted_zs_list, currentEndTime);
|
||
}
|
||
|
||
// 过滤MACD
|
||
if (subsetData.macd && subsetData.macd.length) {
|
||
subsetData.macd = replayData.macd.slice(0, endIndex);
|
||
|
||
// 验证MACD数据
|
||
subsetData.macd.forEach((item, index) => {
|
||
['dif', 'dea', 'macd'].forEach(field => {
|
||
if (item[field] === null || item[field] === undefined || isNaN(item[field])) {
|
||
console.warn(`MACD数据[${index}]的${field}字段无效,设置为0`);
|
||
item[field] = 0;
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// 过滤MACD背离
|
||
if (subsetData.macd_divergence && subsetData.macd_divergence.length) {
|
||
subsetData.macd_divergence = filterDataByTime(replayData.macd_divergence, currentEndTime);
|
||
}
|
||
|
||
// 过滤分型类型
|
||
if (subsetData.klc_fx_type && subsetData.klc_fx_type.length) {
|
||
subsetData.klc_fx_type = filterDataByTime(replayData.klc_fx_type, currentEndTime, 'time');
|
||
}
|
||
} catch (e) {
|
||
console.error('过滤数据时出错:', e);
|
||
}
|
||
|
||
return subsetData;
|
||
}
|
||
|
||
// 根据时间过滤数据
|
||
function filterDataByTime(dataList, endTime, timeField = 'end_time') {
|
||
if (!dataList || !Array.isArray(dataList)) return [];
|
||
|
||
return dataList.filter(item => {
|
||
const itemTime = new Date(item[timeField]).getTime();
|
||
return itemTime <= endTime;
|
||
});
|
||
}
|
||
|
||
// 更新回放状态显示
|
||
function updateReplayStatus() {
|
||
const percent = Math.floor((currentReplayIndex / totalReplaySteps) * 100);
|
||
$('#replayStatus').text(`回放中 (${currentReplayIndex}/${totalReplaySteps})`);
|
||
$('.progress-bar').css('width', `${percent}%`);
|
||
}
|
||
|
||
// 暂停回放
|
||
function pauseDataReplay() {
|
||
if (replayTimer) {
|
||
clearInterval(replayTimer);
|
||
replayTimer = null;
|
||
}
|
||
|
||
// 切换按钮状态
|
||
$('#pauseReplay').hide();
|
||
$('#startReplay').show();
|
||
|
||
if (currentReplayIndex >= totalReplaySteps) {
|
||
$('#replayStatus').text('回放完成');
|
||
} else {
|
||
$('#replayStatus').text(`已暂停 (${currentReplayIndex}/${totalReplaySteps})`);
|
||
}
|
||
}
|
||
|
||
// 停止回放
|
||
function stopDataReplay() {
|
||
// 清除定时器
|
||
if (replayTimer) {
|
||
clearInterval(replayTimer);
|
||
replayTimer = null;
|
||
}
|
||
|
||
// 重置回放状态
|
||
isReplaying = false;
|
||
currentReplayIndex = 0;
|
||
replayData = null;
|
||
|
||
// 更新UI
|
||
$('#replayStatus').text('');
|
||
$('#replayProgress').hide();
|
||
$('#pauseReplay').hide();
|
||
$('#stopReplay').hide();
|
||
$('#startReplay').show();
|
||
|
||
// 恢复图表自动缩放
|
||
if (tvWidget.mainChart) {
|
||
tvWidget.mainChart.priceScale().applyOptions({
|
||
autoScale: true
|
||
});
|
||
}
|
||
}
|
||
|
||
// 获取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);
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |