Files
Chan/web/templates/index.html
T
2025-08-14 02:29:27 +08:00

9475 lines
457 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html>
<head>
<title>缠论分析系统</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css" rel="stylesheet">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.datatables.net/1.11.5/js/jquery.dataTables.min.js"></script>
<link href="https://cdn.datatables.net/1.11.5/css/jquery.dataTables.min.css" rel="stylesheet">
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- TradingView Widget BEGIN -->
<script src="https://cdn.jsdelivr.net/npm/lightweight-charts@4.0.1/dist/lightweight-charts.standalone.production.js"></script>
<!-- TradingView Widget END -->
<style>
body {
font-family: "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
margin: 0;
padding: 10px;
background-color: #f8f9fa;
color: #333;
}
.container {
max-width: 100%;
margin: 0 auto;
background-color: white;
padding: 5px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.header {
margin-bottom: 0px;
padding-bottom: 0px;
}
.controls {
margin-bottom: 10px;
padding: 10px;
background-color: #f8f9fa;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.form-label {
font-weight: 500;
margin-bottom: 6px;
color: #495057;
}
.chart-container {
width: 100%;
height: 900px;
margin-top: 10px;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 10px;
background-color: #fff;
position: relative;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
margin-bottom: 20px; /* 增加下边距,防止被下方数据面板遮挡 */
}
#tradingview_chart {
width: 100%;
height: 100%;
position: relative;
z-index: 1; /* 确保图表在数据面板之上 */
}
.chart-options {
position: absolute;
top: 10px;
right: 10px;
z-index: 100;
background-color: rgba(255, 255, 255, 0.9);
padding: 12px;
border-radius: 8px;
font-size: 13px;
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
}
.data-container {
margin-top: 30px; /* 增加上边距,避免与图表重叠 */
position: relative;
z-index: 10;
background-color: white;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
clear: both; /* 清除浮动 */
}
.nav-tabs {
margin-bottom: 10px;
position: relative;
z-index: 20;
background-color: white;
border-radius: 8px 8px 0 0;
padding: 10px 10px 0 10px;
}
.nav-tabs .nav-link {
border-radius: 6px 6px 0 0;
margin-right: 5px;
font-weight: 500;
transition: all 0.2s ease;
}
.nav-tabs .nav-link:hover {
background-color: #f8f9fa;
border-color: #dee2e6;
}
.nav-tabs .nav-link.active {
background-color: #0d6efd;
color: white;
border-color: #0d6efd;
}
/* 特别突出显示股票筛选tab */
#stock-filter-tab {
background-color: #28a745 !important;
color: white !important;
border-color: #28a745 !important;
font-weight: bold !important;
box-shadow: 0 2px 4px rgba(40, 167, 69, 0.3) !important;
}
#stock-filter-tab:hover {
background-color: #218838 !important;
border-color: #1e7e34 !important;
}
#stock-filter-tab.active {
background-color: #155724 !important;
border-color: #155724 !important;
}
.table-container {
overflow-x: auto;
}
.btn-primary {
background-color: #0d6efd;
border-color: #0d6efd;
color: white;
padding: 8px 16px;
font-weight: 500;
transition: all 0.2s;
}
.btn-primary:hover {
background-color: #0a58ca;
border-color: #0a53be;
}
#loadingIndicator {
display: none;
text-align: center;
padding: 20px;
font-size: 18px;
color: #666;
}
/* 方向列颜色 */
.direction-up {
color: #dc3545;
font-weight: bold;
}
.direction-down {
color: #28a745;
font-weight: bold;
}
/* MACD列颜色 */
.positive {
color: #dc3545;
}
.negative {
color: #28a745;
}
/* 自定义控制面板 */
.form-check-label {
cursor: pointer;
user-select: none;
}
/* 增加表单元素间距 */
.g-3 {
--bs-gutter-y: 1rem;
}
.point-tooltip {
position: absolute;
background: rgba(40, 40, 40, 0.9);
color: white;
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
z-index: 1000;
pointer-events: none;
max-width: 300px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
display: none;
}
.buy-point {
color: #ff1744;
font-weight: bold;
}
.sell-point {
color: #00e676;
font-weight: bold;
}
.buy-marker {
background-color: #ff1744;
border: 2px solid white;
}
.sell-marker {
background-color: #00e676;
border: 2px solid white;
}
/* 主次周期元素样式区分 */
.main-bi {
color: #dc3545;
font-weight: bold;
}
.element-bi {
color: #9c27b0;
font-weight: bold;
}
.main-seg {
color: #FF6B6B;
font-weight: bold;
}
.element-seg {
color: #673ab7;
font-weight: bold;
}
.main-zs {
color: #F1C40F;
font-weight: bold;
}
.element-zs {
color: #3f51b5;
font-weight: bold;
}
/* 加载指示器旋转动画 */
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-spinner {
display: inline-block;
width: 18px;
height: 18px;
border: 2px solid rgba(0, 0, 0, 0.1);
border-left-color: #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-left: 8px;
vertical-align: middle;
}
/* 数据回放相关样式 */
#replayStatus {
min-width: 120px;
}
#replayProgress {
border-radius: 4px;
overflow: hidden;
}
.crosshair-tooltip {
position: absolute;
background: rgba(40, 40, 40, 0.9);
color: white;
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
z-index: 1000;
pointer-events: none;
max-width: 300px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
display: none;
}
/* 主次周期元素样式区分 */
/* 均线系统样式 */
.ma-panel {
position: absolute;
top: 10px;
left: 10px;
z-index: 1000;
background: rgba(255, 255, 255, 0.95);
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 8px;
max-width: 300px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.ma-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 0;
font-size: 12px;
border-bottom: 1px solid #f0f0f0;
}
.ma-item:last-child {
border-bottom: none;
}
.ma-info {
display: flex;
align-items: center;
gap: 8px;
}
.ma-label {
font-weight: 500;
}
.ma-value {
color: #666;
}
.ma-actions {
display: flex;
gap: 4px;
}
.ma-action-btn {
background: none;
border: none;
cursor: pointer;
padding: 2px 4px;
border-radius: 3px;
font-size: 11px;
color: #666;
}
.ma-action-btn:hover {
background-color: #f0f0f0;
color: #333;
}
.ma-color-indicator {
width: 12px;
height: 2px;
border-radius: 1px;
margin-right: 4px;
}
.indicator-dropdown {
position: absolute;
top: 10px;
right: 120px;
z-index: 100;
}
.add-indicator-btn {
background-color: #0d6efd;
color: white;
border: none;
padding: 8px 12px;
border-radius: 6px;
font-size: 13px;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
display: flex;
align-items: center;
gap: 5px;
}
.add-indicator-btn:hover {
background-color: #0a58ca;
}
.add-indicator-btn:focus {
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
}
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
display: none;
min-width: 160px;
padding: 0.5rem 0;
margin: 0.125rem 0 0;
font-size: 0.875rem;
color: #212529;
text-align: left;
background-color: #fff;
background-clip: padding-box;
border: 1px solid rgba(0,0,0,.15);
border-radius: 0.375rem;
box-shadow: 0 0.5rem 1rem rgba(0,0,0,.15);
}
.dropdown-menu.show {
display: block;
}
.dropdown-item {
display: block;
width: 100%;
padding: 0.375rem 1rem;
clear: both;
font-weight: 400;
color: #212529;
text-align: inherit;
text-decoration: none;
white-space: nowrap;
background-color: transparent;
border: 0;
cursor: pointer;
}
.dropdown-item:hover,
.dropdown-item:focus {
background-color: #f8f9fa;
color: #16181b;
}
.dropdown-item i {
margin-right: 8px;
}
.indicator-panel {
position: absolute;
top: 10px;
left: 10px;
z-index: 1000;
background: rgba(255, 255, 255, 0.95);
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 8px;
max-width: 350px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.indicator-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 0;
font-size: 12px;
border-bottom: 1px solid #f0f0f0;
}
.indicator-item:last-child {
border-bottom: none;
}
.indicator-info {
display: flex;
align-items: center;
gap: 8px;
}
.indicator-label {
font-weight: 500;
}
.indicator-value {
color: #666;
}
.indicator-actions {
display: flex;
gap: 4px;
}
.indicator-action-btn {
background: none;
border: none;
cursor: pointer;
padding: 2px 4px;
border-radius: 3px;
font-size: 11px;
color: #666;
}
.indicator-action-btn:hover {
background-color: #f0f0f0;
color: #333;
}
.indicator-color-indicator {
width: 12px;
height: 2px;
border-radius: 1px;
margin-right: 4px;
}
/* 配置弹窗样式 */
.ma-config-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 10000;
display: none;
justify-content: center;
align-items: center;
}
.ma-config-content {
background: white;
border-radius: 8px;
padding: 20px;
width: 400px;
max-width: 90vw;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
}
.ma-config-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 20px;
color: #333;
}
.ma-config-form {
display: grid;
gap: 16px;
}
.ma-config-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.ma-config-label {
font-size: 14px;
font-weight: 500;
color: #555;
}
.ma-config-input, .ma-config-select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.ma-config-input:focus, .ma-config-select:focus {
outline: none;
border-color: #0d6efd;
box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
}
.ma-config-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 20px;
}
.ma-config-btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
}
.ma-config-btn-primary {
background-color: #0d6efd;
color: white;
}
.ma-config-btn-primary:hover {
background-color: #0a58ca;
}
.ma-config-btn-secondary {
background-color: #6c757d;
color: white;
}
.ma-config-btn-secondary:hover {
background-color: #5c636a;
}
.ma-line-preview {
width: 100%;
height: 30px;
border: 1px solid #ddd;
border-radius: 4px;
margin-top: 8px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f8f9fa;
}
.ma-line-preview svg {
width: 80%;
height: 20px;
}
/* 布林带面板样式 */
.bb-panel {
position: absolute;
bottom: 20px;
left: 20px;
background: rgba(255, 255, 255, 0.95);
border: 1px solid #ddd;
border-radius: 8px;
padding: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
min-width: 200px;
max-width: 300px;
z-index: 50;
}
.bb-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px solid #f0f0f0;
font-size: 12px;
}
.bb-item:last-child {
border-bottom: none;
}
.bb-info {
flex: 1;
margin-right: 10px;
}
.bb-label {
font-weight: 600;
color: #333;
}
.bb-value {
color: #666;
font-size: 11px;
}
.bb-actions {
display: flex;
gap: 6px;
}
.bb-action-btn {
width: 20px;
height: 20px;
border: none;
background: #f8f9fa;
border-radius: 3px;
cursor: pointer;
font-size: 10px;
color: #666;
}
.bb-action-btn:hover {
background: #e9ecef;
color: #333;
}
.bb-color-indicator {
width: 12px;
height: 12px;
border-radius: 2px;
margin-right: 4px;
display: inline-block;
}
.bb-config-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: none;
justify-content: center;
align-items: center;
z-index: 1000;
}
.bb-config-content {
background: white;
border-radius: 8px;
padding: 16px;
width: 400px;
max-width: 90vw;
max-height: 85vh;
overflow-y: auto;
}
.bb-config-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 20px;
color: #333;
}
.bb-config-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.bb-config-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.bb-config-label {
font-weight: 500;
color: #555;
font-size: 14px;
}
.bb-config-input, .bb-config-select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.2s;
}
.bb-config-input:focus, .bb-config-select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.bb-config-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 20px;
}
.bb-config-btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.bb-config-btn-primary {
background: #667eea;
color: white;
}
.bb-config-btn-primary:hover {
background: #5a67d8;
}
.bb-config-btn-secondary {
background: #f8f9fa;
color: #6c757d;
}
.bb-config-btn-secondary:hover {
background: #e9ecef;
}
.bb-line-preview {
height: 30px;
border: 1px solid #ddd;
border-radius: 4px;
margin-top: 8px;
background: linear-gradient(to right, #f8f9fa, #fff);
position: relative;
}
.bb-line-preview svg {
width: 100%;
height: 100%;
}
</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="showKlcFxType" checked>
<label class="form-check-label" for="showKlcFxType">KLC分型</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showKluFxType">
<label class="form-check-label" for="showKluFxType">KLU分型</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showMainBollinger">
<label class="form-check-label" for="showMainBollinger">布林带</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showMacd" checked>
<label class="form-check-label" for="showMacd">MACD</label>
</div>
</div>
<div class="d-flex align-items-center mt-2">
<label class="form-label me-3 mb-0">次周期:</label>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="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="showElementKlcFxType">
<label class="form-check-label" for="showElementKlcFxType">KLC分型</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showElementKluFxType">
<label class="form-check-label" for="showElementKluFxType">KLU分型</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 class="indicator-dropdown dropdown">
<button class="add-indicator-btn dropdown-toggle" type="button" id="indicatorDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-plus-lg"></i> 添加指标
</button>
<ul class="dropdown-menu" aria-labelledby="indicatorDropdown">
<li><a class="dropdown-item" href="#" onclick="showMAConfig(); return false;">
<i class="bi bi-graph-up"></i> 添加均线
</a></li>
<li><a class="dropdown-item" href="#" onclick="showBBConfig(); return false;">
<i class="bi bi-arrows-expand"></i> 添加布林带
</a></li>
</ul>
</div>
<!-- 技术指标信息面板 -->
<div id="indicatorPanel" class="indicator-panel" style="display: none;"></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="trend-filter-tab" data-bs-toggle="tab" data-bs-target="#trend-filter" 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>
<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="trend-filter" role="tabpanel">
<div class="container-fluid">
<div class="row g-3 mb-2">
<div class="col-md-2">
<label class="form-label">K线周期</label>
<select id="trendTimeframe" class="form-select">
<option value="1m">1m</option>
<option value="5m">5m</option>
<option value="15m" selected>15m</option>
<option value="30m">30m</option>
<option value="1h">1h</option>
<option value="4h">4h</option>
<option value="1d">1d</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">方向</label>
<select id="trendDirection" class="form-select">
<option value="">全部</option>
<option value="bull">多头</option>
<option value="bear">空头</option>
<option value="sideways">盘整</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">阶段</label>
<select id="trendStage" class="form-select">
<option value="">全部</option>
<option value="early">初期</option>
<option value="mid">中期</option>
<option value="late">末期</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">强度 (0-100)</label>
<div class="d-flex align-items-center">
<input type="range" id="trendMinStrength" class="form-range me-2" min="0" max="100" value="30">
<input type="number" id="trendMinStrengthNum" class="form-control" style="width:90px" min="0" max="100" value="30">
</div>
</div>
<div class="col-md-3">
<label class="form-label">自选币对(逗号分隔)</label>
<input id="trendSymbols" class="form-control" placeholder="例如: BTC/USDT:USDT,ETH/USDT:USDT,可留空">
</div>
<div class="col-md-3">
<label class="form-label">开始时间</label>
<input type="datetime-local" id="trendStart" class="form-control">
</div>
<div class="col-md-3">
<label class="form-label">结束时间</label>
<input type="datetime-local" id="trendEnd" class="form-control">
</div>
<div class="col-md-2 d-flex align-items-end">
<button class="btn btn-primary w-100" id="btnTrendFilter">开始筛选</button>
</div>
</div>
<div class="table-container mb-3">
<div id="trendFilterStatus" class="alert alert-info py-2" style="display:none;">
<span class="loading-spinner"></span>
<span class="ms-2">正在筛选,请稍候...</span>
</div>
<table id="trendFilterTable" class="display compact" style="width:100%">
<thead>
<tr>
<th>交易对</th>
<th>时间</th>
<th>方向</th>
<th>阶段</th>
<th>强度</th>
<th>收盘</th>
<th>EMA5</th>
<th>EMA10</th>
<th>EMA24</th>
<th>EMA52</th>
<th>操作</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
趋势详情
<span id="trendDetailStatus" class="text-muted ms-3" style="display:none;">
<span class="loading-spinner"></span>
<span class="ms-2">正在加载详情...</span>
</span>
</div>
<div class="card-body">
<div id="trendChartContainer" style="height:420px;"></div>
<div class="table-container mt-3">
<table id="trendDetailTable" class="display compact" style="width:100%">
<thead>
<tr>
<th>时间</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th>成交量</th>
<th>EMA5</th>
<th>EMA10</th>
<th>EMA24</th>
<th>EMA52</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</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 trendTable = null;
let trendDetailTable = null;
let trendChart = null;
function initTrendTables() {
if (!trendTable) {
trendTable = $('#trendFilterTable').DataTable({
paging: true,
searching: false,
info: true,
order: [[4, 'desc']],
});
}
if (!trendDetailTable) {
trendDetailTable = $('#trendDetailTable').DataTable({
paging: true,
searching: false,
info: true,
order: [[0, 'desc']],
});
}
}
function bindTrendControls() {
// 双向绑定强度滑块与数字框
$('#trendMinStrength').on('input change', function(){
$('#trendMinStrengthNum').val($(this).val());
});
$('#trendMinStrengthNum').on('input change', function(){
let v = Math.max(0, Math.min(100, parseFloat($(this).val()||0)));
$(this).val(v);
$('#trendMinStrength').val(v);
});
// 周期变化时,自动填充当前时间回溯300根K线的时间范围
$('#trendTimeframe').on('change', function(){
const tf = $(this).val();
const tfToMs = {
'1m': 60*1000, '5m': 5*60*1000, '15m': 15*60*1000, '30m': 30*60*1000,
'1h': 60*60*1000, '4h': 4*60*60*1000, '1d': 24*60*60*1000
};
const step = tfToMs[tf] || (60*60*1000);
const now = new Date();
const endMs = now.getTime();
const startMs = endMs - 300 * step;
const toLocal = (ms) => new Date(ms - new Date(ms).getTimezoneOffset()*60000).toISOString().slice(0,16);
$('#trendEnd').val(toLocal(endMs));
$('#trendStart').val(toLocal(startMs));
});
$('#btnTrendFilter').on('click', async function(){
await runTrendFilter();
});
}
async function runTrendFilter() {
initTrendTables();
trendTable.clear().draw();
const timeframe = $('#trendTimeframe').val();
const direction = $('#trendDirection').val();
const stage = $('#trendStage').val();
const minStrength = $('#trendMinStrength').val();
const symbols = $('#trendSymbols').val();
let start = $('#trendStart').val();
let end = $('#trendEnd').val();
// 前端必须提供时间范围:若为空,自动以当前时间回溯300根
if (!start || !end) {
const tfToMs = {
'1m': 60*1000, '5m': 5*60*1000, '15m': 15*60*1000, '30m': 30*60*1000,
'1h': 60*60*1000, '4h': 4*60*60*1000, '1d': 24*60*60*1000
};
const step = tfToMs[timeframe] || (60*60*1000);
const now = Date.now();
const startMsAuto = now - 300 * step;
const toLocal = (ms) => new Date(ms - new Date(ms).getTimezoneOffset()*60000).toISOString().slice(0,16);
if (!end) $('#trendEnd').val(toLocal(now));
if (!start) $('#trendStart').val(toLocal(startMsAuto));
start = $('#trendStart').val();
end = $('#trendEnd').val();
}
let startMs = start ? new Date(start).getTime() : '';
let endMs = end ? new Date(end).getTime() : '';
const params = $.param({
timeframe: timeframe,
direction: direction || '',
stage: stage || '',
min_strength: minStrength,
symbols: symbols || '',
start_time: startMs || '',
end_time: endMs || ''
});
// 显示筛选状态
$('#trendFilterStatus').show();
try {
const res = await $.getJSON(`/api/trend_filter?${params}`);
// 初筛后端结果,再次用前端方向筛选(避免后端噪声)
const dirVal = $('#trendDirection').val();
const rows = (res.results || [])
.filter(r => {
if (!dirVal) return true;
return r.direction === dirVal;
})
.map(r => [
r.symbol,
new Date(r.time).toLocaleString('zh-CN', { timeZone: $('#timezone').val() || 'Asia/Shanghai' }),
r.direction === 'bull' ? '多头' : (r.direction === 'bear' ? '空头' : '盘整'),
r.stage === 'early' ? '初期' : (r.stage === 'mid' ? '中期' : '末期'),
r.strength,
r.close,
r.ema5,
r.ema10,
r.ema24,
r.ema52,
`<button class="btn btn-sm btn-outline-primary" data-symbol="${r.symbol}" data-timeframe="${timeframe}">查看</button>`
]);
trendTable.rows.add(rows).draw();
// 绑定查看按钮
$('#trendFilterTable').off('click', 'button').on('click', 'button', function(){
const sym = $(this).data('symbol');
const tf = $(this).data('timeframe');
loadTrendDetail(sym, tf, startMs, endMs);
});
// 精细化阶段判定(前端基于明细重算)
refineTrendStages(Array.from(new Set((res.results||[]).map(r => r.symbol))).slice(0, 20), timeframe, startMs, endMs);
} catch (e) {
alert('趋势筛选失败: ' + e);
} finally {
$('#trendFilterStatus').hide();
}
}
async function loadTrendDetail(symbol, timeframe, startMs, endMs) {
const params = $.param({
symbol: symbol,
timeframe: timeframe,
start_time: startMs || '',
end_time: endMs || '',
timezone: $('#timezone').val() || 'Asia/Shanghai'
});
// 显示详情加载状态
$('#trendDetailStatus').show();
try {
const data = await $.getJSON(`/api/trend_detail?${params}`);
// 填表
trendDetailTable.clear();
(data.kline_data || []).forEach(row => {
trendDetailTable.row.add([
new Date(row.timestamp).toLocaleString('zh-CN', { timeZone: data.timezone }),
row.open, row.high, row.low, row.close, row.volume,
row.ema5, row.ema10, row.ema24, row.ema52
]);
});
trendDetailTable.draw();
// 画图
drawTrendChart(data);
} catch (e) {
alert('加载趋势详情失败: ' + e);
} finally {
$('#trendDetailStatus').hide();
}
}
function drawTrendChart(data) {
const container = document.getElementById('trendChartContainer');
if (!container) return;
container.innerHTML = '';
const chart = LightweightCharts.createChart(container, {
layout: { background: { color: '#ffffff' }, textColor: '#333' },
rightPriceScale: { visible: true },
timeScale: { timeVisible: true, secondsVisible: false },
crosshair: { mode: LightweightCharts.CrosshairMode.Normal },
grid: { vertLines: { color: '#eee' }, horzLines: { color: '#eee' } },
autoSize: true
});
trendChart = chart;
const candle = chart.addCandlestickSeries();
// 关闭均线的价格线与最后值标签,仅保留K线的当前价格虚线
const ema5 = chart.addLineSeries({ color: '#ff0000', lineWidth: 2, lastValueVisible: false, priceLineVisible: false });
const ema10 = chart.addLineSeries({ color: '#2962FF', lineWidth: 2, lastValueVisible: false, priceLineVisible: false });
const ema24 = chart.addLineSeries({ color: '#008000', lineWidth: 2, lastValueVisible: false, priceLineVisible: false });
const ema52 = chart.addLineSeries({ color: '#800080', lineWidth: 2, lastValueVisible: false, priceLineVisible: false });
const k = (data.kline_data || []).map(r => ({
time: Math.floor(r.timestamp / 1000),
open: Number(r.open), high: Number(r.high), low: Number(r.low), close: Number(r.close)
}));
candle.setData(k);
// 前端过滤均线前导缺失/无效值,避免绘制为0
const sanitizeMA = (field) => {
const rows = data.kline_data || [];
const out = [];
let started = false;
for (let i = 0; i < rows.length; i++) {
const r = rows[i];
const raw = r[field];
const v = Number(raw);
const valid = Number.isFinite(v) && v > 0;
if (!started) {
if (!valid) continue;
started = true;
}
if (!valid) continue;
out.push({ time: Math.floor(r.timestamp / 1000), value: v });
}
return out;
};
ema5.setData(sanitizeMA('ema5'));
ema10.setData(sanitizeMA('ema10'));
ema24.setData(sanitizeMA('ema24'));
ema52.setData(sanitizeMA('ema52'));
// 趋势线(使用返回的拟合参数)
const trend = data.trend_line || null;
if (trend && k.length > 1) {
const L = Math.min(trend.length, k.length);
const startIdx = k.length - L;
const lineData = [];
for (let i = 0; i < L; i++) {
const y = trend.slope * i + trend.intercept;
const point = { time: k[startIdx + i].time, value: y };
lineData.push(point);
}
const trendSeries = chart.addLineSeries({ color: '#ffa500', lineWidth: 2, lineStyle: 0, lastValueVisible: false, priceLineVisible: false });
trendSeries.setData(lineData);
}
}
// ===== 前端精细化阶段判定 =====
function computeEMA(arr, period) {
const k = 2 / (period + 1);
const out = [];
let emaPrev = null;
for (let i = 0; i < arr.length; i++) {
const price = arr[i];
if (price == null || !isFinite(price)) { out.push(null); continue; }
if (emaPrev == null) {
// 取首个可用SMA种子
const start = Math.max(0, i - period + 1);
const window = arr.slice(start, i + 1).filter(v => v != null && isFinite(v));
const sma = window.length ? window.reduce((a,b)=>a+b,0)/window.length : price;
emaPrev = sma;
}
const ema = price * k + emaPrev * (1 - k);
out.push(ema);
emaPrev = ema;
}
return out;
}
function computeMACDSeries(close) {
const ema12 = computeEMA(close, 12);
const ema26 = computeEMA(close, 26);
const macd = close.map((_, i) => (ema12[i] != null && ema26[i] != null) ? (ema12[i] - ema26[i]) : null);
const signal = computeEMA(macd.map(v => v ?? null), 9);
const hist = macd.map((v, i) => (v != null && signal[i] != null) ? (v - signal[i]) : null);
return { macd, signal, hist };
}
function slope(series, win) {
const n = series.length;
const k = Math.min(win, n);
if (k < 3) return 0;
const y = series.slice(n - k).filter(v => v != null && isFinite(v));
if (y.length < 3) return 0;
const x = [...Array(y.length).keys()];
const xm = x.reduce((a,b)=>a+b,0)/x.length;
const ym = y.reduce((a,b)=>a+b,0)/y.length;
let num = 0, den = 0;
for (let i=0;i<x.length;i++){ num += (x[i]-xm)*(y[i]-ym); den += (x[i]-xm)*(x[i]-xm); }
return den ? num/den : 0;
}
function classifyStageFrontend(kline, directionHint) {
const close = kline.map(r => Number(r.close));
const ema24 = computeEMA(close, 24);
const ema52 = computeEMA(close, 52);
const last = close[close.length-1];
const e24 = ema24[ema24.length-1];
const e52 = ema52[ema52.length-1];
const s24 = slope(ema24, 20);
const s52 = slope(ema52, 30);
const dist52 = (e52 && isFinite(e52)) ? (last - e52)/e52 : 0;
const { hist } = computeMACDSeries(close);
const recent = hist.slice(-9).filter(v => v != null);
const earlier = hist.slice(-18, -9).filter(v => v != null);
const growth = (recent.length && earlier.length) ? (avgAbs(recent) - avgAbs(earlier)) : 0;
function avgAbs(arr){ return arr.reduce((a,b)=>a+Math.abs(b),0)/arr.length; }
let direction = directionHint;
if (!direction) {
if (e24 > e52 && s24 > 0 && s52 > 0) direction = 'bull';
else if (e24 < e52 && s24 < 0 && s52 < 0) direction = 'bear';
else direction = 'sideways';
}
let stage = 'early';
const ad = Math.abs(dist52);
if (direction === 'bull') {
if (ad < 0.03 && growth > 0) stage = 'early';
else if (ad < 0.10 && (growth >= 0 || s24 > 0)) stage = 'mid';
else stage = 'late';
} else if (direction === 'bear') {
if (ad < 0.03 && growth > 0) stage = 'early';
else if (ad < 0.10 && (growth >= 0 || s24 < 0)) stage = 'mid';
else stage = 'late';
} else {
stage = 'early';
}
return { direction, stage };
}
async function refineTrendStages(symbols, timeframe, startMs, endMs) {
if (!symbols || symbols.length === 0) return;
// 在表头上方提示
const info = $('<div class="text-muted mb-2" id="refineInfo">正在优化阶段判定...</div>');
$('#trendFilterTable').before(info);
const tz = $('#timezone').val() || 'Asia/Shanghai';
const selectedDir = $('#trendDirection').val(); // bull/bear/sideways/''
for (const sym of symbols) {
try {
const params = $.param({ symbol: sym, timeframe, start_time: startMs, end_time: endMs, timezone: tz });
const data = await $.getJSON(`/api/trend_detail?${params}`);
const { direction, stage } = classifyStageFrontend(data.kline_data || [], null);
// 若与选择的方向不一致,则在前端移除该行,避免“选择多头仍出现空头/盘整”
if (selectedDir && direction !== selectedDir) {
if (trendTable) {
trendTable.rows().every(function(){
const rowData = this.data();
if (rowData && rowData[0] === sym) {
this.remove();
}
});
trendTable.draw(false);
}
continue;
}
// 否则更新该行方向与阶段展示
$('#trendFilterTable tbody tr').each(function(){
const tds = $(this).find('td');
if (tds.eq(0).text() === sym) {
tds.eq(2).text(direction === 'bull' ? '多头' : (direction === 'bear' ? '空头' : '盘整'));
tds.eq(3).text(stage === 'early' ? '初期' : stage === 'mid' ? '中期' : '末期');
}
});
} catch(e) {
// 忽略单个失败
}
}
info.remove();
}
// 页面初始化时绑定控件
$(function(){
initTrendTables();
bindTrendControls();
});
let tvWidget = {
mainChart: null,
volumeChart: null,
macdChart: null,
series: {
candleSeries: null,
lineSeries: null,
volumeSeries: null,
atrLineSeries: null,
macdLineSeries: null,
signalLineSeries: null,
histogramSeries: null,
mainBiSeries: [],
mainSegSeries: [],
mainZsSeries: [],
mainUncompletedZsSeries: [],
elementBiSeries: [],
elementSegSeries: [],
elementZsSeries: [],
elementUncompletedZsSeries: [],
tradePointSeries: [],
mainBollingerSeries: [],
elementBollingerSeries: [],
maSeries: [], // 添加均线系列
bbSeries: [] // 添加布林带系列
},
state: {
isInitialized: false,
visibleRange: null,
logicalRange: null
}
};
// 买卖点类型定义
const TRADE_POINT_TYPE = {
BUY1: 1, // 一类买点
BUY2: 2, // 二类买点
BUY3: 3, // 三类买点
SELL1: -1, // 一类卖点
SELL2: -2, // 二类卖点
SELL3: -3 // 三类卖点
};
// 买卖点样式定义
const TRADE_POINT_STYLE = {
[TRADE_POINT_TYPE.BUY1]: {color: '#FF1744', shape: 'arrowUp', text: '买1', size: 2},
[TRADE_POINT_TYPE.BUY2]: {color: '#F50057', shape: 'circle', text: '买2', size: 2},
[TRADE_POINT_TYPE.BUY3]: {color: '#D500F9', shape: 'square', text: '买3', size: 2},
[TRADE_POINT_TYPE.SELL1]: {color: '#00E676', shape: 'arrowDown', text: '卖1', size: 2},
[TRADE_POINT_TYPE.SELL2]: {color: '#00B0FF', shape: 'circle', text: '卖2', size: 2},
[TRADE_POINT_TYPE.SELL3]: {color: '#FFEA00', shape: 'square', text: '卖3', size: 2}
};
// 定义标记垂直偏移系数 - 合约市场通常波动较大,减小偏移防止显示在范围外
const TRADE_POINT_OFFSET = {
[TRADE_POINT_TYPE.BUY1]: 0, // 一类买点向下偏移2.0%的价格
[TRADE_POINT_TYPE.BUY2]: 0, // 二类买点向下偏移1.5%的价格
[TRADE_POINT_TYPE.BUY3]: 0, // 三类买点向下偏移1.0%的价格
[TRADE_POINT_TYPE.SELL1]: 0, // 一类卖点向上偏移2.0%的价格
[TRADE_POINT_TYPE.SELL2]: 0,// 二类卖点向上偏移1.5%的价格
[TRADE_POINT_TYPE.SELL3]: 0 // 三类卖点向上偏移1.0%的价格
};
// 更改为基于价格百分比的垂直偏移 - 合约市场适用的更小偏移
const PRICE_PERCENT_OFFSET = {
[TRADE_POINT_TYPE.BUY1]: 0, // 一类买点向下偏移价格的0.2%
[TRADE_POINT_TYPE.BUY2]: 0, // 二类买点向下偏移价格的0.15%
[TRADE_POINT_TYPE.BUY3]: 0, // 三类买点向下偏移价格的0.1%
[TRADE_POINT_TYPE.SELL1]: 0, // 一类卖点向上偏移价格的0.2%
[TRADE_POINT_TYPE.SELL2]: 0, // 二类卖点向上偏移价格的0.15%
[TRADE_POINT_TYPE.SELL3]: 0 // 三类卖点向上偏移价格的0.1%
};
// 对于高价格标的如BTC,设置零偏移,完全不影响价格显示
const USE_FIXED_OFFSET = true; // 是否使用固定偏移而非百分比
const PRICE_FIXED_OFFSET = {
[TRADE_POINT_TYPE.BUY1]: 0, // 一类买点零偏移
[TRADE_POINT_TYPE.BUY2]: 0, // 二类买点零偏移
[TRADE_POINT_TYPE.BUY3]: 0, // 三类买点零偏移
[TRADE_POINT_TYPE.SELL1]: 0, // 一类卖点零偏移
[TRADE_POINT_TYPE.SELL2]: 0, // 二类卖点零偏移
[TRADE_POINT_TYPE.SELL3]: 0 // 三类卖点零偏移
};
// 同一时间点的标记堆叠间距系数
const STACK_OFFSET_FACTOR = 5; // 增加堆叠标记的间距
// 添加CSS样式定义买卖点标记的样式
const styleElement = document.createElement('style');
styleElement.textContent = `
.point-tooltip {
position: absolute;
background: rgba(40, 40, 40, 0.9);
color: white;
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
z-index: 1000;
pointer-events: none;
max-width: 300px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
display: none;
}
.buy-point {
color: #ff1744;
font-weight: bold;
}
.sell-point {
color: #00e676;
font-weight: bold;
}
.buy-marker {
background-color: #ff1744;
border: 2px solid white;
}
.sell-marker {
background-color: #00e676;
border: 2px solid white;
}
`;
document.head.appendChild(styleElement);
// 添加买卖点悬浮提示元素
const tooltipElement = document.createElement('div');
tooltipElement.className = 'point-tooltip';
// document.body.appendChild(tooltipElement);
// 添加自定义十字线信息显示
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);
});
// 添加分型类型复选框变更事件
$('#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.atrChart) {
tvWidget.atrChart.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.atrChart) {
tvWidget.atrChart.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: [],
maSeries: [], // 添加均线系列数组
bbSeries: [] // 添加布林带系列数组
},
state: {
isInitialized: false,
visibleRange: null,
logicalRange: null
}
};
// 设置父容器样式
const container = document.getElementById('tradingview_chart');
container.style.position = 'relative';
container.style.width = '100%';
container.style.height = '100%';
// 是否显示MACD
const showMacd = $('#showMacd').is(':checked');
const showOriginalKline = $('#showOriginalKline').is(':checked');
// 创建主图容器
const mainChartContainer = document.createElement('div');
mainChartContainer.style.width = '100%';
mainChartContainer.style.position = 'absolute';
mainChartContainer.style.top = '0';
mainChartContainer.style.left = '0';
mainChartContainer.style.right = '0';
// 创建成交量副图容器
const volumeChartContainer = document.createElement('div');
volumeChartContainer.style.width = '100%';
volumeChartContainer.style.position = 'absolute';
volumeChartContainer.style.left = '0';
volumeChartContainer.style.right = '0';
volumeChartContainer.style.borderTop = '1px solid #e0e0e0';
// 添加ATR图表容器
const atrChartContainer = document.createElement('div');
atrChartContainer.style.width = '100%';
atrChartContainer.style.position = 'absolute';
atrChartContainer.style.left = '0';
atrChartContainer.style.right = '0';
atrChartContainer.style.borderTop = '1px solid #e0e0e0';
// 如果需要显示MACD,创建MACD容器
let macdChartContainer = null;
if (showMacd) {
// 显示MACD时:主图(45%) → MACD(17.5%) → 成交量(20%) → ATR(17.5%)
mainChartContainer.style.height = '45%'; // 主图占45%
// MACD 放到成交量上方
macdChartContainer = document.createElement('div');
macdChartContainer.style.width = '100%';
macdChartContainer.style.height = '17.5%'; // MACD图占17.5%
macdChartContainer.style.position = 'absolute';
macdChartContainer.style.top = '45%'; // 紧贴主图下方
macdChartContainer.style.left = '0';
macdChartContainer.style.right = '0';
macdChartContainer.style.borderTop = '1px solid #e0e0e0';
// 成交量位于 MACD 之下
volumeChartContainer.style.top = '62.5%';
volumeChartContainer.style.height = '20%'; // 成交量图占20%
// ATR 位于最底部
atrChartContainer.style.top = '82.5%'; // ATR图从82.5%位置开始
atrChartContainer.style.height = '17.5%'; // ATR图占17.5%
} else {
// 不显示MACD时的高度 - 主图、成交量图和ATR图分配
mainChartContainer.style.height = '55%'; // 主图占55%
volumeChartContainer.style.top = '55%';
volumeChartContainer.style.height = '22.5%'; // 成交量图占22.5%
atrChartContainer.style.top = '77.5%'; // ATR图从77.5%位置开始
atrChartContainer.style.height = '22.5%'; // ATR图占22.5%
}
container.appendChild(mainChartContainer);
container.appendChild(volumeChartContainer);
container.appendChild(atrChartContainer);
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 === 'atr') {
chartHeight = atrChartContainer.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,
// 确保所有图表使用相同的边距设置
rightOffset: 12,
// 移除可能影响拖动的固定边缘设置
// 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'));
// 创建ATR图表
const atrChart = LightweightCharts.createChart(atrChartContainer, createChartOptions(false, 'atr'));
// 创建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;
// 添加ATR图表
const atrLineSeries = atrChart.addLineSeries({
color: '#FF9800',
lineWidth: 2,
title: 'ATR',
lastValueVisible: false,
priceLineVisible: false,
});
// 准备ATR数据
const atrData = [];
// 使用与K线数据相同的数据源来确保时间对齐
const atrKlineDataSource = useElementPeriod ? currentData.element_kline_data : currentData.kline_data;
const atrDataSource = useElementPeriod ?
(currentData.element_atr || currentData.atr) : // 如果有次周期ATR数据则使用,否则使用主周期
currentData.atr; // 主周期使用主周期ATR数据
console.log('ATR数据源选择:', useElementPeriod ? '次周期' : '主周期');
console.log('ATR数据长度:', atrDataSource ? atrDataSource.length : 0);
console.log('K线数据长度:', atrKlineDataSource ? atrKlineDataSource.length : 0);
if (atrDataSource && Array.isArray(atrDataSource) && atrKlineDataSource && Array.isArray(atrKlineDataSource)) {
// 关键修复:为每个K线时间点都创建ATR数据点,包括没有ATR值的前期数据
for (let i = 0; i < atrKlineDataSource.length; i++) {
const kline = atrKlineDataSource[i];
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
// 为每个时间点都添加数据以保持时间轴对齐,但ATR为0时不显示
if (atrDataSource[i] !== undefined) {
if (atrDataSource[i] > 0) {
// ATR有效值,正常显示
atrData.push({
time: timestamp,
value: atrDataSource[i]
});
} else {
// ATR为0,添加时间点但不显示线条(使用undefined作为value
atrData.push({
time: timestamp,
value: undefined
});
}
}
}
console.log('处理后的ATR数据点数:', atrData.length);
console.log('ATR数据样本:', atrData.slice(0, 5));
}
console.log('处理后的ATR数据点数:', atrData.length);
atrLineSeries.setData(atrData);
tvWidget.series.atrLineSeries = atrLineSeries;
// 添加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 ? '成交量图' :
sourceChart === atrChart ? 'ATR图' : '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);
}
}
// 同步ATR图
if (sourceChart !== atrChart && atrChart && atrChart.timeScale) {
try {
atrChart.timeScale().setVisibleLogicalRange(logicalRange);
console.log('✅ ATR图同步完成');
} catch (e) {
console.error('❌ ATR图同步失败:', 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,
atr: 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 ? '成交量图' :
chart === atrChart ? 'ATR图' : 'MACD图');
// 确定当前图表类型
const chartType = chart === mainChart ? 'main' :
chart === volumeChart ? 'volume' :
chart === atrChart ? 'atr' : '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);
addChartSyncEvents(atrChartContainer, atrChart);
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
});
// 调整ATR图大小
atrChart.applyOptions({
width: atrChartContainer.clientWidth,
height: atrChartContainer.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 = [];
tvWidget.series.mainUncompletedBiSeries = [];
// 遍历处理每个笔
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}条`);
// 清空已有的次周期笔系列
tvWidget.series.elementBiSeries = [];
tvWidget.series.elementUncompletedBiSeries = [];
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);
}
});
}
// 绘制未完成笔 - 主周期
if ($('#showMainBi').is(':checked') && currentData.uncompleted_bi_list && currentData.uncompleted_bi_list.length > 0) {
console.log(`绘制主周期未完成笔数据,共${currentData.uncompleted_bi_list.length}条`);
currentData.uncompleted_bi_list.forEach(function(bi) {
try {
// 直接使用UTC时间戳(秒)
const startTime = Math.floor(new Date(bi.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('主周期未完成笔时间转换错误:', bi.start_time);
return;
}
const startPrice = parseFloat(bi.start_price);
if (isNaN(startPrice)) {
console.error('主周期未完成笔价格转换错误:', bi.start_price);
return;
}
// 根据笔的方向确定终点价格
let endPrice;
const latestKline = currentData.kline_data[currentData.kline_data.length-1];
if (bi.direction === 1) {
// 向上笔,终点为最新K线的最高点
endPrice = parseFloat(latestKline.high);
} else {
// 向下笔,终点为最新K线的最低点
endPrice = parseFloat(latestKline.low);
}
// 添加未完成笔(红色虚线)
biLines.push({
startTime: startTime,
endTime: endTime,
startPrice: startPrice,
endPrice: endPrice,
color: '#FF0000', // 红色
lineWidth: 1,
lineStyle: 2 // 虚线
});
// 添加到图表对象
tvWidget.series.mainUncompletedBiSeries.push({
time: startTime,
value: startPrice,
color: '#FF0000',
lineWidth: 1,
lineStyle: 2
});
} catch (e) {
console.error('主周期未完成笔处理出错:', e);
}
});
}
// 绘制未完成笔 - 次周期
if ($('#showElementBi').is(':checked') && currentData.element_uncompleted_bi_list && currentData.element_uncompleted_bi_list.length > 0) {
console.log(`绘制次周期未完成笔数据,共${currentData.element_uncompleted_bi_list.length}条`);
currentData.element_uncompleted_bi_list.forEach(function(bi) {
try {
// 直接使用UTC时间戳(秒)
const startTime = Math.floor(new Date(bi.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('次周期未完成笔时间转换错误:', bi.start_time);
return;
}
const startPrice = parseFloat(bi.start_price);
if (isNaN(startPrice)) {
console.error('次周期未完成笔价格转换错误:', bi.start_price);
return;
}
// 根据笔的方向确定终点价格
let endPrice;
const latestKline = currentData.kline_data[currentData.kline_data.length-1];
if (bi.direction === 1) {
// 向上笔,终点为最新K线的最高点
endPrice = parseFloat(latestKline.high);
} else {
// 向下笔,终点为最新K线的最低点
endPrice = parseFloat(latestKline.low);
}
// 添加未完成笔(红色虚线)
biLines.push({
startTime: startTime,
endTime: endTime,
startPrice: startPrice,
endPrice: endPrice,
color: '#FF0000', // 红色
lineWidth: 1,
lineStyle: 2 // 虚线
});
// 添加到图表对象
tvWidget.series.elementUncompletedBiSeries.push({
time: startTime,
value: startPrice,
color: '#FF0000',
lineWidth: 1,
lineStyle: 2
});
} catch (e) {
console.error('次周期未完成笔处理出错:', e);
}
});
}
// 添加所有笔到图表
biLines.forEach(line => {
const lineSeries = mainChart.addLineSeries({
color: line.color,
lineWidth: line.lineWidth,
lineStyle: line.lineStyle || 0, // 支持虚线样式
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}条`);
// 清空已有的主周期线段系列
tvWidget.series.mainSegSeries = [];
tvWidget.series.mainUncompletedSegSeries = [];
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}条`);
// 清空已有的次周期线段系列
tvWidget.series.elementSegSeries = [];
tvWidget.series.elementUncompletedSegSeries = [];
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);
}
});
}
// 绘制未完成线段 - 主周期
if ($('#showMainSeg').is(':checked') && currentData.uncompleted_seg_list && currentData.uncompleted_seg_list.length > 0) {
console.log(`绘制主周期未完成线段数据,共${currentData.uncompleted_seg_list.length}条`);
currentData.uncompleted_seg_list.forEach(function(seg) {
try {
// 直接使用UTC时间戳(秒)
const startTime = Math.floor(new Date(seg.start_time).getTime() / 1000);
if (isNaN(startTime)) {
console.error('主周期未完成线段时间转换错误:', seg.start_time);
return;
}
const startPrice = parseFloat(seg.start_price);
if (isNaN(startPrice)) {
console.error('主周期未完成线段价格转换错误:', seg.start_price);
return;
}
let endTime, endPrice;
if (seg.end_time && seg.end_price) {
// 有结束时间和价格的未完成线段(倒数第二个等)
endTime = Math.floor(new Date(seg.end_time).getTime() / 1000);
endPrice = parseFloat(seg.end_price);
if (isNaN(endTime) || isNaN(endPrice)) {
console.error('主周期未完成线段结束时间或价格转换错误:', seg.end_time, seg.end_price);
return;
}
} else {
// 没有结束时间和价格的未完成线段(最后一个)
endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
if (isNaN(endTime)) {
console.error('主周期未完成线段结束时间转换错误');
return;
}
// 根据线段的方向确定终点价格
const latestKline = currentData.kline_data[currentData.kline_data.length-1];
if (seg.direction === 1) {
// 向上线段,终点为最新K线的最高点
endPrice = parseFloat(latestKline.high);
} else {
// 向下线段,终点为最新K线的最低点
endPrice = parseFloat(latestKline.low);
}
}
// 添加未完成线段(红色虚线)
segLines.push({
startTime: startTime,
endTime: endTime,
startPrice: startPrice,
endPrice: endPrice,
color: '#FF0000', // 红色
lineWidth: 2,
lineStyle: 2 // 虚线
});
// 添加到图表对象
tvWidget.series.mainUncompletedSegSeries.push({
time: startTime,
value: startPrice,
color: '#FF0000',
lineWidth: 2,
lineStyle: 2
});
} catch (e) {
console.error('主周期未完成线段处理出错:', e);
}
});
}
// 绘制未完成线段 - 次周期
if ($('#showElementSeg').is(':checked') && currentData.element_uncompleted_seg_list && currentData.element_uncompleted_seg_list.length > 0) {
console.log(`绘制次周期未完成线段数据,共${currentData.element_uncompleted_seg_list.length}条`);
currentData.element_uncompleted_seg_list.forEach(function(seg) {
try {
// 直接使用UTC时间戳(秒)
const startTime = Math.floor(new Date(seg.start_time).getTime() / 1000);
if (isNaN(startTime)) {
console.error('次周期未完成线段时间转换错误:', seg.start_time);
return;
}
const startPrice = parseFloat(seg.start_price);
if (isNaN(startPrice)) {
console.error('次周期未完成线段价格转换错误:', seg.start_price);
return;
}
let endTime, endPrice;
if (seg.end_time && seg.end_price) {
// 有结束时间和价格的未完成线段(倒数第二个等)
endTime = Math.floor(new Date(seg.end_time).getTime() / 1000);
endPrice = parseFloat(seg.end_price);
if (isNaN(endTime) || isNaN(endPrice)) {
console.error('次周期未完成线段结束时间或价格转换错误:', seg.end_time, seg.end_price);
return;
}
} else {
// 没有结束时间和价格的未完成线段(最后一个)
endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
if (isNaN(endTime)) {
console.error('次周期未完成线段结束时间转换错误');
return;
}
// 根据线段的方向确定终点价格
const latestKline = currentData.kline_data[currentData.kline_data.length-1];
if (seg.direction === 1) {
// 向上线段,终点为最新K线的最高点
endPrice = parseFloat(latestKline.high);
} else {
// 向下线段,终点为最新K线的最低点
endPrice = parseFloat(latestKline.low);
}
}
// 添加未完成线段(红色虚线)
segLines.push({
startTime: startTime,
endTime: endTime,
startPrice: startPrice,
endPrice: endPrice,
color: '#FF0000', // 红色
lineWidth: 2,
lineStyle: 2 // 虚线
});
// 添加到图表对象
tvWidget.series.elementUncompletedSegSeries.push({
time: startTime,
value: startPrice,
color: '#FF0000',
lineWidth: 2,
lineStyle: 2
});
} catch (e) {
console.error('次周期未完成线段处理出错:', e);
}
});
}
// 添加所有线段到图表
segLines.forEach(line => {
const lineSeries = mainChart.addLineSeries({
color: line.color,
lineWidth: line.lineWidth,
lineStyle: line.lineStyle || 0, // 支持虚线样式
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); // 中枢下沿
const gg = parseFloat(zs.gg); // 中枢高高
const dd = parseFloat(zs.dd); // 中枢低低
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 }
]);
// 绘制gg线(中枢高高)
if (!isNaN(gg) && gg > 0) {
const ggSeries = mainChart.addLineSeries({
color: '#F1C40F', // 使用中枢自己的颜色
lineWidth: 1,
lastValueVisible: false,
priceLineVisible: false,
});
ggSeries.setData([
{ time: startTime, value: gg },
{ time: endTime, value: gg }
]);
}
// 绘制dd线(中枢低低)
if (!isNaN(dd) && dd > 0) {
const ddSeries = mainChart.addLineSeries({
color: '#F1C40F', // 使用中枢自己的颜色
lineWidth: 1,
lastValueVisible: false,
priceLineVisible: false,
});
ddSeries.setData([
{ time: startTime, value: dd },
{ time: endTime, value: dd }
]);
}
// 添加到图表对象
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); // 中枢下沿
const gg = parseFloat(zs.gg); // 中枢高高
const dd = parseFloat(zs.dd); // 中枢低低
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 }
]);
// 绘制gg线(中枢高高)
if (!isNaN(gg) && gg > 0) {
const ggSeries = mainChart.addLineSeries({
color: '#3f51b5', // 使用次周期中枢自己的颜色
lineWidth: 1,
lastValueVisible: false,
priceLineVisible: false,
});
ggSeries.setData([
{ time: startTime, value: gg },
{ time: endTime, value: gg }
]);
}
// 绘制dd线(中枢低低)
if (!isNaN(dd) && dd > 0) {
const ddSeries = mainChart.addLineSeries({
color: '#3f51b5', // 使用次周期中枢自己的颜色
lineWidth: 1,
lastValueVisible: false,
priceLineVisible: false,
});
ddSeries.setData([
{ time: startTime, value: dd },
{ time: endTime, value: dd }
]);
}
// 添加到图表对象
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); // 中枢下沿
const gg = parseFloat(zs.gg); // 中枢高高
const dd = parseFloat(zs.dd); // 中枢低低
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 markerSeries = mainChart.addLineSeries({
lastValueVisible: false,
priceLineVisible: false,
});
markerSeries.setMarkers([
{
time: startTime,
position: 'aboveBar',
color: '#F1C40F',
shape: 'circle',
text: '未完',
size: 1
}
]);
// 绘制gg线(中枢高高)
if (!isNaN(gg) && gg > 0) {
const ggSeries = mainChart.addLineSeries({
color: '#F1C40F', // 使用中枢自己的颜色
lineWidth: 1,
lastValueVisible: false,
priceLineVisible: false,
});
ggSeries.setData([
{ time: startTime, value: gg },
{ time: endTime, value: gg }
]);
}
// 绘制dd线(中枢低低)
if (!isNaN(dd) && dd > 0) {
const ddSeries = mainChart.addLineSeries({
color: '#F1C40F', // 使用中枢自己的颜色
lineWidth: 1,
lastValueVisible: false,
priceLineVisible: false,
});
ddSeries.setData([
{ time: startTime, value: dd },
{ time: endTime, value: dd }
]);
}
// 添加到图表对象
tvWidget.series.mainUncompletedZsSeries.push({
time: startTime,
value: zg,
color: '#F1C40F',
lineWidth: 1
});
tvWidget.series.mainUncompletedZsSeries.push({
time: endTime,
value: zg,
color: '#F1C40F',
lineWidth: 1
});
tvWidget.series.mainUncompletedZsSeries.push({
time: startTime,
value: zd,
color: '#F1C40F',
lineWidth: 1
});
tvWidget.series.mainUncompletedZsSeries.push({
time: endTime,
value: zd,
color: '#F1C40F',
lineWidth: 1
});
} 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); // 中枢下沿
const gg = parseFloat(zs.gg); // 中枢高高
const dd = parseFloat(zs.dd); // 中枢低低
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 markerSeries = mainChart.addLineSeries({
lastValueVisible: false,
priceLineVisible: false,
});
markerSeries.setMarkers([
{
time: startTime,
position: 'aboveBar',
color: '#3f51b5',
shape: 'circle',
text: '未完',
size: 1
}
]);
// 绘制gg线(中枢高高)
if (!isNaN(gg) && gg > 0) {
const ggSeries = mainChart.addLineSeries({
color: '#3f51b5', // 使用次周期中枢自己的颜色
lineWidth: 1,
lastValueVisible: false,
priceLineVisible: false,
});
ggSeries.setData([
{ time: startTime, value: gg },
{ time: endTime, value: gg }
]);
}
// 绘制dd线(中枢低低)
if (!isNaN(dd) && dd > 0) {
const ddSeries = mainChart.addLineSeries({
color: '#3f51b5', // 使用次周期中枢自己的颜色
lineWidth: 1,
lastValueVisible: false,
priceLineVisible: false,
});
ddSeries.setData([
{ time: startTime, value: dd },
{ time: endTime, value: dd }
]);
}
// 添加到图表对象
tvWidget.series.elementUncompletedZsSeries.push({
time: startTime,
value: zg,
color: '#3f51b5',
lineWidth: 1
});
tvWidget.series.elementUncompletedZsSeries.push({
time: endTime,
value: zg,
color: '#3f51b5',
lineWidth: 1
});
tvWidget.series.elementUncompletedZsSeries.push({
time: startTime,
value: zd,
color: '#3f51b5',
lineWidth: 1
});
tvWidget.series.elementUncompletedZsSeries.push({
time: endTime,
value: zd,
color: '#3f51b5',
lineWidth: 1
});
} catch (e) {
console.error('次周期未完成中枢处理出错:', e);
}
});
}
} else {
console.log('绘制未完成中枢 - 已禁用');
}
// 添加买卖点标记
if ($('#showTradePoints').is(':checked')) {
console.log('绘制买卖点 - 已启用');
// 优先使用小周期数据,如果不存在则使用主周期数据
const tradePointsData = currentData.element_trade_points || currentData.trade_points;
console.log(`绘制${currentData.element_trade_points ? '元素周期' : '主周期'}买卖点数据,共${tradePointsData ? tradePointsData.length : 0}条`);
// 调试信息 - 输出完整的买卖点数据
if (tradePointsData && tradePointsData.length > 0) {
console.log("买卖点数据样例:", tradePointsData[0]);
// 检查数据格式,如果time不是标准格式,进行格式化处理
const checkDataFormat = () => {
for (let i = 0; i < tradePointsData.length; i++) {
if (tradePointsData[i].time) {
// 确保时间是标准格式
try {
const timeValue = new Date(tradePointsData[i].time);
if (isNaN(timeValue.getTime())) {
console.error(`买卖点 #${i} 时间格式无效:`, tradePointsData[i].time);
}
} catch (e) {
console.error(`买卖点 #${i} 时间格式异常:`, e);
}
} else {
console.error(`买卖点 #${i} 缺少时间属性`);
}
}
};
// 执行格式检查
checkDataFormat();
// 对买卖点按时间排序,用于后续优化显示
const sortedPoints = [...tradePointsData].sort((a, b) => {
return new Date(a.time) - new Date(b.time);
});
// 记录已处理的时间点 - 按类型分开计数
const processedTimes = {};
// 创建买卖点标记系列
const buyMarkers = [];
const sellMarkers = [];
// 计数器,追踪成功和失败的处理次数
let successCount = 0;
let errorCount = 0;
sortedPoints.forEach(function(point, index) {
try {
// 检查所有必要的属性是否存在且有效
if (!point.time || !point.price || point.type === undefined) {
console.error(`买卖点 #${index} 数据不完整:`, point);
errorCount++;
return;
}
const time = Math.floor(new Date(point.time).getTime() / 1000);
const price = parseFloat(point.price);
const type = parseInt(point.type);
if (isNaN(time) || isNaN(price) || isNaN(type)) {
console.error(`买卖点 #${index} 数据格式错误:`,
{ time: isNaN(time), price: isNaN(price), type: isNaN(type) }, point);
errorCount++;
return;
}
// 获取买卖点样式
const style = TRADE_POINT_STYLE[type] || {
color: '#999999',
shape: 'circle',
text: '?',
size: 1
};
// 初始化该时间点的类型计数器
if (!processedTimes[time]) {
processedTimes[time] = {};
}
// 优化:检查是否有相同时间点和相同类型的标记,如果有,进行类型内的偏移
let stackIndex = 0;
if (processedTimes[time][type]) {
// 已经有相同时间和类型的标记,记录堆叠索引
stackIndex = processedTimes[time][type];
processedTimes[time][type]++;
} else {
// 第一次出现这个时间点的这个类型
processedTimes[time][type] = 1;
}
// 为不同类型的买卖点获取基础垂直偏移系数
const baseOffset = TRADE_POINT_OFFSET[type] || 0;
// 创建标记对象,包含额外的信息用于悬停提示
const marker = {
time: time,
position: 'inBar', // 改为在K线内部显示,不影响数据
color: style.color,
shape: style.shape,
text: style.text,
size: style.size,
// 记录堆叠索引
stackIndex: stackIndex,
// 添加悬停提示的数据
tooltip: `<span class="${type > 0 ? 'buy-point' : 'sell-point'}">${point.desc || (type > 0 ? '买点' : '卖点')}</span><br>
时间: ${formatTime(point.time)}<br>
价格: ${price.toFixed(2)}`,
// 额外添加基础类型偏移
baseOffset: baseOffset,
// 添加边框
borderColor: 'white',
borderWidth: 1,
// 添加价格偏移系数
pricePercentOffset: PRICE_PERCENT_OFFSET[type] || 0,
// 保存实际价格用于计算
price: price,
// 保存类型
type: type
};
// 区分买卖点
if (type > 0) {
buyMarkers.push(marker);
} else {
sellMarkers.push(marker);
}
successCount++;
} catch (e) {
console.error(`处理买卖点 #${index} 出错:`, e, point);
errorCount++;
}
});
console.log(`买卖点处理完成: 成功=${successCount}, 失败=${errorCount}, 买点=${buyMarkers.length}, 卖点=${sellMarkers.length}`);
// 分别添加买卖点标记
if (buyMarkers.length > 0) {
const buyMarkersSeries = mainChart.addLineSeries({
lastValueVisible: false,
priceLineVisible: false,
lineVisible: false,
title: '买点'
});
// 设置临时的数据点
buyMarkersSeries.setData([{ time: buyMarkers[0].time, value: 0 }]);
try {
// 设置买点标记,并添加偏移
buyMarkersSeries.setMarkers(
buyMarkers.map(marker => {
// 使用固定偏移而非百分比
const fixedOffset = PRICE_FIXED_OFFSET[marker.type] || 20;
// 为同一时间点的多个买点额外增加堆叠偏移
const stackOffset = marker.stackIndex > 0 ?
10 * marker.stackIndex : 0;
// 使用实际价格位置添加标记,不使用偏移
return {
...marker,
position: 'aboveBar', // 显示在K线上方
// 使用原始价格
price: marker.price
};
})
);
console.log(`成功添加 ${buyMarkers.length} 个买点标记`);
} catch (e) {
console.error("设置买点标记时出错:", e);
}
}
if (sellMarkers.length > 0) {
const sellMarkersSeries = mainChart.addLineSeries({
lastValueVisible: false,
priceLineVisible: false,
lineVisible: false,
title: '卖点'
});
// 设置临时的数据点
sellMarkersSeries.setData([{ time: sellMarkers[0].time, value: 0 }]);
try {
// 设置卖点标记,并添加偏移
sellMarkersSeries.setMarkers(
sellMarkers.map(marker => {
// 使用固定偏移而非百分比
const fixedOffset = PRICE_FIXED_OFFSET[marker.type] || 20;
// 为同一时间点的多个卖点额外增加堆叠偏移
const stackOffset = marker.stackIndex > 0 ?
10 * marker.stackIndex : 0;
// 使用实际价格位置添加标记,不使用偏移
return {
...marker,
position: 'belowBar', // 显示在K线下方
// 使用原始价格
price: marker.price
};
})
);
console.log(`成功添加 ${sellMarkers.length} 个卖点标记`);
} catch (e) {
console.error("设置卖点标记时出错:", e);
}
}
// 添加鼠标悬停事件显示提示
mainChart.subscribeCrosshairMove(param => {
// 十字线同步到其他图表 - 通过DOM元素绘制垂直线实现虚线延长效果
if (param.time && param.point && volumeChart) {
try {
// 清除之前的十字线标记
const existingVolumeLines = document.querySelectorAll('.volume-crosshair-line');
existingVolumeLines.forEach(line => line.remove());
const existingAtrLines = document.querySelectorAll('.atr-crosshair-line');
existingAtrLines.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);
}
// 在ATR图上绘制垂直线
if (atrChart && atrChartContainer) {
const atrTimeCoordinate = atrChart.timeScale().timeToCoordinate(param.time);
if (atrTimeCoordinate !== null) {
const atrChartRect = atrChartContainer.getBoundingClientRect();
const atrLine = document.createElement('div');
atrLine.className = 'atr-crosshair-line';
atrLine.style.position = 'fixed'; // 改为fixed定位
atrLine.style.left = (atrChartRect.left + atrTimeCoordinate) + 'px';
atrLine.style.top = atrChartRect.top + 'px';
atrLine.style.width = '1px';
atrLine.style.height = atrChartRect.height + 'px';
atrLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)';
atrLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)';
atrLine.style.pointerEvents = 'none';
atrLine.style.zIndex = '1000';
document.body.appendChild(atrLine);
}
}
// 如果有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 existingAtrLines = document.querySelectorAll('.atr-crosshair-line');
existingAtrLines.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';
});
}
} 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('showKlcFxType勾选状态:', $('#showKlcFxType').is(':checked'));
console.log('showKluFxType勾选状态:', $('#showKluFxType').is(':checked'));
console.log('currentData.klc_fx_info存在:', !!currentData.klc_fx_info);
console.log('currentData.klu_fx_info存在:', !!currentData.klu_fx_info);
console.log('currentData.klc_fx_info长度:', currentData.klc_fx_info ? currentData.klc_fx_info.length : 'undefined');
console.log('currentData.klu_fx_info长度:', currentData.klu_fx_info ? currentData.klu_fx_info.length : 'undefined');
if (currentData.klc_fx_info && currentData.klc_fx_info.length > 0) {
console.log('前3个klc分型数据样本:', currentData.klc_fx_info.slice(0, 3));
}
if (currentData.klu_fx_info && currentData.klu_fx_info.length > 0) {
console.log('前3个klu分型数据样本:', currentData.klu_fx_info.slice(0, 3));
}
// 收集所有主周期分型标记
const allMainFxMarkers = [];
const mainFxMarkers = []; // 用于tooltip支持
// 处理主周期KLC分型
if ($('#showKlcFxType').is(':checked') && currentData.klc_fx_info && currentData.klc_fx_info.length > 0) {
console.log(`绘制主周期K线合并分型标签,共${currentData.klc_fx_info.length}条`);
currentData.klc_fx_info.forEach(function(fx) {
try {
// 直接使用UTC时间戳(秒)
const timestamp = Math.floor(new Date(fx.time).getTime() / 1000);
const price = parseFloat(fx.price);
if (isNaN(timestamp) || isNaN(price)) {
console.error('主周期KLC分型时间或价格转换错误:', fx.time, fx.price);
return;
}
// 主周期 KLC
// 确定颜色和位置
const color = fx.is_bottom ? '#28a745' : '#dc3545'; // 底分型绿色,顶分型红色
// 根据强度等级调整颜色强度
let strengthColor = color;
// 构建显示文本,包含分型类型和强度信息
let displayText = `${fx.fx_strength.toFixed(1)}`;
if (fx.fx_strength < 1.0) { // 降低阈值,让更多分型显示
displayText = fx.fx_strength >= 0.8 ? '' : '' // 0.8以上显示点,0.8以下不显示文本
}
displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("1", "").replace("2", "").replace("3", "");
// 添加标记配置
const markerConfig = {
time: timestamp,
position: fx.is_bottom ? 'belowBar' : 'aboveBar',
color: strengthColor,
shape: 'triangle',
text: displayText,
size: 2 // 调整尺寸,强分型稍大,普通分型更小
};
allMainFxMarkers.push(markerConfig);
// 创建分型标记对象,包含tooltip信息
const fxMarker = {
time: timestamp,
tooltip: `<div style="color: ${strengthColor}; font-weight: bold;">
主周期${fx.is_bottom ? '底分型' : '顶分型'}(合): ${fx.fx_type}<br>
强度分数: ${fx.fx_strength}分<br>
强度等级: ${fx.fx_strength_level}<br>
是否强分型: ${fx.is_strong_fx ? '是' : '否'}<br>
价格: ${price.toFixed(4)}<br>
时间: ${fx.time}
</div>`
};
mainFxMarkers.push(fxMarker);
} catch (e) {
console.error('绘制主周期KLC分型标签出错:', e);
}
});
}
// 处理主周期KLU分型
if ($('#showKluFxType').is(':checked') && currentData.klu_fx_info && currentData.klu_fx_info.length > 0) {
console.log(`绘制主周期K线未合并分型标签,共${currentData.klu_fx_info.length}条`);
currentData.klu_fx_info.forEach(function(fx) {
try {
// 直接使用UTC时间戳(秒)
const timestamp = Math.floor(new Date(fx.time).getTime() / 1000);
const price = parseFloat(fx.price);
if (isNaN(timestamp) || isNaN(price)) {
console.error('主周期KLU分型时间或价格转换错误:', fx.time, fx.price);
return;
}
// 主周期 KLU
const color = fx.is_bottom ? '#17a2b8' : '#fd7e14'; // 底分型用青色,顶分型用橙色
// 根据强度等级调整颜色强度
let strengthColor = color;
if (fx.is_strong_fx) {
// 强分型使用更亮的颜色
strengthColor = fx.is_bottom ? '#20c997' : '#fd7e14';
}
// 构建显示文本,包含分型类型和强度信息
let displayText = `${fx.fx_strength.toFixed(1)}`;
if (fx.fx_strength < 1.0) { // 降低阈值,让更多分型显示
displayText = fx.fx_strength >= 1.5 ? '' : '' // 0.8以上显示点,0.8以下不显示文本
}
displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("1", "").replace("2", "").replace("3", "");
// 添加标记配置
const markerConfig = {
time: timestamp,
position: fx.is_bottom ? 'belowBar' : 'aboveBar',
color: strengthColor,
// shape: 'triangle', // 使用三角形区分KLU分型
text: displayText,
size: fx.is_strong_fx ? 0.8 : 0.5 // KLU分型稍小一些
};
allMainFxMarkers.push(markerConfig);
// 创建分型标记对象,包含tooltip信息
const fxMarker = {
time: timestamp,
tooltip: `<div style="color: ${strengthColor}; font-weight: bold;">
主周期${fx.is_bottom ? '底分型' : '顶分型'}(原): ${fx.fx_type}<br>
强度分数: ${fx.fx_strength}分<br>
强度等级: ${fx.fx_strength_level}<br>
是否强分型: ${fx.is_strong_fx ? '是' : '否'}<br>
价格: ${price.toFixed(4)}<br>
时间: ${fx.time}
</div>`
};
mainFxMarkers.push(fxMarker);
} catch (e) {
console.error('绘制主周期KLU分型标签出错:', e);
}
});
}
// 暂存主周期分型标记
window.mainFxMarkers = allMainFxMarkers;
// 将分型标记添加到全局markers中以支持tooltip功能
if (window.fxMarkers) {
window.fxMarkers = [...window.fxMarkers, ...mainFxMarkers];
} else {
window.fxMarkers = mainFxMarkers;
}
// 检查是否有任何主周期分型数据
const hasMainFxData = ($('#showKlcFxType').is(':checked') && currentData.klc_fx_info && currentData.klc_fx_info.length > 0) ||
($('#showKluFxType').is(':checked') && currentData.klu_fx_info && currentData.klu_fx_info.length > 0);
if (!hasMainFxData) {
console.log('绘制主周期分型标记 - 已禁用或无数据');
// 清空主周期分型标记
window.mainFxMarkers = [];
window.fxMarkers = [];
}
// 绘制小周期分型标记
if (($('#showElementKlcFxType').is(':checked') && currentData.element_klc_fx_info && currentData.element_klc_fx_info.length > 0) ||
($('#showElementKluFxType').is(':checked') && currentData.element_klu_fx_info && currentData.element_klu_fx_info.length > 0)) {
// 收集所有小周期分型标记
const allElementFxMarkers = [];
const elementFxMarkers = []; // 用于tooltip支持
// 处理小周期KLC分型
if ($('#showElementKlcFxType').is(':checked') && currentData.element_klc_fx_info && currentData.element_klc_fx_info.length > 0) {
console.log(`绘制小周期K线合并分型标记,共${currentData.element_klc_fx_info.length}条`);
currentData.element_klc_fx_info.forEach(function(fx) {
try {
// 直接使用UTC时间戳(秒)
const timestamp = Math.floor(new Date(fx.time).getTime() / 1000);
const price = parseFloat(fx.price);
if (isNaN(timestamp) || isNaN(price)) {
console.error('小周期KLC分型时间或价格转换错误:', fx.time, fx.price);
return;
}
// 小周期 KLC
let strengthColor = fx.is_bottom ? '#11116B' : '#222222'; // 底分型用珊瑚红,顶分型用薄荷绿
let displayText = `${fx.fx_strength.toFixed(1)}`;
// 构建小周期分型显示文本
if (fx.fx_strength < 1.0){ // 调整小周期阈值
displayText = fx.fx_strength >= 0.6 ? '' : '' // 0.6以上显示点
}
displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("1", "").replace("2", "").replace("3", "");
// 小周期分型标记配置
const markerConfig = {
time: timestamp,
position: fx.is_bottom ? 'belowBar' : 'aboveBar',
color: strengthColor,
shape: 'triangle',
text: displayText,
size: fx.is_strong_fx ? 0.8 : 0.6 // 小周期标记整体更小一些
};
allElementFxMarkers.push(markerConfig);
// 创建小周期分型标记对象,包含tooltip信息
const elementFxMarker = {
time: timestamp,
tooltip: `<div style="color: ${strengthColor}; font-weight: bold;">
小周期${fx.is_bottom ? '底分型' : '顶分型'}(合): ${fx.fx_type}<br>
强度分数: ${fx.fx_strength}分<br>
强度等级: ${fx.fx_strength_level}<br>
是否强分型: ${fx.is_strong_fx ? '是' : '否'}<br>
价格: ${price.toFixed(4)}<br>
时间: ${fx.time}
</div>`
};
elementFxMarkers.push(elementFxMarker);
} catch (e) {
console.error('绘制小周期KLC分型标记出错:', e);
}
});
}
// 处理小周期KLU分型
if ($('#showElementKluFxType').is(':checked') && currentData.element_klu_fx_info && currentData.element_klu_fx_info.length > 0) {
console.log(`绘制小周期K线未合并分型标记,共${currentData.element_klu_fx_info.length}条`);
currentData.element_klu_fx_info.forEach(function(fx) {
try {
// 直接使用UTC时间戳(秒)
const timestamp = Math.floor(new Date(fx.time).getTime() / 1000);
const price = parseFloat(fx.price);
if (isNaN(timestamp) || isNaN(price)) {
console.error('小周期KLU分型时间或价格转换错误:', fx.time, fx.price);
return;
}
// 小周期 KLU
let strengthColor = fx.is_bottom ? '#9A8C98' : '#F2CC8F'; // 底分型用灰紫色,顶分型用浅黄色
let displayText = `${fx.fx_strength.toFixed(1)}`;
// 构建小周期分型显示文本
if (fx.fx_strength < 2.0){ // 调整小周期阈值
displayText = fx.fx_strength >= 1.5 ? '' : '' // 0.6以上显示点
}
displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("1", "").replace("2", "").replace("3", "");
// 小周期KLU分型标记配置
const markerConfig = {
time: timestamp,
position: fx.is_bottom ? 'belowBar' : 'aboveBar',
color: strengthColor,
// shape: 'triangle', // 使用三角形区分KLU分型
text: displayText,
size: fx.is_strong_fx ? 0.7 : 0.5 // 小周期KLU标记更小一些
};
allElementFxMarkers.push(markerConfig);
// 创建小周期分型标记对象,包含tooltip信息
const elementFxMarker = {
time: timestamp,
tooltip: `<div style="color: ${strengthColor}; font-weight: bold;">
小周期${fx.is_bottom ? '底分型' : '顶分型'}(原): ${fx.fx_type}<br>
强度分数: ${fx.fx_strength}分<br>
强度等级: ${fx.fx_strength_level}<br>
是否强分型: ${fx.is_strong_fx ? '是' : '否'}<br>
价格: ${price.toFixed(4)}<br>
时间: ${fx.time}
</div>`
};
elementFxMarkers.push(elementFxMarker);
} catch (e) {
console.error('绘制小周期KLU分型标记出错:', e);
}
});
}
// 将小周期分型标记添加到全局markers中以支持tooltip功能
if (window.fxMarkers) {
window.fxMarkers = [...window.fxMarkers, ...elementFxMarkers];
} else {
window.fxMarkers = elementFxMarkers;
}
// 合并主周期和小周期分型标记,统一设置到K线数据系列
const combinedMarkers = [...(window.mainFxMarkers || []), ...allElementFxMarkers];
if (combinedMarkers.length > 0) {
console.log('合并设置', combinedMarkers.length, '个分型标记(主周期:', (window.mainFxMarkers || []).length, '个,小周期:', allElementFxMarkers.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.mainFxMarkers && window.mainFxMarkers.length > 0) {
console.log('仅设置', window.mainFxMarkers.length, '个主周期分型标记');
// 尝试在K线系列上设置标记
if (showOriginalKline && tvWidget.series.candleSeries) {
tvWidget.series.candleSeries.setMarkers(window.mainFxMarkers);
console.log('主周期标记已设置到蜡烛图系列');
} else if (!showOriginalKline && tvWidget.series.lineSeries) {
tvWidget.series.lineSeries.setMarkers(window.mainFxMarkers);
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([]);
}
}
}
// 同步所有图表的时间轴配置
const syncTimeScaleSettings = () => {
// 获取主图表的时间轴设置
const mainTimeScale = mainChart.timeScale();
const baseOptions = {
timeVisible: true,
secondsVisible: false,
borderColor: '#ddd',
barSpacing: symbolConfig.type === 'a_stock' ? 6 : 10,
rightOffset: 12,
lockVisibleTimeRangeOnResize: true,
// 关键:确保所有图表边缘行为完全一致
fixLeftEdge: false,
fixRightEdge: false,
// 确保时间刻度行为一致
ticksVisible: true,
minimumHeight: 0,
};
console.log('🔧 同步时间轴设置:', baseOptions);
// 应用相同的设置到所有图表
mainChart.timeScale().applyOptions(baseOptions);
volumeChart.timeScale().applyOptions(baseOptions);
atrChart.timeScale().applyOptions(baseOptions);
if (showMacd && macdChart) {
macdChart.timeScale().applyOptions(baseOptions);
}
};
// 首先同步时间轴设置
syncTimeScaleSettings();
// 然后让主图表适应内容
mainChart.timeScale().fitContent();
// 立即同步其他图表到主图表的范围
setTimeout(() => {
const visibleRange = mainChart.timeScale().getVisibleRange();
if (visibleRange) {
console.log('🔧 同步可见范围:', visibleRange);
volumeChart.timeScale().setVisibleRange(visibleRange);
atrChart.timeScale().setVisibleRange(visibleRange);
if (showMacd && macdChart) {
macdChart.timeScale().setVisibleRange(visibleRange);
}
console.log('🔧 时间轴同步完成');
}
}, 50);
// 保存图表对象
tvWidget.mainChart = mainChart;
tvWidget.volumeChart = volumeChart;
tvWidget.atrChart = atrChart;
tvWidget.macdChart = macdChart;
tvWidget.state.isInitialized = true;
// 初始化默认EMA配置(仅在首次初始化时)
if (movingAverages.length === 0) {
console.log('初始化默认EMA指标');
// 添加默认EMA配置
const defaultEMAs = [
{ type: 'EMA', length: 5, color: '#FF0000', name: 'EMA5' }, // 红色
{ type: 'EMA', length: 10, color: '#0000FF', name: 'EMA10' }, // 蓝色
{ type: 'EMA', length: 26, color: '#008000', name: 'EMA26' }, // 深绿色
{ type: 'EMA', length: 52, color: '#800080', name: 'EMA52' } // 紫色
];
defaultEMAs.forEach(ema => {
const config = {
id: ++maIdCounter,
type: ema.type,
length: ema.length,
source: 'close',
smoothType: 'none',
smoothLength: 3,
lineWidth: 1, // 1px线宽
lineStyle: 0, // 实线
color: ema.color,
visible: true
};
movingAverages.push(config);
console.log(`添加默认${ema.name}:`, ema.color);
});
console.log('默认EMA配置完成,共添加', movingAverages.length, '个指标');
}
// 添加均线到图表
addMovingAveragesToChart(candles);
// 添加布林带到图表
addBollingerBandsToChart(candles);
// 更新技术指标面板显示
updateIndicatorPanel();
// 绑定同步事件
bindSyncEvents(mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, mainChart, volumeChart, atrChart, macdChart, showMacd);
// 最终确保所有图表时间轴对齐
setTimeout(() => {
const visibleRange = mainChart.timeScale().getVisibleRange();
if (visibleRange) {
console.log('🔧 最终同步可见范围:', visibleRange);
// 强制重新设置所有图表的可见范围
volumeChart.timeScale().setVisibleRange(visibleRange);
atrChart.timeScale().setVisibleRange(visibleRange);
if (showMacd && macdChart) {
macdChart.timeScale().setVisibleRange(visibleRange);
}
console.log('🔧 最终时间轴对齐完成');
}
}, 150);
// 只有在时间输入框都为空时才设置图表默认时间范围
if (!$('#start_time').val() && !$('#end_time').val()) {
setDefaultTimeRange();
}
// 添加买卖点提示
setupTooltip(mainChart, [], [], mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, volumeChart, atrChart, macdChart, showMacd);
// 显示买卖点
if ($('#showTradePoints').is(':checked')) {
displayTradePoints();
}
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);
}
// 更新均线数据
addMovingAveragesToChart(candles);
// 更新布林带数据
addBollingerBandsToChart(candles);
// 更新成交量数据
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);
}
// 更新ATR数据
if (tvWidget.series.atrLineSeries) {
const atrData = [];
const atrDataSource = useElementPeriod ?
(currentData.element_atr || currentData.atr) :
currentData.atr;
if (atrDataSource && Array.isArray(atrDataSource)) {
const klineDataSource = useElementPeriod ? currentData.element_kline_data : currentData.kline_data;
// 修复:为每个K线时间点都创建ATR数据点,包括没有ATR值的前期数据
for (let i = 0; i < klineDataSource.length; i++) {
const kline = klineDataSource[i];
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
// 为每个时间点都添加数据以保持时间轴对齐,但ATR为0时不显示
if (atrDataSource[i] !== undefined) {
if (atrDataSource[i] > 0) {
// ATR有效值,正常显示
atrData.push({
time: timestamp,
value: atrDataSource[i]
});
} else {
// ATR为0,添加时间点但不显示线条(使用undefined作为value
atrData.push({
time: timestamp,
value: undefined
});
}
}
}
console.log('🔄 增量更新ATR数据点数:', atrData.length);
}
tvWidget.series.atrLineSeries.setData(atrData);
}
// 更新MACD数据
if (tvWidget.series.macdLineSeries && currentData.macd && currentData.kline_data && Array.isArray(currentData.kline_data)) {
// 提取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();
// 恢复之前的可视范围 - 优先使用visibleRange以确保时间轴对齐
if (tvWidget.mainChart) {
if (tvWidget.state.visibleRange) {
console.log('🔄 恢复可见范围:', tvWidget.state.visibleRange);
tvWidget.mainChart.timeScale().setVisibleRange(tvWidget.state.visibleRange);
if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleRange(tvWidget.state.visibleRange);
if (tvWidget.atrChart) tvWidget.atrChart.timeScale().setVisibleRange(tvWidget.state.visibleRange);
if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleRange(tvWidget.state.visibleRange);
} else if (tvWidget.state.logicalRange) {
console.log('🔄 恢复逻辑范围:', tvWidget.state.logicalRange);
tvWidget.mainChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange);
if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange);
if (tvWidget.atrChart) tvWidget.atrChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange);
if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange);
}
}
console.log('增量更新图表完成');
} catch (e) {
console.error('增量更新图表错误,回退到完全重绘:', e);
// 出错时回退到完全重绘
initTradingView($('#symbol').val(), $('#timeframe').val());
}
}
function bindSyncEvents(mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, mainChart, volumeChart, atrChart, macdChart, showMacd) {
// 防止同步过程中的无限循环
let syncInProgress = false;
// 用于跟踪所有图表的拖动状态 - 在函数内部定义以确保作用域正确
let localDragStates = {
main: false,
volume: false,
atr: false,
macd: false
};
// 同步图表的时间范围
function syncCharts(sourceChart, sourceContainer) {
// 防止无限循环 - 使用更精确的检查
if (syncInProgress) {
console.log('🔄 同步正在进行中,跳过此次同步');
return;
}
syncInProgress = true;
console.log('🚀 开始同步图表,来源:',
sourceChart === mainChart ? '主图' :
sourceChart === volumeChart ? '成交量图' :
sourceChart === atrChart ? 'ATR图' : '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);
}
}
// 同步ATR图
if (sourceChart !== atrChart && atrChart && atrChart.timeScale) {
try {
atrChart.timeScale().setVisibleLogicalRange(logicalRange);
console.log('✅ ATR图同步完成');
} catch (e) {
console.error('❌ ATR图同步失败:', 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;
// 同时保存可见范围以确保精确对齐
try {
const visibleRange = sourceChart.timeScale().getVisibleRange();
tvWidget.state.visibleRange = visibleRange;
console.log('💾 保存状态 - 逻辑范围:', logicalRange, '可见范围:', visibleRange);
} catch (e) {
console.warn('⚠️ 保存可见范围失败:', e);
}
}
} else {
console.warn('⚠️ 无效的逻辑范围:', logicalRange);
}
} else {
console.warn('⚠️ 无效的源图表或时间刻度');
}
} catch (e) {
console.error('💥 同步图表出错:', e);
}
// 立即重置同步标志,提高响应速度
setTimeout(() => {
syncInProgress = false;
console.log('🔓 同步标志已重置');
}, 1);
}
// 为每个图表添加事件监听
const addChartSyncEvents = (chartContainer, chart) => {
console.log('为图表添加同步事件监听:',
chart === mainChart ? '主图' :
chart === volumeChart ? '成交量图' :
chart === atrChart ? 'ATR图' : 'MACD图');
// 确定当前图表类型
const chartType = chart === mainChart ? 'main' :
chart === volumeChart ? 'volume' :
chart === atrChart ? 'atr' : '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 (atrChartContainer && atrChart) {
addChartSyncEvents(atrChartContainer, atrChart);
}
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
});
}
// 调整ATR图大小
if (atrChart && atrChartContainer) {
atrChart.applyOptions({
width: atrChartContainer.clientWidth,
height: atrChartContainer.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, atrChartContainer, macdChartContainer, volumeChart, atrChart, 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 existingAtrLines = document.querySelectorAll('.atr-crosshair-line');
existingAtrLines.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);
}
// 在ATR图上绘制垂直线
if (atrChart && atrChartContainer) {
const atrTimeCoordinate = atrChart.timeScale().timeToCoordinate(param.time);
if (atrTimeCoordinate !== null) {
const atrChartRect = atrChartContainer.getBoundingClientRect();
const atrLine = document.createElement('div');
atrLine.className = 'atr-crosshair-line';
atrLine.style.position = 'fixed'; // 改为fixed定位
atrLine.style.left = (atrChartRect.left + atrTimeCoordinate) + 'px';
atrLine.style.top = atrChartRect.top + 'px';
atrLine.style.width = '1px';
atrLine.style.height = atrChartRect.height + 'px';
atrLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)';
atrLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)';
atrLine.style.pointerEvents = 'none';
atrLine.style.zIndex = '1000';
document.body.appendChild(atrLine);
}
}
// 如果有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 existingAtrLines = document.querySelectorAll('.atr-crosshair-line');
existingAtrLines.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 displayTradePoints() {
// 优先使用小周期数据,如果不存在则使用主周期数据
const tradePointsData = currentData.element_trade_points || currentData.trade_points;
console.log(`绘制${currentData.element_trade_points ? '元素周期' : '主周期'}买卖点数据,共${tradePointsData ? tradePointsData.length : 0}条`);
// 调试信息 - 输出完整的买卖点数据
if (tradePointsData && tradePointsData.length > 0) {
console.log("买卖点数据样例:", tradePointsData[0]);
// 检查数据格式,如果time不是标准格式,进行格式化处理
const checkDataFormat = () => {
for (let i = 0; i < tradePointsData.length; i++) {
if (tradePointsData[i].time) {
// 确保时间是标准格式
try {
const timeValue = new Date(tradePointsData[i].time);
if (isNaN(timeValue.getTime())) {
console.error(`买卖点 #${i} 时间格式无效:`, tradePointsData[i].time);
}
} catch (e) {
console.error(`买卖点 #${i} 时间格式异常:`, e);
}
} else {
console.error(`买卖点 #${i} 缺少时间属性`);
}
}
};
// 执行格式检查
checkDataFormat();
// 对买卖点按时间排序,用于后续优化显示
const sortedPoints = [...tradePointsData].sort((a, b) => {
return new Date(a.time) - new Date(b.time);
});
// 记录已处理的时间点 - 按类型分开计数
const processedTimes = {};
// 创建买卖点标记系列
const buyMarkers = [];
const sellMarkers = [];
// 计数器,追踪成功和失败的处理次数
let successCount = 0;
let errorCount = 0;
sortedPoints.forEach(function(point, index) {
try {
// 检查所有必要的属性是否存在且有效
if (!point.time || !point.price || point.type === undefined) {
console.error(`买卖点 #${index} 数据不完整:`, point);
errorCount++;
return;
}
const time = Math.floor(new Date(point.time).getTime() / 1000);
const price = parseFloat(point.price);
const type = parseInt(point.type);
if (isNaN(time) || isNaN(price) || isNaN(type)) {
console.error(`买卖点 #${index} 数据格式错误:`,
{ time: isNaN(time), price: isNaN(price), type: isNaN(type) }, point);
errorCount++;
return;
}
// 获取买卖点样式
const style = TRADE_POINT_STYLE[type] || {
color: '#999999',
shape: 'circle',
text: '?',
size: 1
};
// 初始化该时间点的类型计数器
if (!processedTimes[time]) {
processedTimes[time] = {};
}
// 优化:检查是否有相同时间点和相同类型的标记,如果有,进行类型内的偏移
let stackIndex = 0;
if (processedTimes[time][type]) {
// 已经有相同时间和类型的标记,记录堆叠索引
stackIndex = processedTimes[time][type];
processedTimes[time][type]++;
} else {
// 第一次出现这个时间点的这个类型
processedTimes[time][type] = 1;
}
// 为不同类型的买卖点获取基础垂直偏移系数
const baseOffset = TRADE_POINT_OFFSET[type] || 0;
// 创建标记对象,包含额外的信息用于悬停提示
const marker = {
time: time,
position: 'inBar', // 改为在K线内部显示,不影响数据
color: style.color,
shape: style.shape,
text: style.text,
size: style.size,
// 记录堆叠索引
stackIndex: stackIndex,
// 添加悬停提示的数据
tooltip: `<span class="${type > 0 ? 'buy-point' : 'sell-point'}">${point.desc || (type > 0 ? '买点' : '卖点')}</span><br>
时间: ${formatTime(point.time)}<br>
价格: ${price.toFixed(2)}`,
// 额外添加基础类型偏移
baseOffset: baseOffset,
// 添加边框
borderColor: 'white',
borderWidth: 1,
// 添加价格偏移系数
pricePercentOffset: PRICE_PERCENT_OFFSET[type] || 0,
// 保存实际价格用于计算
price: price,
// 保存类型
type: type
};
// 区分买卖点
if (type > 0) {
buyMarkers.push(marker);
} else {
sellMarkers.push(marker);
}
successCount++;
} catch (e) {
console.error(`处理买卖点 #${index} 出错:`, e, point);
errorCount++;
}
});
console.log(`买卖点处理完成: 成功=${successCount}, 失败=${errorCount}, 买点=${buyMarkers.length}, 卖点=${sellMarkers.length}`);
// 分别添加买卖点标记
if (buyMarkers.length > 0) {
const buyMarkersSeries = mainChart.addLineSeries({
lastValueVisible: false,
priceLineVisible: false,
lineVisible: false,
title: '买点'
});
// 设置临时的数据点
buyMarkersSeries.setData([{ time: buyMarkers[0].time, value: 0 }]);
try {
// 设置买点标记,并添加偏移
buyMarkersSeries.setMarkers(
buyMarkers.map(marker => {
// 使用固定偏移而非百分比
const fixedOffset = PRICE_FIXED_OFFSET[marker.type] || 20;
// 为同一时间点的多个买点额外增加堆叠偏移
const stackOffset = marker.stackIndex > 0 ?
10 * marker.stackIndex : 0;
// 使用实际价格位置添加标记,不使用偏移
return {
...marker,
position: 'aboveBar', // 显示在K线上方
// 使用原始价格
price: marker.price
};
})
);
console.log(`成功添加 ${buyMarkers.length} 个买点标记`);
} catch (e) {
console.error("设置买点标记时出错:", e);
}
}
if (sellMarkers.length > 0) {
const sellMarkersSeries = mainChart.addLineSeries({
lastValueVisible: false,
priceLineVisible: false,
lineVisible: false,
title: '卖点'
});
// 设置临时的数据点
sellMarkersSeries.setData([{ time: sellMarkers[0].time, value: 0 }]);
try {
// 设置卖点标记,并添加偏移
sellMarkersSeries.setMarkers(
sellMarkers.map(marker => {
// 使用固定偏移而非百分比
const fixedOffset = PRICE_FIXED_OFFSET[marker.type] || 20;
// 为同一时间点的多个卖点额外增加堆叠偏移
const stackOffset = marker.stackIndex > 0 ?
10 * marker.stackIndex : 0;
// 使用实际价格位置添加标记,不使用偏移
return {
...marker,
position: 'belowBar', // 显示在K线下方
// 使用原始价格
price: marker.price
};
})
);
console.log(`成功添加 ${sellMarkers.length} 个卖点标记`);
} catch (e) {
console.error("设置卖点标记时出错:", e);
}
}
}
}
function updateTables(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, uncompletedSegData;
if (useElementPeriod && data.element_seg_list && data.element_seg_list.length > 0) {
segData = data.element_seg_list;
uncompletedSegData = data.element_uncompleted_seg_list || [];
segSource = '小周期';
} else {
segData = data.seg_list;
uncompletedSegData = data.uncompleted_seg_list || [];
segSource = '主周期';
}
// 合并已完成和未完成的线段数据
let allSegData = [];
if (segData && segData.length > 0) {
allSegData = allSegData.concat(segData.map(seg => ({...seg, status: '已完成'})));
}
if (uncompletedSegData && uncompletedSegData.length > 0) {
allSegData = allSegData.concat(uncompletedSegData.map(seg => ({...seg, status: '未完成'})));
}
console.log(`表格显示${segSource}线段数据,已完成${segData ? segData.length : 0}条,未完成${uncompletedSegData ? uncompletedSegData.length : 0}条,总计${allSegData.length}条`);
tables.seg = $('#segTable').DataTable({
data: allSegData,
order: [[0, 'desc']],
pageLength: 25,
columns: [
{ data: 'start_time', render: formatTime },
{ data: 'end_time', render: function(data, type, row) {
if (data === null || data === undefined) {
return type === 'display' ? '<span style="color: red;">未完成</span>' : '';
}
return formatTime(data, type, row);
}},
{ data: 'sure_time', render: formatConfirmTime },
{ data: 'start_price', render: formatPrice },
{ data: 'end_price', render: function(data, type, row) {
if (data === null || data === undefined) {
return type === 'display' ? '<span style="color: red;">未完成</span>' : '';
}
return formatPrice(data, type, row);
}},
{ data: 'direction', render: formatDirection },
{ data: 'status', render: function(data, type, row) {
if (type === 'display') {
const color = data === '已完成' ? 'green' : 'red';
return `<span style="color: ${color}; font-weight: bold;">${data}</span>`;
}
return data;
}}
]
});
// 中枢数据表更新
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.atrChart = null;
tvWidget.macdChart = null;
// 重置系列数据
tvWidget.series = {
candleSeries: null,
lineSeries: null,
volumeSeries: null,
atrLineSeries: null,
macdLineSeries: null,
signalLineSeries: null,
histogramSeries: null,
mainBiSeries: [],
mainSegSeries: [],
mainZsSeries: [],
mainUncompletedZsSeries: [],
elementBiSeries: [],
elementSegSeries: [],
elementZsSeries: [],
elementUncompletedZsSeries: [],
tradePointSeries: [],
mainBollingerSeries: [],
elementBollingerSeries: [],
maSeries: [], // 添加均线系列
bbSeries: [] // 添加布林带系列
};
} 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 existingAtrLines = document.querySelectorAll('.atr-crosshair-line');
existingAtrLines.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 = $('#showMacd').is(':checked');
const showOriginalKline = $('#showOriginalKline').is(':checked');
const showBi = $('#showMainBi').is(':checked');
const showSeg = $('#showMainSeg').is(':checked');
const showZs = $('#showMainZs').is(':checked');
// 保存原始K线和MACD数据,确保即使使用小级别,我们依然使用主周期的K线和MACD数据
const originalKlineData = currentData.kline_data;
const originalMacdData = currentData.macd;
// 重新初始化图表 - 这将清除所有系列并重新创建
console.log('重新初始化图表...');
// 临时存储currentData中可能被修改的字段
const tempCurrentData = {
kline_data: originalKlineData,
macd: originalMacdData
};
// 在重绘前确保K线和MACD数据不变
if (currentData.original_kline_data && currentData.original_macd) {
// 如果已经保存了原始数据,恢复它们
currentData.kline_data = currentData.original_kline_data;
currentData.macd = currentData.original_macd;
} else {
// 首次运行 - 保存原始数据
currentData.original_kline_data = originalKlineData;
currentData.original_macd = originalMacdData;
}
// 调用初始化函数
initTradingView($('#symbol').val(), $('#timeframe').val());
// 恢复原始可见范围
setTimeout(() => {
if (tvWidget && tvWidget.mainChart) {
console.log('恢复图表可见范围...');
if (logicalRange) {
tvWidget.mainChart.timeScale().setVisibleLogicalRange(logicalRange);
if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleLogicalRange(logicalRange);
if (tvWidget.atrChart) tvWidget.atrChart.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.atrChart) tvWidget.atrChart.timeScale().setVisibleRange(visibleRange);
if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleRange(visibleRange);
}
console.log('图表可见范围已恢复(包括ATR图表)');
} 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': $('#showMacd').is(':checked'),
'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 replayStepData = 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,
need_replay_data: true // 请求逐步计算的回放数据
},
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.replay_data) {
console.log('使用新的逐步计算回放数据');
replayStepData = replayData.replay_data; // 存储逐步计算的数据
totalReplaySteps = Object.keys(replayStepData).length;
if (totalReplaySteps === 0) {
$('#replayStatus').text('没有可回放的数据');
$('#replayProgress').hide();
$('#startReplay').show();
$('#stopReplay').hide();
return;
}
} else if (replayData && replayData.kline_data && replayData.kline_data.length > 0) {
console.log('使用传统回放数据结构');
replayStepData = null; // 标记使用传统方式
totalReplaySteps = replayData.kline_data.length;
} else {
$('#replayStatus').text('没有可回放的数据');
$('#replayProgress').hide();
$('#startReplay').show();
$('#stopReplay').hide();
return;
}
// 设置回放变量
isReplaying = true;
currentReplayIndex = 0;
// 更新回放状态
$('#replayStatus').text(`准备回放 (0/${totalReplaySteps})`);
$('.progress-bar').css('width', '0%');
// 准备初始数据
prepareInitialReplayData();
// 开始回放
continueDataReplay();
}
// 准备初始回放数据
function prepareInitialReplayData() {
let initialData;
// 检查是否使用新的逐步计算回放数据
if (replayStepData) {
// 使用第一步的数据作为初始数据
initialData = $.extend(true, {}, replayStepData[0]);
console.log('使用逐步计算的初始数据');
} else {
// 使用传统方式,创建初始数据集,只包含第一根K线
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.trade_points = [];
initialData.element_trade_points = [];
initialData.macd_divergence = [];
initialData.klc_fx_type = [];
// 清空分型相关数据
initialData.klc_fx_info = [];
initialData.klu_fx_info = [];
initialData.element_klc_fx_info = [];
initialData.element_klu_fx_info = [];
// 初始化MACD数据
if (initialData.macd && typeof initialData.macd === 'object') {
// MACD数据是对象结构 {macd: [], signal: [], histogram: []}
if (initialData.macd.macd && initialData.macd.macd.length > 0) {
initialData.macd = {
macd: [initialData.macd.macd[0] || 0],
signal: [initialData.macd.signal[0] || 0],
histogram: [initialData.macd.histogram[0] || 0]
};
} else {
// 如果没有MACD数据,创建默认值
initialData.macd = {
macd: [0],
signal: [0],
histogram: [0]
};
}
} else {
// 如果MACD数据结构不正确,创建默认值
initialData.macd = {
macd: [0],
signal: [0],
histogram: [0]
};
}
console.log('使用传统方式生成初始数据');
}
// 更新图表,固定坐标轴范围
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);
}
}
// 输出调试信息,确认数据已正确清空
console.log('回放初始化完成,数据统计:');
console.log('K线数据:', initialData.kline_data ? initialData.kline_data.length : 0, '条');
console.log('笔数据:', initialData.bi_list ? initialData.bi_list.length : 0, '条');
console.log('线段数据:', initialData.seg_list ? initialData.seg_list.length : 0, '条');
console.log('中枢数据:', initialData.zs_list ? initialData.zs_list.length : 0, '条');
console.log('KLC分型数据:', initialData.klc_fx_info ? initialData.klc_fx_info.length : 0, '条');
console.log('KLU分型数据:', initialData.klu_fx_info ? initialData.klu_fx_info.length : 0, '条');
console.log('买卖点数据:', initialData.trade_points ? initialData.trade_points.length : 0, '条');
// 刷新图表
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();
let subsetData;
// 检查是否使用新的逐步计算数据
if (replayStepData) {
// 直接使用预计算的数据
subsetData = $.extend(true, {}, replayStepData[currentReplayIndex - 1]);
console.log(`回放步骤 ${currentReplayIndex}/${totalReplaySteps} (使用预计算数据)`);
} else {
// 使用传统方式创建数据子集
subsetData = createDataSubset(currentReplayIndex);
console.log(`回放步骤 ${currentReplayIndex}/${totalReplaySteps} (传统方式)`);
}
// 输出回放进度调试信息
if (currentReplayIndex % 10 === 0 || currentReplayIndex <= 5) { // 每10步输出一次,或前5步
console.log('K线数据:', subsetData.kline_data ? subsetData.kline_data.length : 0, '条');
console.log('笔数据:', subsetData.bi_list ? subsetData.bi_list.length : 0, '条');
console.log('KLC分型:', subsetData.klc_fx_info ? subsetData.klc_fx_info.length : 0, '条');
console.log('KLU分型:', subsetData.klu_fx_info ? subsetData.klu_fx_info.length : 0, '条');
}
// 更新图表
currentData = subsetData;
refreshChart(subsetData);
// 固定Y轴范围
if (!replayStepData) { // 只有传统方式需要固定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 && typeof subsetData.macd === 'object') {
if (replayData.macd.macd && replayData.macd.macd.length > 0) {
subsetData.macd = {
macd: replayData.macd.macd.slice(0, endIndex),
signal: replayData.macd.signal.slice(0, endIndex),
histogram: replayData.macd.histogram.slice(0, endIndex)
};
// 验证MACD数据
['macd', 'signal', 'histogram'].forEach(field => {
subsetData.macd[field].forEach((value, index) => {
if (value === null || value === undefined || isNaN(value)) {
console.warn(`MACD ${field}[${index}]数据无效,设置为0`);
subsetData.macd[field][index] = 0;
}
});
});
} else {
// 如果没有有效的MACD数据,创建默认数组
subsetData.macd = {
macd: new Array(endIndex).fill(0),
signal: new Array(endIndex).fill(0),
histogram: new Array(endIndex).fill(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');
}
// 过滤分型信息数据
if (subsetData.klc_fx_info && subsetData.klc_fx_info.length) {
subsetData.klc_fx_info = filterDataByTime(replayData.klc_fx_info, currentEndTime, 'time');
}
if (subsetData.klu_fx_info && subsetData.klu_fx_info.length) {
subsetData.klu_fx_info = filterDataByTime(replayData.klu_fx_info, currentEndTime, 'time');
}
if (subsetData.element_klc_fx_info && subsetData.element_klc_fx_info.length) {
subsetData.element_klc_fx_info = filterDataByTime(replayData.element_klc_fx_info, currentEndTime, 'time');
}
if (subsetData.element_klu_fx_info && subsetData.element_klu_fx_info.length) {
subsetData.element_klu_fx_info = filterDataByTime(replayData.element_klu_fx_info, currentEndTime, 'time');
}
// 过滤买卖点数据
if (subsetData.trade_points && subsetData.trade_points.length) {
subsetData.trade_points = filterDataByTime(replayData.trade_points, currentEndTime, 'time');
}
if (subsetData.element_trade_points && subsetData.element_trade_points.length) {
subsetData.element_trade_points = filterDataByTime(replayData.element_trade_points, 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;
replayStepData = 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);
});
}
// 均线系统全局变量
let movingAverages = []; // 存储所有均线配置
let maIdCounter = 0; // 均线ID计数器
// 布林带系统全局变量
let bollingerBands = []; // 存储所有布林带配置
let bbIdCounter = 0; // 布林带ID计数器
// 获取随机颜色
function getRandomColor() {
const colors = ['#2962FF', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4',
'#FECA57', '#48CAE4', '#F38BA8', '#A8DADC', '#F1C0E8'];
return colors[Math.floor(Math.random() * colors.length)];
}
// 显示布林带配置弹窗
function showBBConfig() {
console.log('📂 显示布林带配置弹窗,设置为添加模式');
$('#bbConfigModal').css('display', 'flex');
// 重置表单
$('#bbType').val('Bollinger Bands');
$('#bbLength').val(20);
$('#bbStdDev').val(2);
$('#bbSource').val('close');
$('#bbLineWidth').val(2);
$('#bbLineStyle').val(0);
$('#bbColor').val(getRandomColor());
// 移除所有现有的点击事件,避免事件冲突
$('#bbConfigSubmitBtn').off('click');
// 设置按钮为添加模式
$('#bbConfigSubmitBtn').text('添加').on('click', function(e) {
e.preventDefault();
e.stopPropagation();
console.log('🖱️ 点击了添加按钮(来自showBBConfig),准备添加新布林带');
addBollingerBand();
});
console.log('🔄 按钮已设置为添加模式');
// 更新预览
setTimeout(updateBBLinePreview, 50);
}
// 显示均线配置弹窗
function showMAConfig() {
console.log('📂 显示均线配置弹窗,设置为添加模式');
$('#maConfigModal').css('display', 'flex');
// 重置表单
$('#maType').val('SMA');
$('#maLength').val(20);
$('#maSource').val('close');
$('#maSmoothType').val('none');
$('#maSmoothLength').val(3).prop('disabled', true);
$('#maLineWidth').val(2);
$('#maLineStyle').val(0);
$('#maColor').val(getRandomColor());
// 移除所有现有的点击事件,避免事件冲突
$('#maConfigSubmitBtn').off('click');
// 设置按钮为添加模式
$('#maConfigSubmitBtn').text('添加').on('click', function(e) {
e.preventDefault();
e.stopPropagation();
console.log('🖱️ 点击了添加按钮(来自showMAConfig),准备添加新均线');
addMovingAverage();
});
console.log('🔄 按钮已设置为添加模式');
// 更新预览
setTimeout(updateLinePreview, 50);
}
// 隐藏均线配置弹窗
function hideMAConfig() {
console.log('🔒 关闭均线配置弹窗');
$('#maConfigModal').css('display', 'none');
// 移除所有现有的点击事件
$('#maConfigSubmitBtn').off('click');
// 恢复按钮为添加模式 - 但不立即绑定事件,防止意外触发
$('#maConfigSubmitBtn').text('添加');
console.log('🔄 按钮已重置为添加模式,等待用户主动点击"添加均线"按钮');
}
// 添加均线
function addMovingAverage() {
console.log('🚨🚨🚨 警告:addMovingAverage函数被调用了!!!');
console.log('📊 添加前均线配置数量:', movingAverages.length);
console.log('🔍 调用堆栈:', new Error().stack);
const config = {
id: ++maIdCounter,
type: $('#maType').val(),
length: parseInt($('#maLength').val()),
source: $('#maSource').val(),
smoothType: $('#maSmoothType').val(),
smoothLength: parseInt($('#maSmoothLength').val()),
lineWidth: parseInt($('#maLineWidth').val()),
lineStyle: parseInt($('#maLineStyle').val()),
color: $('#maColor').val(),
visible: true
};
console.log('📝 新均线配置:', {
id: config.id,
type: config.type,
length: config.length,
source: config.source,
color: config.color
});
// 验证输入
if (config.length < 1) {
alert('均线长度必须大于0');
return;
}
if (config.smoothType !== 'none' && (config.smoothLength < 1 || config.smoothLength > 50)) {
alert('平滑长度必须在1-50之间');
return;
}
movingAverages.push(config);
console.log('📊 添加后均线配置数量:', movingAverages.length);
hideMAConfig();
// 直接使用前端数据计算和添加均线,不请求后台
if (tvWidget.mainChart && currentData && currentData.kline_data) {
const candles = getCurrentCandleData();
addMovingAveragesToChart(candles);
}
}
// 计算均线数据
function calculateMA(data, type, length, source) {
if (!data || data.length < length) return [];
const sourceData = data.map(candle => {
switch(source) {
case 'open': return candle.open;
case 'high': return candle.high;
case 'low': return candle.low;
case 'close': return candle.close;
case 'hl2': return (candle.high + candle.low) / 2;
case 'hlc3': return (candle.high + candle.low + candle.close) / 3;
case 'ohlc4': return (candle.open + candle.high + candle.low + candle.close) / 4;
default: return candle.close;
}
});
const result = [];
for (let i = length - 1; i < sourceData.length; i++) {
let value;
switch(type) {
case 'SMA':
value = sourceData.slice(i - length + 1, i + 1)
.reduce((sum, val) => sum + val, 0) / length;
break;
case 'EMA':
const multiplier = 2 / (length + 1);
if (result.length === 0) {
// 第一个EMA值使用SMA作为种子值
value = sourceData.slice(i - length + 1, i + 1)
.reduce((sum, val) => sum + val, 0) / length;
} else {
// 后续EMA值使用标准公式
value = sourceData[i] * multiplier + result[result.length - 1].value * (1 - multiplier);
}
break;
case 'WMA':
let weightSum = 0;
let valueSum = 0;
for (let j = 0; j < length; j++) {
const weight = j + 1;
weightSum += weight;
valueSum += sourceData[i - length + 1 + j] * weight;
}
value = valueSum / weightSum;
break;
default:
value = sourceData[i];
}
result.push({
time: data[i].time,
value: value
});
}
return result;
}
// 应用平滑处理
function applySmoothToMA(maData, smoothType, smoothLength) {
if (smoothType === 'none' || maData.length < smoothLength) {
return maData;
}
const result = [];
for (let i = smoothLength - 1; i < maData.length; i++) {
let value;
switch(smoothType) {
case 'SMA':
value = maData.slice(i - smoothLength + 1, i + 1)
.reduce((sum, item) => sum + item.value, 0) / smoothLength;
break;
case 'EMA':
const multiplier = 2 / (smoothLength + 1);
if (result.length === 0) {
// 第一个EMA值使用SMA作为种子值
value = maData.slice(i - smoothLength + 1, i + 1)
.reduce((sum, item) => sum + item.value, 0) / smoothLength;
} else {
// 后续EMA值使用标准公式
value = maData[i].value * multiplier + result[result.length - 1].value * (1 - multiplier);
}
break;
case 'WMA':
let weightSum = 0;
let valueSum = 0;
for (let j = 0; j < smoothLength; j++) {
const weight = j + 1;
weightSum += weight;
valueSum += maData[i - smoothLength + 1 + j].value * weight;
}
value = valueSum / weightSum;
break;
default:
value = maData[i].value;
}
result.push({
time: maData[i].time,
value: value
});
}
return result;
}
// 更新技术指标面板(统一面板)
function updateIndicatorPanel() {
const panel = $('#indicatorPanel');
if (movingAverages.length === 0 && bollingerBands.length === 0) {
panel.hide();
return;
}
let html = '';
let hasVisibleIndicator = false;
// 添加均线指标
movingAverages.forEach(ma => {
// 获取最新价格
const latestValue = ma.data && ma.data.length > 0 ?
ma.data[ma.data.length - 1].value.toFixed(2) : '--';
const smoothText = ma.smoothType !== 'none' ?
`, ${ma.smoothType}(${ma.smoothLength})` : '';
// 获取线条样式描述
const lineStyleNames = ['实线', '点线', '虚线', '大虚线'];
const styleText = ma.lineWidth && ma.lineStyle !== undefined ?
` ${ma.lineWidth}px ${lineStyleNames[ma.lineStyle] || '实线'}` : '';
// 显示所有均线(包括隐藏的),但用不同样式区分
const itemStyle = ma.visible ? '' : 'opacity: 0.5;';
const eyeIcon = ma.visible ? 'bi-eye' : 'bi-eye-slash';
html += `
<div class="indicator-item" data-indicator-type="ma" data-indicator-id="${ma.id}" style="${itemStyle}">
<div class="indicator-info">
<div class="indicator-color-indicator" style="background-color: ${ma.color}"></div>
<span class="indicator-label">${ma.type}(${ma.length})${smoothText}${styleText}</span>
<span class="indicator-value">${ma.visible ? latestValue : '--'}</span>
</div>
<div class="indicator-actions">
<button class="indicator-action-btn" onclick="toggleMAVisibility('${ma.id}')" title="${ma.visible ? '隐藏' : '显示'}">
<i class="bi ${eyeIcon}"></i>
</button>
<button class="indicator-action-btn" onclick="configMA('${ma.id}')" title="配置">
<i class="bi bi-gear"></i>
</button>
<button class="indicator-action-btn" onclick="deleteMA('${ma.id}')" title="删除">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
`;
if (ma.visible) {
hasVisibleIndicator = true;
}
});
// 添加布林带指标
bollingerBands.forEach(bb => {
// 获取最新价格
const latestValue = bb.data && bb.data.length > 0 ?
bb.data[bb.data.length - 1].middle.toFixed(2) : '--';
// 获取线条样式描述
const lineStyleNames = ['实线', '点线', '虚线', '大虚线'];
const styleText = bb.lineWidth && bb.lineStyle !== undefined ?
` ${bb.lineWidth}px ${lineStyleNames[bb.lineStyle] || '实线'}` : '';
// 显示所有布林带(包括隐藏的),但用不同样式区分
const itemStyle = bb.visible ? '' : 'opacity: 0.5;';
const eyeIcon = bb.visible ? 'bi-eye' : 'bi-eye-slash';
// 创建渐变颜色指示器显示三种颜色
const colorIndicatorStyle = `
background: linear-gradient(90deg,
${bb.upperColor || '#ff6b6b'} 0%,
${bb.middleColor || '#667eea'} 50%,
${bb.lowerColor || '#4ecdc4'} 100%);
border-radius: 2px;
`;
html += `
<div class="indicator-item" data-indicator-type="bb" data-indicator-id="${bb.id}" style="${itemStyle}">
<div class="indicator-info">
<div class="indicator-color-indicator" style="${colorIndicatorStyle}"></div>
<span class="indicator-label">BB(${bb.length}, ${bb.upperMultiplier || 2}, ${bb.lowerMultiplier || 2})${styleText}</span>
<span class="indicator-value">${bb.visible ? latestValue : '--'}</span>
</div>
<div class="indicator-actions">
<button class="indicator-action-btn" onclick="toggleBBVisibility('${bb.id}')" title="${bb.visible ? '隐藏' : '显示'}">
<i class="bi ${eyeIcon}"></i>
</button>
<button class="indicator-action-btn" onclick="configBB('${bb.id}')" title="配置">
<i class="bi bi-gear"></i>
</button>
<button class="indicator-action-btn" onclick="deleteBB('${bb.id}')" title="删除">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
`;
if (bb.visible) {
hasVisibleIndicator = true;
}
});
panel.html(html);
if (hasVisibleIndicator || movingAverages.length > 0 || bollingerBands.length > 0) {
panel.show();
} else {
panel.hide();
}
}
// 更新均线面板(兼容旧代码)
function updateMAPanel() {
updateIndicatorPanel();
}
// 切换均线可见性
function toggleMAVisibility(maId) {
console.log('👁️ 切换均线可见性,ID:', maId, 'Type:', typeof maId);
const ma = movingAverages.find(m => m.id == maId); // 使用==而不是===来兼容类型转换
if (ma) {
console.log('📝 找到均线,当前可见性:', ma.visible);
ma.visible = !ma.visible;
console.log('✅ 切换后可见性:', ma.visible);
// 直接使用前端数据重新计算均线
if (tvWidget.mainChart && currentData && currentData.kline_data) {
const candles = getCurrentCandleData();
addMovingAveragesToChart(candles);
}
} else {
console.error('❌ 未找到要切换可见性的均线,ID:', maId);
}
}
// 配置均线
function configMA(maId) {
console.log('⚙️ 开始配置均线,ID:', maId, 'Type:', typeof maId);
console.log('📊 当前所有均线ID:', movingAverages.map(m => ({id: m.id, type: typeof m.id})));
const ma = movingAverages.find(m => m.id == maId); // 使用==而不是===来兼容类型转换
if (!ma) {
console.error('❌ 未找到要配置的均线,ID:', maId);
return;
}
console.log('📝 找到要配置的均线:', ma.type, ma.length, ma.color);
// 填充表单
$('#maType').val(ma.type);
$('#maLength').val(ma.length);
$('#maSource').val(ma.source);
$('#maSmoothType').val(ma.smoothType);
$('#maSmoothLength').val(ma.smoothLength);
$('#maLineWidth').val(ma.lineWidth || 2);
$('#maLineStyle').val(ma.lineStyle || 0);
$('#maColor').val(ma.color);
// 启用/禁用平滑长度输入
$('#maSmoothLength').prop('disabled', ma.smoothType === 'none');
// 移除所有现有的点击事件,避免事件冲突
$('#maConfigSubmitBtn').off('click');
// 修改按钮为更新模式
$('#maConfigSubmitBtn').text('更新').on('click', (function(capturedMaId) {
return function(e) {
e.preventDefault();
e.stopPropagation();
console.log('🖱️ 点击了更新按钮,准备更新ID:', capturedMaId);
console.log('🔍 捕获的ID类型:', typeof capturedMaId);
updateMovingAverage(capturedMaId);
};
})(maId));
console.log('🔄 按钮已切换为更新模式');
$('#maConfigModal').css('display', 'flex');
// 更新预览
setTimeout(updateLinePreview, 50);
}
// 更新均线
function updateMovingAverage(maId) {
console.log('🔄 开始更新均线,ID:', maId, 'Type:', typeof maId);
console.log('📊 更新前均线配置数量:', movingAverages.length);
console.log('📊 当前所有均线:', movingAverages.map(m => ({id: m.id, type: typeof m.id, maType: m.type, length: m.length, color: m.color})));
const ma = movingAverages.find(m => m.id == maId); // 使用==而不是===来兼容类型转换
if (!ma) {
console.error('❌ 未找到要更新的均线配置,ID:', maId);
console.error('❌ 可用的均线ID:', movingAverages.map(m => m.id));
alert('错误:未找到要更新的均线配置!');
return;
}
console.log('📝 找到要更新的均线:', ma.type, ma.length, ma.color);
// 验证输入
const newLength = parseInt($('#maLength').val());
const newSmoothLength = parseInt($('#maSmoothLength').val());
const newSmoothType = $('#maSmoothType').val();
if (newLength < 1) {
alert('均线长度必须大于0');
return;
}
if (newSmoothType !== 'none' && (newSmoothLength < 1 || newSmoothLength > 50)) {
alert('平滑长度必须在1-50之间');
return;
}
// 记录更新前的配置
console.log('🔧 更新前配置:', {
type: ma.type,
length: ma.length,
source: ma.source,
color: ma.color
});
// 更新配置
ma.type = $('#maType').val();
ma.length = newLength;
ma.source = $('#maSource').val();
ma.smoothType = newSmoothType;
ma.smoothLength = newSmoothLength;
ma.lineWidth = parseInt($('#maLineWidth').val());
ma.lineStyle = parseInt($('#maLineStyle').val());
ma.color = $('#maColor').val();
// 记录更新后的配置
console.log('✅ 更新后配置:', {
type: ma.type,
length: ma.length,
source: ma.source,
color: ma.color
});
console.log('📊 更新后均线配置数量:', movingAverages.length);
hideMAConfig();
// 直接使用前端数据重新计算均线
if (tvWidget.mainChart && currentData && currentData.kline_data) {
const candles = getCurrentCandleData();
addMovingAveragesToChart(candles);
}
}
// 删除均线
function deleteMA(maId) {
console.log('🗑️ 删除均线,ID:', maId, 'Type:', typeof maId);
console.log('📊 删除前均线配置数量:', movingAverages.length);
const beforeCount = movingAverages.length;
movingAverages = movingAverages.filter(m => m.id != maId); // 使用!=而不是!==来兼容类型转换
console.log('📊 删除后均线配置数量:', movingAverages.length);
console.log('✅ 是否成功删除:', beforeCount > movingAverages.length);
// 直接使用前端数据重新计算均线
if (tvWidget.mainChart && currentData && currentData.kline_data) {
const candles = getCurrentCandleData();
addMovingAveragesToChart(candles);
}
}
// 获取当前K线数据的辅助函数
function getCurrentCandleData() {
if (!currentData || !currentData.kline_data) {
return [];
}
// 检查是否使用小周期数据
const useElementPeriod = $('#elementPeriodKline').is(':checked') &&
currentData.element_kline_data &&
Array.isArray(currentData.element_kline_data);
let candles = [];
if (useElementPeriod) {
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 {
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),
};
});
}
return candles;
}
// 添加均线到图表
function addMovingAveragesToChart(candleData) {
if (!tvWidget.mainChart || !candleData || candleData.length === 0) {
return;
}
console.log('🔄 重新绘制均线,当前均线配置数量:', movingAverages.length);
// 强制清除现有的均线系列
if (tvWidget.series.maSeries && tvWidget.series.maSeries.length > 0) {
console.log('🗑️ 清除现有均线系列:', tvWidget.series.maSeries.length);
tvWidget.series.maSeries.forEach((series, index) => {
try {
tvWidget.mainChart.removeSeries(series);
console.log('✅ 移除均线系列:', index);
} catch (e) {
console.error('❌ 移除均线系列失败:', index, e);
}
});
}
// 重置均线系列数组
tvWidget.series.maSeries = [];
// 为每个均线配置创建系列
let addedCount = 0;
movingAverages.forEach((maConfig, index) => {
if (!maConfig.visible) {
console.log(`⏭️ 跳过隐藏的均线 [${index}]:`, maConfig.type, maConfig.length);
return;
}
try {
console.log(`📈 开始创建均线 [${index}]:`, maConfig.type, maConfig.length, maConfig.color);
// 计算均线数据
const maData = calculateMA(candleData, maConfig.type, maConfig.length, maConfig.source);
// 应用平滑处理
const smoothedData = maConfig.smoothType !== 'none' ?
applySmoothToMA(maData, maConfig.smoothType, maConfig.smoothLength) :
maData;
// 创建线系列
const maSeries = tvWidget.mainChart.addLineSeries({
color: maConfig.color,
lineWidth: maConfig.lineWidth || 2,
lineStyle: maConfig.lineStyle || 0, // 0=实线, 1=点线, 2=虚线, 3=大虚线
title: `${maConfig.type}(${maConfig.length})`,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: true,
});
// 设置数据
maSeries.setData(smoothedData);
// 保存数据到配置中以便显示最新值
maConfig.data = smoothedData;
// 保存系列引用
tvWidget.series.maSeries.push(maSeries);
addedCount++;
console.log(`✅ 均线创建成功 [${index}]:`, maConfig.type, maConfig.length, '数据点:', smoothedData.length);
} catch (e) {
console.error('❌ 添加均线时出错:', e, maConfig);
}
});
console.log(`🎯 均线添加完成,共添加 ${addedCount} 条均线`);
console.log('📊 当前图表中的均线系列数量:', tvWidget.series.maSeries.length);
// 更新技术指标面板
updateIndicatorPanel();
}
// 监听平滑类型变化
$(document).on('change', '#maSmoothType', function() {
const isNone = $(this).val() === 'none';
$('#maSmoothLength').prop('disabled', isNone);
});
// 显示布林带配置弹窗
function showBBConfig() {
console.log('📂 显示布林带配置弹窗,设置为添加模式');
$('#bbConfigModal').css('display', 'flex');
// 重置表单
$('#bbType').val('Bollinger Bands');
$('#bbLength').val(20);
$('#bbStdDev').val(2);
$('#bbSource').val('close');
$('#bbLineWidth').val(2);
$('#bbLineStyle').val(0);
$('#bbColor').val(getRandomColor());
// 移除所有现有的点击事件,避免事件冲突
$('#bbConfigSubmitBtn').off('click');
// 设置按钮为添加模式
$('#bbConfigSubmitBtn').text('添加').on('click', function(e) {
e.preventDefault();
e.stopPropagation();
console.log('🖱️ 点击了添加按钮(来自showBBConfig),准备添加新布林带');
addBollingerBand();
});
console.log('🔄 按钮已设置为添加模式');
// 更新预览
setTimeout(updateBBLinePreview, 50);
}
// 隐藏布林带配置弹窗
function hideBBConfig() {
console.log('🔒 关闭布林带配置弹窗');
$('#bbConfigModal').css('display', 'none');
// 移除所有现有的点击事件
$('#bbConfigSubmitBtn').off('click');
// 恢复按钮为添加模式 - 但不立即绑定事件,防止意外触发
$('#bbConfigSubmitBtn').text('添加');
console.log('🔄 按钮已重置为添加模式,等待用户主动点击"添加布林带"按钮');
}
// 添加布林带
function addBollingerBand() {
console.log('🚨🚨🚨 警告:addBollingerBand函数被调用了!!!');
console.log('📊 添加前布林带配置数量:', bollingerBands.length);
console.log('🔍 调用堆栈:', new Error().stack);
const config = {
id: ++bbIdCounter,
type: $('#bbType').val(),
length: parseInt($('#bbLength').val()),
upperMultiplier: parseFloat($('#bbUpperMultiplier').val()),
lowerMultiplier: parseFloat($('#bbLowerMultiplier').val()),
source: $('#bbSource').val(),
lineWidth: parseInt($('#bbLineWidth').val()),
lineStyle: parseInt($('#bbLineStyle').val()),
upperColor: $('#bbUpperColor').val(),
middleColor: $('#bbMiddleColor').val(),
lowerColor: $('#bbLowerColor').val(),
visible: true
};
console.log('📝 新布林带配置:', {
id: config.id,
type: config.type,
length: config.length,
upperMultiplier: config.upperMultiplier,
lowerMultiplier: config.lowerMultiplier,
source: config.source,
colors: {
upper: config.upperColor,
middle: config.middleColor,
lower: config.lowerColor
}
});
// 验证输入
if (config.length < 1) {
alert('布林带长度必须大于0');
return;
}
if (config.upperMultiplier < 0.1) {
alert('上轨倍数必须大于0.1');
return;
}
if (config.lowerMultiplier < 0.1) {
alert('下轨倍数必须大于0.1');
return;
}
bollingerBands.push(config);
console.log('📊 添加后布林带配置数量:', bollingerBands.length);
hideBBConfig();
// 直接使用前端数据计算和添加布林带,不请求后台
if (tvWidget.mainChart && currentData && currentData.kline_data) {
const candles = getCurrentCandleData();
addBollingerBandsToChart(candles);
addMovingAveragesToChart(candles); // 同时更新均线
}
}
// 计算布林带数据
function calculateBB(data, length, upperMultiplier, lowerMultiplier, source) {
if (!data || data.length < length) return [];
const sourceData = data.map(candle => {
switch(source) {
case 'open': return candle.open;
case 'high': return candle.high;
case 'low': return candle.low;
case 'close': return candle.close;
case 'hl2': return (candle.high + candle.low) / 2;
case 'hlc3': return (candle.high + candle.low + candle.close) / 3;
case 'ohlc4': return (candle.open + candle.high + candle.low + candle.close) / 4;
default: return candle.close;
}
});
const result = [];
for (let i = length - 1; i < sourceData.length; i++) {
const start = Math.max(0, i - length + 1);
const slice = sourceData.slice(start, i + 1);
const avg = slice.reduce((sum, val) => sum + val, 0) / length;
const std = Math.sqrt(slice.reduce((sum, val) => sum + Math.pow(val - avg, 2), 0) / length);
result.push({
time: data[i].time,
upper: avg + upperMultiplier * std,
middle: avg,
lower: avg - lowerMultiplier * std
});
}
return result;
}
// 更新布林带面板(兼容旧代码)
function updateBBPanel() {
updateIndicatorPanel();
}
// 切换布林带可见性
function toggleBBVisibility(bbId) {
console.log('👁️ 切换布林带可见性,ID:', bbId, 'Type:', typeof bbId);
const bb = bollingerBands.find(b => b.id == bbId); // 使用==而不是===来兼容类型转换
if (bb) {
console.log('📝 找到布林带,当前可见性:', bb.visible);
bb.visible = !bb.visible;
console.log('✅ 切换后可见性:', bb.visible);
// 直接使用前端数据重新计算布林带
if (tvWidget.mainChart && currentData && currentData.kline_data) {
const candles = getCurrentCandleData();
addBollingerBandsToChart(candles);
addMovingAveragesToChart(candles); // 同时更新均线
}
} else {
console.error('❌ 未找到要切换可见性的布林带,ID:', bbId);
}
}
// 配置布林带
function configBB(bbId) {
console.log('⚙️ 开始配置布林带,ID:', bbId, 'Type:', typeof bbId);
console.log('📊 当前所有布林带ID:', bollingerBands.map(b => ({id: b.id, type: typeof b.id})));
const bb = bollingerBands.find(b => b.id == bbId); // 使用==而不是===来兼容类型转换
if (!bb) {
console.error('❌ 未找到要配置的布林带,ID:', bbId);
return;
}
console.log('📝 找到要配置的布林带:', bb.type, bb.length, bb.upperMultiplier, bb.lowerMultiplier, bb.upperColor, bb.middleColor, bb.lowerColor);
// 填充表单
$('#bbType').val(bb.type);
$('#bbLength').val(bb.length);
$('#bbUpperMultiplier').val(bb.upperMultiplier || 2);
$('#bbLowerMultiplier').val(bb.lowerMultiplier || 2);
$('#bbSource').val(bb.source);
$('#bbLineWidth').val(bb.lineWidth || 2);
$('#bbLineStyle').val(bb.lineStyle || 0);
$('#bbUpperColor').val(bb.upperColor || '#ff6b6b');
$('#bbMiddleColor').val(bb.middleColor || '#667eea');
$('#bbLowerColor').val(bb.lowerColor || '#4ecdc4');
// 移除所有现有的点击事件,避免事件冲突
$('#bbConfigSubmitBtn').off('click');
// 修改按钮为更新模式
$('#bbConfigSubmitBtn').text('更新').on('click', (function(capturedBbId) {
return function(e) {
e.preventDefault();
e.stopPropagation();
console.log('🖱️ 点击了更新按钮,准备更新ID:', capturedBbId);
console.log('🔍 捕获的ID类型:', typeof capturedBbId);
updateBollingerBand(capturedBbId);
};
})(bbId));
console.log('🔄 按钮已切换为更新模式');
$('#bbConfigModal').css('display', 'flex');
// 更新预览
setTimeout(updateBBLinePreview, 50);
}
// 更新布林带
function updateBollingerBand(bbId) {
console.log('🔄 开始更新布林带,ID:', bbId, 'Type:', typeof bbId);
console.log('📊 更新前布林带配置数量:', bollingerBands.length);
console.log('📊 当前所有布林带:', bollingerBands.map(b => ({id: b.id, type: typeof b.id, bbType: b.type, length: b.length, upperMultiplier: b.upperMultiplier, lowerMultiplier: b.lowerMultiplier, upperColor: b.upperColor, middleColor: b.middleColor, lowerColor: b.lowerColor})));
const bb = bollingerBands.find(b => b.id == bbId); // 使用==而不是===来兼容类型转换
if (!bb) {
console.error('❌ 未找到要更新的布林带配置,ID:', bbId);
console.error('❌ 可用的布林带ID:', bollingerBands.map(b => b.id));
alert('错误:未找到要更新的布林带配置!');
return;
}
console.log('📝 找到要更新的布林带:', bb.type, bb.length, bb.upperMultiplier, bb.lowerMultiplier, bb.upperColor, bb.middleColor, bb.lowerColor);
// 验证输入
const newLength = parseInt($('#bbLength').val());
const newUpperMultiplier = parseFloat($('#bbUpperMultiplier').val());
const newLowerMultiplier = parseFloat($('#bbLowerMultiplier').val());
if (newLength < 1) {
alert('布林带长度必须大于0');
return;
}
if (newUpperMultiplier < 0.1) {
alert('上轨倍数必须大于0.1');
return;
}
if (newLowerMultiplier < 0.1) {
alert('下轨倍数必须大于0.1');
return;
}
// 记录更新前的配置
console.log('🔧 更新前配置:', {
type: bb.type,
length: bb.length,
upperMultiplier: bb.upperMultiplier,
lowerMultiplier: bb.lowerMultiplier,
source: bb.source,
colors: {
upper: bb.upperColor,
middle: bb.middleColor,
lower: bb.lowerColor
}
});
// 更新配置
bb.type = $('#bbType').val();
bb.length = newLength;
bb.upperMultiplier = newUpperMultiplier;
bb.lowerMultiplier = newLowerMultiplier;
bb.source = $('#bbSource').val();
bb.lineWidth = parseInt($('#bbLineWidth').val());
bb.lineStyle = parseInt($('#bbLineStyle').val());
bb.upperColor = $('#bbUpperColor').val();
bb.middleColor = $('#bbMiddleColor').val();
bb.lowerColor = $('#bbLowerColor').val();
// 记录更新后的配置
console.log('✅ 更新后配置:', {
type: bb.type,
length: bb.length,
upperMultiplier: bb.upperMultiplier,
lowerMultiplier: bb.lowerMultiplier,
source: bb.source,
colors: {
upper: bb.upperColor,
middle: bb.middleColor,
lower: bb.lowerColor
}
});
console.log('📊 更新后布林带配置数量:', bollingerBands.length);
hideBBConfig();
// 直接使用前端数据重新计算布林带
if (tvWidget.mainChart && currentData && currentData.kline_data) {
const candles = getCurrentCandleData();
addBollingerBandsToChart(candles);
}
}
// 删除布林带
function deleteBB(bbId) {
console.log('🗑️ 删除布林带,ID:', bbId, 'Type:', typeof bbId);
console.log('📊 删除前布林带配置数量:', bollingerBands.length);
const beforeCount = bollingerBands.length;
bollingerBands = bollingerBands.filter(b => b.id != bbId); // 使用!=而不是!==来兼容类型转换
console.log('📊 删除后布林带配置数量:', bollingerBands.length);
console.log('✅ 是否成功删除:', beforeCount > bollingerBands.length);
// 直接使用前端数据重新计算布林带
if (tvWidget.mainChart && currentData && currentData.kline_data) {
const candles = getCurrentCandleData();
addBollingerBandsToChart(candles);
}
}
// 添加布林带到图表
function addBollingerBandsToChart(candleData) {
if (!tvWidget.mainChart || !candleData || candleData.length === 0) {
return;
}
console.log('🔄 重新绘制布林带,当前布林带配置数量:', bollingerBands.length);
// 强制清除现有的布林带系列
if (tvWidget.series.bbSeries && tvWidget.series.bbSeries.length > 0) {
console.log('🗑️ 清除现有布林带系列:', tvWidget.series.bbSeries.length);
tvWidget.series.bbSeries.forEach((series, index) => {
try {
tvWidget.mainChart.removeSeries(series);
console.log('✅ 移除布林带系列:', index);
} catch (e) {
console.error('❌ 移除布林带系列失败:', index, e);
}
});
}
// 重置布林带系列数组
tvWidget.series.bbSeries = [];
// 为每个布林带配置创建系列
let addedCount = 0;
bollingerBands.forEach((bbConfig, index) => {
if (!bbConfig.visible) {
console.log(`⏭️ 跳过隐藏的布林带 [${index}]:`, bbConfig.type, bbConfig.length, bbConfig.upperMultiplier, bbConfig.lowerMultiplier);
return;
}
try {
console.log(`📈 开始创建布林带 [${index}]:`, bbConfig.type, bbConfig.length, bbConfig.upperMultiplier, bbConfig.lowerMultiplier, bbConfig.upperColor, bbConfig.middleColor, bbConfig.lowerColor);
// 计算布林带数据
const bbData = calculateBB(candleData, bbConfig.length, bbConfig.upperMultiplier, bbConfig.lowerMultiplier, bbConfig.source);
// 创建线系列
const upperSeries = tvWidget.mainChart.addLineSeries({
color: bbConfig.upperColor,
lineWidth: bbConfig.lineWidth || 2,
lineStyle: bbConfig.lineStyle || 0, // 0=实线, 1=点线, 2=虚线, 3=大虚线
title: `BB(${bbConfig.length}, ${bbConfig.upperMultiplier}, ${bbConfig.lowerMultiplier}) Upper`,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: true,
});
const middleSeries = tvWidget.mainChart.addLineSeries({
color: bbConfig.middleColor,
lineWidth: bbConfig.lineWidth || 2,
lineStyle: bbConfig.lineStyle || 0, // 0=实线, 1=点线, 2=虚线, 3=大虚线
title: `BB(${bbConfig.length}, ${bbConfig.upperMultiplier}, ${bbConfig.lowerMultiplier}) Middle`,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: true,
});
const lowerSeries = tvWidget.mainChart.addLineSeries({
color: bbConfig.lowerColor,
lineWidth: bbConfig.lineWidth || 2,
lineStyle: bbConfig.lineStyle || 0, // 0=实线, 1=点线, 2=虚线, 3=大虚线
title: `BB(${bbConfig.length}, ${bbConfig.upperMultiplier}, ${bbConfig.lowerMultiplier}) Lower`,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: true,
});
// 设置数据
upperSeries.setData(bbData.map(item => ({ time: item.time, value: item.upper })));
middleSeries.setData(bbData.map(item => ({ time: item.time, value: item.middle })));
lowerSeries.setData(bbData.map(item => ({ time: item.time, value: item.lower })));
// 保存数据到配置中以便显示最新值
bbConfig.data = bbData;
// 保存系列引用
tvWidget.series.bbSeries.push(upperSeries, middleSeries, lowerSeries);
addedCount++;
console.log(`✅ 布林带创建成功 [${index}]:`, bbConfig.type, bbConfig.length, bbConfig.upperMultiplier, bbConfig.lowerMultiplier, '数据点:', bbData.length);
} catch (e) {
console.error('❌ 添加布林带时出错:', e, bbConfig);
}
});
console.log(`🎯 布林带添加完成,共添加 ${addedCount} 条布林带`);
console.log('📊 当前图表中的布林带系列数量:', tvWidget.series.bbSeries.length);
// 更新技术指标面板
updateIndicatorPanel();
}
// 更新均线预览
function updateLinePreview() {
// 检查是否在均线配置窗口
if ($('#maConfigModal').is(':visible')) {
const color = $('#maColor').val();
const width = $('#maLineWidth').val();
const style = $('#maLineStyle').val();
const line = $('#previewLine');
line.attr('stroke', color);
line.attr('stroke-width', width);
// 设置线条样式
switch(parseInt(style)) {
case 0: // 实线
line.attr('stroke-dasharray', 'none');
break;
case 1: // 点线
line.attr('stroke-dasharray', '2,3');
break;
case 2: // 虚线
line.attr('stroke-dasharray', '5,5');
break;
case 3: // 大虚线
line.attr('stroke-dasharray', '10,5');
break;
}
}
}
// 更新布林带预览
function updateBBLinePreview() {
const upperColor = $('#bbUpperColor').val();
const middleColor = $('#bbMiddleColor').val();
const lowerColor = $('#bbLowerColor').val();
const width = $('#bbLineWidth').val();
const style = $('#bbLineStyle').val();
const upperLine = $('#bbPreviewUpper');
const middleLine = $('#bbPreviewMiddle');
const lowerLine = $('#bbPreviewLower');
// 设置各条线的颜色
upperLine.attr('stroke', upperColor);
middleLine.attr('stroke', middleColor);
lowerLine.attr('stroke', lowerColor);
// 设置线条宽度和样式
[upperLine, middleLine, lowerLine].forEach(line => {
line.attr('stroke-width', width);
// 设置线条样式
switch(parseInt(style)) {
case 0: // 实线
line.attr('stroke-dasharray', 'none');
break;
case 1: // 点线
line.attr('stroke-dasharray', '2,3');
break;
case 2: // 虚线
line.attr('stroke-dasharray', '5,5');
break;
case 3: // 大虚线
line.attr('stroke-dasharray', '10,5');
break;
}
});
}
// 监听配置变化以更新预览
$(document).on('change', '#maColor, #maLineWidth, #maLineStyle', updateLinePreview);
$(document).on('change', '#bbUpperColor, #bbMiddleColor, #bbLowerColor, #bbLineWidth, #bbLineStyle', updateBBLinePreview);
// 点击弹窗外部关闭
$(document).on('click', '#bbConfigModal', function(e) {
if (e.target === this) {
hideBBConfig();
}
});
// 初始化技术指标下拉菜单
function initIndicatorDropdown() {
// 确保Bootstrap下拉菜单正常工作
try {
// 如果Bootstrap没有正确加载,添加手动下拉菜单功能
$('#indicatorDropdown').off('click').on('click', function(e) {
e.preventDefault();
const $menu = $(this).next('.dropdown-menu');
// 切换菜单显示状态
if ($menu.hasClass('show')) {
$menu.removeClass('show');
} else {
// 隐藏其他所有下拉菜单
$('.dropdown-menu').removeClass('show');
$menu.addClass('show');
}
});
// 点击菜单外部关闭下拉菜单
$(document).on('click', function(e) {
if (!$(e.target).closest('.indicator-dropdown').length) {
$('.dropdown-menu').removeClass('show');
}
});
console.log('✅ 技术指标下拉菜单初始化完成');
} catch (error) {
console.error('❌ 技术指标下拉菜单初始化失败:', error);
}
}
// 页面加载完成后初始化
$(document).ready(function() {
// 初始化技术指标下拉菜单
initIndicatorDropdown();
// 设置默认的筛选时间(最近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 existingAtrLines = document.querySelectorAll('.atr-crosshair-line');
existingAtrLines.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>
<!-- 均线配置弹窗 -->
<div id="maConfigModal" class="ma-config-modal">
<div class="ma-config-content">
<div class="ma-config-title">添加均线</div>
<div class="ma-config-form">
<div class="ma-config-group">
<label class="ma-config-label">类型</label>
<select id="maType" class="ma-config-select">
<option value="SMA">SMA (简单移动平均)</option>
<option value="EMA">EMA (指数移动平均)</option>
<option value="WMA">WMA (加权移动平均)</option>
</select>
</div>
<div class="ma-config-group">
<label class="ma-config-label">长度</label>
<input type="number" id="maLength" class="ma-config-input" value="20" min="1" max="200">
</div>
<div class="ma-config-group">
<label class="ma-config-label">数据来源</label>
<select id="maSource" class="ma-config-select">
<option value="close">收盘价</option>
<option value="open">开盘价</option>
<option value="high">最高价</option>
<option value="low">最低价</option>
<option value="hl2">最高最低平均价 (HL2)</option>
<option value="hlc3">典型价 (HLC3)</option>
<option value="ohlc4">平均价 (OHLC4)</option>
</select>
</div>
<div class="ma-config-group">
<label class="ma-config-label">平滑线类型</label>
<select id="maSmoothType" class="ma-config-select">
<option value="none">无平滑</option>
<option value="SMA">SMA</option>
<option value="EMA">EMA</option>
<option value="WMA">WMA</option>
</select>
</div>
<div class="ma-config-group">
<label class="ma-config-label">平滑长度</label>
<input type="number" id="maSmoothLength" class="ma-config-input" value="3" min="1" max="50" disabled>
</div>
<div class="ma-config-group">
<label class="ma-config-label">线条粗细</label>
<select id="maLineWidth" class="ma-config-select">
<option value="1">细线 (1px)</option>
<option value="2" selected>普通 (2px)</option>
<option value="3">粗线 (3px)</option>
<option value="4">很粗 (4px)</option>
<option value="5">超粗 (5px)</option>
</select>
</div>
<div class="ma-config-group">
<label class="ma-config-label">线条样式</label>
<select id="maLineStyle" class="ma-config-select">
<option value="0">实线</option>
<option value="1">点线</option>
<option value="2">虚线</option>
<option value="3">大虚线</option>
</select>
</div>
<div class="ma-config-group">
<label class="ma-config-label">颜色</label>
<input type="color" id="maColor" class="ma-config-input" value="#2962FF">
</div>
<div class="ma-config-group">
<label class="ma-config-label">预览</label>
<div id="maLinePreview" class="ma-line-preview">
<svg id="previewSvg">
<line id="previewLine" x1="10%" y1="50%" x2="90%" y2="50%" stroke="#2962FF" stroke-width="2"/>
</svg>
</div>
</div>
</div>
<div class="ma-config-actions">
<button class="ma-config-btn ma-config-btn-secondary" onclick="hideMAConfig()">取消</button>
<button class="ma-config-btn ma-config-btn-primary" id="maConfigSubmitBtn">添加</button>
</div>
</div>
</div>
<!-- 布林带配置弹窗 -->
<div id="bbConfigModal" class="bb-config-modal">
<div class="bb-config-content">
<div class="bb-config-title">添加布林带</div>
<div class="bb-config-form">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
<div class="bb-config-group">
<label class="bb-config-label">类型</label>
<select id="bbType" class="bb-config-select">
<option value="Bollinger Bands">BB</option>
</select>
</div>
<div class="bb-config-group">
<label class="bb-config-label">长度</label>
<input type="number" id="bbLength" class="bb-config-input" value="20" min="1" max="200">
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
<div class="bb-config-group">
<label class="bb-config-label">上轨倍数</label>
<input type="number" id="bbUpperMultiplier" class="bb-config-input" value="2" min="0.1" max="10" step="0.1">
</div>
<div class="bb-config-group">
<label class="bb-config-label">下轨倍数</label>
<input type="number" id="bbLowerMultiplier" class="bb-config-input" value="2" min="0.1" max="10" step="0.1">
</div>
</div>
<div class="bb-config-group">
<label class="bb-config-label">数据来源</label>
<select id="bbSource" class="bb-config-select">
<option value="close">收盘价</option>
<option value="open">开盘价</option>
<option value="high">最高价</option>
<option value="low">最低价</option>
<option value="hl2">最高最低平均价 (HL2)</option>
<option value="hlc3">典型价 (HLC3)</option>
<option value="ohlc4">平均价 (OHLC4)</option>
</select>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
<div class="bb-config-group">
<label class="bb-config-label">线条粗细</label>
<select id="bbLineWidth" class="bb-config-select">
<option value="1">细线 (1px)</option>
<option value="2" selected>普通 (2px)</option>
<option value="3">粗线 (3px)</option>
</select>
</div>
<div class="bb-config-group">
<label class="bb-config-label">线条样式</label>
<select id="bbLineStyle" class="bb-config-select">
<option value="0">实线</option>
<option value="1">点线</option>
<option value="2">虚线</option>
<option value="3">大虚线</option>
</select>
</div>
</div>
<div class="bb-config-group">
<label class="bb-config-label">颜色配置</label>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px;">
<div>
<label style="font-size: 11px; color: #666;">上轨</label>
<input type="color" id="bbUpperColor" class="bb-config-input" value="#ff6b6b" style="height: 30px;">
</div>
<div>
<label style="font-size: 11px; color: #666;">中轨</label>
<input type="color" id="bbMiddleColor" class="bb-config-input" value="#667eea" style="height: 30px;">
</div>
<div>
<label style="font-size: 11px; color: #666;">下轨</label>
<input type="color" id="bbLowerColor" class="bb-config-input" value="#4ecdc4" style="height: 30px;">
</div>
</div>
</div>
<div class="bb-config-group">
<label class="bb-config-label">预览</label>
<div id="bbLinePreview" class="bb-line-preview">
<svg width="100%" height="30">
<line id="bbPreviewUpper" x1="10%" y1="25%" x2="90%" y2="25%" stroke="#ff6b6b" stroke-width="2"/>
<line id="bbPreviewMiddle" x1="10%" y1="50%" x2="90%" y2="50%" stroke="#667eea" stroke-width="2"/>
<line id="bbPreviewLower" x1="10%" y1="75%" x2="90%" y2="75%" stroke="#4ecdc4" stroke-width="2"/>
</svg>
</div>
</div>
</div>
<div class="bb-config-actions">
<button class="bb-config-btn bb-config-btn-secondary" onclick="hideBBConfig()">取消</button>
<button class="bb-config-btn bb-config-btn-primary" id="bbConfigSubmitBtn">添加</button>
</div>
</div>
</div>
</body>
</html>