Files
Chan/web/templates/index.html
T
2026-04-24 10:13:30 +08:00

10645 lines
564 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>
<script defer src="{{ url_for('static', filename='js/indicators.js') }}"></script>
<script defer src="{{ url_for('static', filename='js/charts.js') }}"></script>
<!-- TradingView Widget END -->
<script>
window.AVAILABLE_TIMEFRAMES = JSON.parse('{{ timeframe_keys_json | safe }}');
window.DEFAULT_MAIN_TIMEFRAME = "{{ default_main_timeframe }}";
window.DEFAULT_ELEMENT_TIMEFRAME = "{{ default_element_timeframe }}";
window.DEFAULT_SUB_SUB_TIMEFRAME = "{{ default_sub_sub_timeframe }}";
window.timeframeToMs = function(tf) {
if (!tf) return null;
var unit = tf.slice(-1);
var value = parseInt(tf.slice(0, -1), 10);
if (isNaN(value)) return null;
var unitMap = {
m: 60 * 1000,
h: 60 * 60 * 1000,
d: 24 * 60 * 60 * 1000,
w: 7 * 24 * 60 * 60 * 1000,
M: 30 * 24 * 60 * 60 * 1000
};
return unitMap[unit] ? value * unitMap[unit] : null;
};
</script>
<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: 1200px;
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;
}
.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;
}
/* 买卖点相关样式已移除 */
/* 主次周期元素样式区分 */
.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%;
}
.macd-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;
}
.macd-config-content {
background: white;
border-radius: 8px;
padding: 20px;
width: 360px;
max-width: 90vw;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
}
.macd-config-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 20px;
color: #333;
}
.macd-config-form {
display: grid;
gap: 14px;
}
.macd-config-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.macd-config-label {
font-size: 14px;
font-weight: 500;
color: #555;
}
.macd-config-input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.macd-config-input:focus {
outline: none;
border-color: #0d6efd;
box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
}
.macd-config-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 20px;
}
.macd-config-btn {
padding: 8px 18px;
border: none;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.macd-config-btn-primary {
background: #0d6efd;
color: white;
}
.macd-config-btn-primary:hover {
background: #0b5ed7;
}
.macd-config-btn-secondary {
background: #f8f9fa;
color: #6c757d;
}
.macd-config-btn-secondary:hover {
background: #e9ecef;
}
.macd-config-hint {
font-size: 12px;
color: #999;
margin-top: 2px;
}
</style>
</head>
<body>
<div class="container">
<div class="controls">
<div class="row g-1 d-flex align-items-end">
<div class="col-md-2">
<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 == default_symbol %}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="timezone" class="form-label">时区:</label>
<select id="timezone" class="form-select">
<option value="UTC">UTC</option>
<option value="Asia/Shanghai" selected>Asia/Shanghai (UTC+8)</option>
<option value="America/New_York">America/New_York (UTC-4/5)</option>
<option value="Europe/London">Europe/London (UTC+0/1)</option>
<option value="Europe/Berlin">Europe/Berlin (UTC+1/2)</option>
<option value="Asia/Tokyo">Asia/Tokyo (UTC+9)</option>
</select>
</div>
<div class="col-md-2">
<label for="start_time" class="form-label">开始时间:</label>
<input type="datetime-local" id="start_time" class="form-control">
</div>
<div class="col-md-2">
<label for="end_time" class="form-label">结束时间:</label>
<input type="datetime-local" id="end_time" class="form-control">
</div>
<div class="col-md-1">
<button class="btn btn-primary w-100" onclick="updateChart()" style="padding: 8px 6px; font-size: 14px;">
分析
</button>
</div>
</div>
<div class="row mt-3">
<div class="col-md-16">
<div class="d-flex align-items-center">
<label class="form-label me-3 mb-0">基础显示:</label>
<div class="form-check form-check-inline">
<select id="klineType" class="form-select form-select-sm me-2" style="width: 120px;">
<option value="candlestick" selected>蜡烛线</option>
<option value="bar">柱条</option>
<option value="line">折线</option>
<option value="area">面积</option>
<option value="baseline">基线</option>
<option value="renko">砖型</option>
<option value="heikin">平均K</option>
<option value="klc">KLC</option>
</select>
<!-- 为兼容现有逻辑,保留隐藏的原始K线开关,与下拉同步 -->
<input class="form-check-input" type="checkbox" id="showOriginalKline" checked style="display:none;">
</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>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="klinePeriod" id="subSubPeriodKline">
<label class="form-check-label" for="subSubPeriodKline">次次周期</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">ChanMACD</label>
<button class="btn btn-sm btn-outline-secondary ms-1 p-0" onclick="showMacdConfig()" style="width:22px;height:22px;line-height:1;font-size:12px;" title="MACD参数设置"></button>
</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>
<div class="d-flex align-items-center mt-1">
<label class="form-label me-0 mb-0">主周期:</label>
<div class="form-check form-check-inline">
<select id="timeframe" class="form-select form-select-sm me-2" style="width: 100px;">
{% for value, label in timeframes.items() %}
<option value="{{ value }}" {% if value == default_main_timeframe %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
<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">SEG中枢</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showMainBiZs">
<label class="form-check-label" for="showMainBiZs">BI中枢</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="showMainTrend">
<label class="form-check-label" for="showMainTrend">Trend</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="toggleUOnMain">
<label class="form-check-label" for="toggleUOnMain">显示U</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showMainBsp">
<label class="form-check-label" for="showMainBsp">买卖点</label>
</div>
</div>
<div class="d-flex align-items-center mt-1">
<label class="form-label me-0 mb-0">次周期:</label>
<div class="form-check form-check-inline">
<select id="elementTimeframe" class="form-select form-select-sm me-2" style="width: 100px;">
{% for value, label in timeframes.items() %}
<option value="{{ value }}" {% if value == default_element_timeframe %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
<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">SEG中枢</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showElementBiZs">
<label class="form-check-label" for="showElementBiZs">BI中枢</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="showElementTrend">
<label class="form-check-label" for="showElementTrend">Trend</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="toggleUOnElement">
<label class="form-check-label" for="toggleUOnElement">显示U</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showElementBsp">
<label class="form-check-label" for="showElementBsp">买卖点</label>
</div>
</div>
<div class="d-flex align-items-center mt-1">
<label class="form-label me-0 mb-0">次次周期:</label>
<div class="form-check form-check-inline">
<select id="subSubTimeframe" class="form-select form-select-sm me-2" style="width: 100px;">
{% for value, label in timeframes.items() %}
<option value="{{ value }}" {% if value == default_sub_sub_timeframe %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showSubSubBi">
<label class="form-check-label" for="showSubSubBi"></label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showSubSubSeg">
<label class="form-check-label" for="showSubSubSeg">线段</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showSubSubZs">
<label class="form-check-label" for="showSubSubZs">SEG中枢</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showSubSubBiZs">
<label class="form-check-label" for="showSubSubBiZs">BI中枢</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showSubSubKlcFxType">
<label class="form-check-label" for="showSubSubKlcFxType">KLC分型</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showSubSubTrend">
<label class="form-check-label" for="showSubSubTrend">Trend</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="toggleUOnSubSub">
<label class="form-check-label" for="toggleUOnSubSub">显示U</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showSubSubBsp">
<label class="form-check-label" for="showSubSubBsp">买卖点</label>
</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">
</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>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
let currentData = null;
let lastRequestId = 0; // 防止过期响应覆盖新数据
const FEATURES = {
trendFilter: false, // 趋势筛选/趋势小图等
dataReplay: false, // 数据回放功能
legacyMacd: false // 旧MACD(已弃用)
};
const tables = {};
// ======= 趋势筛选(币对) =======
let trendTable = null;
let trendDetailTable = null;
let trendChart = null;
function initTrendTables() {
if (!FEATURES.trendFilter) return; // 未启用则跳过初始化
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 step = window.timeframeToMs(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() {
if (!FEATURES.trendFilter) return; // 未启用则早退
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 step = window.timeframeToMs(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.ema26,
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.ema26, 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 ema26 = 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'));
ema26.setData(sanitizeMA('ema26'));
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() {
if (window.App && window.App.Indicators && typeof window.App.Indicators.computeEMA === 'function') {
return window.App.Indicators.computeEMA.apply(null, arguments);
}
console.warn('computeEMA 未就绪,返回空数组');
return [];
}
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 ema26 = computeEMA(close, 26);
const ema52 = computeEMA(close, 52);
const last = close[close.length-1];
const e26 = ema26[ema26.length-1];
const e52 = ema52[ema52.length-1];
const s26 = slope(ema26, 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 (e26 > e52 && s26 > 0 && s52 > 0) direction = 'bull';
else if (e26 < e52 && s26 < 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 || s26 > 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 || s26 < 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();
});
var tvWidget = {
mainChart: null,
volumeChart: null,
macdChart: null,
chanMacdChart: null, // 新增ChanMACD图表
series: {
candleSeries: null,
barSeries: null,
lineSeries: null,
areaSeries: null,
baselineSeries: null,
renkoSeries: null,
volumeSeries: null,
atrLineSeries: null,
macdLineSeries: null,
signalLineSeries: null,
histogramSeries: null,
mainBiSeries: [],
mainSegSeries: [],
mainZsSeries: [],
mainUncompletedZsSeries: [],
elementBiSeries: [],
elementSegSeries: [],
elementZsSeries: [],
elementUncompletedZsSeries: [],
subSubBiSeries: [],
subSubSegSeries: [],
subSubZsSeries: [],
subSubUncompletedZsSeries: [],
mainBollingerSeries: [],
elementBollingerSeries: [],
maSeries: [], // 添加均线系列
bbSeries: [], // 添加布林带系列
ema52Series: [], // 添加EMA52系列数组
chanMacdLineSeries: null, // ChanMACD线
chanMacdSignalSeries: null, // ChanMACD信号线
chanMacdHistSeries: null, // ChanMACD柱状图
chanMacdSegSeries: [], // ChanMACD段
chanMacdUnitTFSeries: [], // ChanMACD UnitTF
chanMacdHistSetSeries: [] // ChanMACD HistSet
},
state: {
isInitialized: false,
visibleRange: null,
logicalRange: null
}
};
// 默认EMA初始化哨兵,防止删除后再次被自动添加
var hasInitializedDefaultMAs = false;
// 买卖点类型定义
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();
});
function showMacdConfig() {
$.get('/api/macd_config', function(data) {
$('#macdFastPeriod').val(data.fast);
$('#macdSlowPeriod').val(data.slow);
$('#macdSignalPeriod').val(data.signal);
$('#macdConfigModal').css('display', 'flex');
});
}
function hideMacdConfig() {
$('#macdConfigModal').css('display', 'none');
}
function resetMacdConfig() {
$('#macdFastPeriod').val(24);
$('#macdSlowPeriod').val(52);
$('#macdSignalPeriod').val(9);
}
function saveMacdConfig() {
const fast = parseInt($('#macdFastPeriod').val());
const slow = parseInt($('#macdSlowPeriod').val());
const signal = parseInt($('#macdSignalPeriod').val());
if (fast >= slow) {
alert('快线周期必须小于慢线周期');
return;
}
if (fast < 2 || slow < 2 || signal < 2) {
alert('周期值必须大于等于2');
return;
}
$.ajax({
url: '/api/macd_config',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ fast: fast, slow: slow, signal: signal }),
success: function() {
hideMacdConfig();
updateChart();
},
error: function() {
alert('保存MACD参数失败');
}
});
}
$(document).on('click', '#macdConfigModal', function(e) {
if (e.target === this) hideMacdConfig();
});
// 添加原始K线复选框变更事件
$('#showOriginalKline').change(function() {
updateChartDisplay();
});
// 添加K线形态下拉变更事件(同步隐藏的原始K线开关并重绘)
$('#klineType').change(function() {
const type = $(this).val();
$('#showOriginalKline').prop('checked', type === 'candlestick');
updateChartDisplay();
});
// 添加笔复选框变更事件
$('#showMainBi').change(function() {
updateChartDisplay();
});
// 添加线段复选框变更事件
$('#showMainSeg').change(function() {
updateChartDisplay();
});
// 添加中枢复选框变更事件
$('#showMainZs').change(function() {
updateChartDisplay();
});
// 添加主周期BI中枢复选框变更事件(委托绑定,避免DOM更新后失效)
console.log('初始化BI中枢事件绑定');
$(document).on('change', '#showMainBiZs', function() {
console.log('主BI中枢切换为:', $('#showMainBiZs').is(':checked'));
updateChartDisplay();
});
// 添加趋势显示复选框变更事件(主/元素),变更后刷新主图
$('#showMainTrend').change(function() {
updateChartDisplay();
});
$('#showElementTrend').change(function() {
updateChartDisplay();
});
// 添加买卖点复选框变更事件
$('#showElementBi').change(function() {
updateChartDisplay();
});
// 添加线段复选框变更事件
$('#showElementSeg').change(function() {
updateChartDisplay();
});
// 添加中枢复选框变更事件
$('#showElementZs').change(function() {
updateChartDisplay();
});
// 添加次周期BI中枢复选框变更事件(委托绑定,避免DOM更新后失效)
$(document).on('change', '#showElementBiZs', function() {
console.log('次BI中枢切换为:', $('#showElementBiZs').is(':checked'));
updateChartDisplay();
});
// 次次周期显示开关变更事件
$('#showSubSubBi, #showSubSubSeg, #showSubSubZs, #showSubSubBiZs, #showSubSubKlcFxType, #showSubSubTrend, #showSubSubBsp').change(function() {
updateChartDisplay();
});
$(document).on('change', '#toggleUOnSubSub', function() {
window.showUOnSubSub = $('#toggleUOnSubSub').is(':checked');
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;
}
// 次次周期必须小于等于次周期
ensureSubSubLteElement();
console.log(`当前选择的元素时间周期: ${elementTimeframe},需要点击分析按钮来应用更改`);
});
// 次次周期变更时校验 <= 次周期
$('#subSubTimeframe').change(function() {
const subSub = $(this).val();
const elementTf = $('#elementTimeframe').val();
if (compareTimeframes(subSub, elementTf) > 0) {
alert('次次周期必须小于或等于次周期。');
ensureSubSubLteElement();
return;
}
});
function ensureSubSubLteElement() {
const timeframes = window.AVAILABLE_TIMEFRAMES || [];
const elementTf = $('#elementTimeframe').val();
const subSubTf = $('#subSubTimeframe').val();
if (compareTimeframes(subSubTf, elementTf) > 0) {
const idxEl = timeframes.indexOf(elementTf);
const validSubSub = idxEl > 0 ? timeframes[idxEl - 1] : timeframes[0];
$('#subSubTimeframe').val(validSubSub || elementTf);
}
}
// 比较两个时间周期的大小
function compareTimeframes(tf1, tf2) {
const v1 = window.timeframeToMs(tf1);
const v2 = window.timeframeToMs(tf2);
if (v1 === null || v2 === null) {
return 0;
}
return v1 - v2;
}
// 设置比主周期小的最大周期
function setSmallestLargerTimeframe(mainTimeframe) {
const timeframes = window.AVAILABLE_TIMEFRAMES || [];
const mainIndex = timeframes.indexOf(mainTimeframe);
if (mainIndex > 0) {
$('#elementTimeframe').val(timeframes[mainIndex - 1]);
} else {
$('#elementTimeframe').val(timeframes[0]);
}
}
// 设置小于或等于主周期的时间周期
function setSmallerOrEqualTimeframe(mainTimeframe) {
const timeframes = window.AVAILABLE_TIMEFRAMES || [];
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);
}
ensureSubSubLteElement();
});
let _lastKlinePeriod = 'main';
function updateChartDisplay() {
if (currentData) {
// 检测K线周期是否切换
const curPeriod = $('#subSubPeriodKline').is(':checked') ? 'subsub' :
($('#elementPeriodKline').is(':checked') ? 'element' : 'main');
const periodChanged = (curPeriod !== _lastKlinePeriod);
_lastKlinePeriod = curPeriod;
// 保存当前的可见范围(周期切换时不保留,避免范围越界)
if (!periodChanged && tvWidget && tvWidget.mainChart) {
try {
window._pendingRestoreView = captureChartViewState(tvWidget.mainChart);
} catch (e) {
window._pendingRestoreView = null;
}
}
console.log('更新图表显示');
// 重新初始化图表(initTradingView 内部会在最终同步时读取 _pendingRestoreView
initTradingView($('#symbol').val(), $('#timeframe').val());
}
}
// 确保所有时间处理都使用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() || window.DEFAULT_MAIN_TIMEFRAME || '5m';
const timezone = $('#timezone').val() || 'Asia/Shanghai';
const elementTimeframe = $('#elementTimeframe').val() || window.DEFAULT_ELEMENT_TIMEFRAME || '1m';
const subSubTimeframe = $('#subSubTimeframe').val() || '';
// 确保时区参数有效
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();
}
// 发送请求
const requestId = ++lastRequestId; // 标记本次请求
$.ajax({
url: '/api/analyze',
data: {
symbol: symbol,
timeframe: timeframe,
timezone: timezone,
element_timeframe: elementTimeframe,
sub_sub_timeframe: subSubTimeframe || undefined,
start_time: startTimeMs,
end_time: endTimeMs,
elements_only: false
},
success: function(data) {
// 隐藏加载图标
$('#refreshLoadingSpinner').hide();
// 忽略过期响应
if (requestId !== lastRequestId) {
return;
}
// 保存当前数据
if (currentData) {
// 覆盖前断开旧引用,帮助GC尽快回收
delete currentData.original_kline_data;
delete currentData.original_macd;
}
currentData = data;
refreshChart(data);
},
error: function(jqXHR, textStatus, errorThrown) {
// 隐藏加载图标
$('#refreshLoadingSpinner').hide();
// 显示错误信息
console.error('加载数据失败:', errorThrown);
alert('加载数据失败: ' + (jqXHR.responseJSON?.error || errorThrown));
}
});
}
function captureChartViewState(chart) {
if (!chart || !chart.timeScale) return null;
const ts = chart.timeScale();
const tsOptions = ts.options ? ts.options() : {};
return {
barSpacing: tsOptions.barSpacing,
rightOffset: tsOptions.rightOffset,
scrollPosition: ts.scrollPosition ? ts.scrollPosition() : null,
visibleRange: ts.getVisibleRange ? ts.getVisibleRange() : null,
logicalRange: ts.getVisibleLogicalRange ? ts.getVisibleLogicalRange() : null
};
}
function restoreChartViewState(charts, viewState) {
if (!viewState || !Array.isArray(charts) || charts.length === 0) return;
const validCharts = charts.filter(c => c && c.timeScale);
if (validCharts.length === 0) return;
validCharts.forEach(c => {
try {
const optionsPatch = {};
if (typeof viewState.barSpacing === 'number') optionsPatch.barSpacing = viewState.barSpacing;
if (typeof viewState.rightOffset === 'number') optionsPatch.rightOffset = viewState.rightOffset;
if (Object.keys(optionsPatch).length) {
c.timeScale().applyOptions(optionsPatch);
}
} catch (e) {}
});
let restored = false;
// 优先按逻辑范围恢复(对新数据更稳健)
if (viewState.logicalRange && viewState.logicalRange.from !== undefined && viewState.logicalRange.to !== undefined) {
validCharts.forEach(c => {
try {
c.timeScale().setVisibleLogicalRange(viewState.logicalRange);
restored = true;
} catch (e) {}
});
}
// 逻辑范围失败时,回退到时间可见范围
if (!restored && viewState.visibleRange && viewState.visibleRange.from !== undefined && viewState.visibleRange.to !== undefined) {
validCharts.forEach(c => {
try {
c.timeScale().setVisibleRange(viewState.visibleRange);
restored = true;
} catch (e) {}
});
}
// 最后回退到滚动位置
if (!restored && typeof viewState.scrollPosition === 'number') {
validCharts.forEach(c => {
try { c.timeScale().scrollToPosition(viewState.scrollPosition, false); } catch (e) {}
});
}
}
// 初始化图表
function initTradingView(symbol, timeframe) {
try {
// 在重新初始化前,尝试释放旧图表与系列资源,避免 GPU 内存累积
try {
if (tvWidget && tvWidget.state && tvWidget.state.isInitialized) {
// 主图
if (tvWidget.mainChart && typeof tvWidget.mainChart.remove === 'function') {
tvWidget.mainChart.remove();
}
// 成交量
if (tvWidget.volumeChart && typeof tvWidget.volumeChart.remove === 'function') {
tvWidget.volumeChart.remove();
}
// 旧 MACD(若存在)
if (tvWidget.macdChart && typeof tvWidget.macdChart.remove === 'function') {
tvWidget.macdChart.remove();
}
// 新 ChanMACD(若存在)
if (tvWidget.chanMacdChart && typeof tvWidget.chanMacdChart.remove === 'function') {
tvWidget.chanMacdChart.remove();
}
// ATR
if (tvWidget.atrChart && typeof tvWidget.atrChart.remove === 'function') {
tvWidget.atrChart.remove();
}
}
} catch (e) {
console.warn('释放旧图表资源失败(可忽略):', e);
}
console.log('初始化TradingView图表:', symbol, timeframe);
// 获取当前交易对的配置
const symbolConfig = getSymbolConfig(symbol);
console.log('交易对配置:', symbolConfig);
// 检查数据是否存在
if (!currentData || !currentData.kline_data) {
console.error('数据加载失败或不存在');
return;
}
// 检查使用哪一档K线数据:次次周期 / 小周期 / 主周期
const useSubSubPeriod = $('#subSubPeriodKline').is(':checked') &&
currentData.sub_sub_kline_data &&
Array.isArray(currentData.sub_sub_kline_data);
const useElementPeriod = $('#elementPeriodKline').is(':checked') &&
currentData.element_kline_data &&
Array.isArray(currentData.element_kline_data);
// 输出K线周期选择状态
const klinePeriodLabel = useSubSubPeriod ? '次次周期' : (useElementPeriod ? '小周期' : '主周期');
console.log('K线周期选择:', klinePeriodLabel);
console.log('当前选择时区:', $('#timezone').val());
console.log('交易对类型:', symbolConfig.type);
let candles = [];
const klineDataSource = useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (useElementPeriod ? (currentData.element_kline_data || []) : (currentData.kline_data || []));
if (useSubSubPeriod || useElementPeriod) {
if (!klineDataSource.length) {
console.error(useSubSubPeriod ? '次次周期K线数据不存在或为空' : '小周期K线数据不存在或为空', klineDataSource);
return;
}
candles = klineDataSource.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.error('主周期K线数据不存在或不是数组:', currentData.kline_data);
return;
}
candles = currentData.kline_data.map((kline) => {
const date = new Date(kline.date);
const timestamp = date.getTime() / 1000;
return {
time: timestamp,
open: parseFloat(kline.open),
high: parseFloat(kline.high),
low: parseFloat(kline.low),
close: parseFloat(kline.close),
};
});
}
// 根据交易对类型过滤数据(仅用于显示优化)
if (symbolConfig.type === 'a_stock' && timeframe.includes('m')) {
// 对于A股分钟级数据,过滤非交易时间
const originalLength = candles.length;
candles = filterTradingHours(candles, symbolConfig);
console.log(`A股数据过滤: ${originalLength} -> ${candles.length} 条记录`);
}
// 清除图表容器(释放旧 DOM 与 Canvas
const chartRoot = document.getElementById('tradingview_chart');
if (chartRoot) chartRoot.innerHTML = '';
// 重置图表对象
tvWidget = {
mainChart: null,
volumeChart: null,
macdChart: null,
series: {
candleSeries: null,
lineSeries: null,
volumeSeries: null,
macdLineSeries: null,
signalLineSeries: null,
histogramSeries: null,
mainBiSeries: [],
mainUncompletedBiSeries: [],
mainSegSeries: [],
mainUncompletedSegSeries: [],
mainZsSeries: [],
mainUncompletedZsSeries: [],
elementBiSeries: [],
elementUncompletedBiSeries: [],
elementSegSeries: [],
elementUncompletedSegSeries: [],
elementZsSeries: [],
elementUncompletedZsSeries: [],
subSubBiSeries: [],
subSubUncompletedBiSeries: [],
subSubSegSeries: [],
subSubUncompletedSegSeries: [],
subSubZsSeries: [],
subSubUncompletedZsSeries: [],
tradePointSeries: [],
mainBollingerSeries: [],
elementBollingerSeries: [],
maSeries: [], // 添加均线系列数组
bbSeries: [], // 添加布林带系列数组
ema52Series: [] // 添加EMA52系列数组
},
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;
let chanMacdChartContainer = null;
if (showMacd) {
// 仅显示新的 ChanMACD 图:让其占用原 MACD+ChanMACD 的整体高度
// 新布局:主图(40%) → ChanMACD(30%) → 成交量(17.5%) → ATR(12.5%)
mainChartContainer.style.height = '40%';
// 隐藏旧 MACD 容器(不创建)
// 创建 ChanMACD 容器占据原 MACD+ChanMACD 高度(30%
chanMacdChartContainer = document.createElement('div');
chanMacdChartContainer.style.width = '100%';
chanMacdChartContainer.style.height = '30%';
chanMacdChartContainer.style.position = 'absolute';
chanMacdChartContainer.style.top = '40%';
chanMacdChartContainer.style.left = '0';
chanMacdChartContainer.style.right = '0';
chanMacdChartContainer.style.borderTop = '1px solid #e0e0e0';
chanMacdChartContainer.style.zIndex = '10';
// 水印:便于区分是新的 ChanMACD 子图
const chanMacdWatermark = document.createElement('div');
chanMacdWatermark.textContent = 'ChanMACD';
chanMacdWatermark.style.position = 'absolute';
chanMacdWatermark.style.top = '4px';
chanMacdWatermark.style.left = '8px';
chanMacdWatermark.style.fontSize = '11px';
chanMacdWatermark.style.color = '#888';
chanMacdWatermark.style.pointerEvents = 'none';
chanMacdChartContainer.appendChild(chanMacdWatermark);
// 成交量位于 ChanMACD 之下
volumeChartContainer.style.top = '70%';
volumeChartContainer.style.height = '17.5%';
// ATR 位于最底部
atrChartContainer.style.top = '87.5%';
atrChartContainer.style.height = '12.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) {
// 只追加新的 ChanMACD 容器
container.appendChild(chanMacdChartContainer);
}
// 防止同步过程中的无限循环
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 if (chartType === 'chanmacd') {
chartHeight = chanMacdChartContainer ? chanMacdChartContainer.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图表(如果需要):仅创建新的 ChanMACD 图
let macdChart = null;
let chanMacdChart = null;
if (showMacd) {
chanMacdChart = LightweightCharts.createChart(chanMacdChartContainer, createChartOptions(false, 'chanmacd'));
}
// 创建主价格系列并设置数据(支持多种图表类型)
(function(){
const klineType = ($('#klineType').val() || 'candlestick');
// 先清空旧的主系列引用
tvWidget.series.candleSeries = null;
tvWidget.series.lineSeries = null;
tvWidget.series.barSeries = null;
tvWidget.series.areaSeries = null;
tvWidget.series.baselineSeries = null;
tvWidget.series.renkoSeries = null;
tvWidget.series.heikinSeries = null;
if (klineType === 'candlestick') {
const series = mainChart.addCandlestickSeries({
upColor: '#28a745',
downColor: '#dc3545',
borderVisible: false,
wickUpColor: '#28a745',
wickDownColor: '#dc3545',
});
series.setData(candles);
tvWidget.series.candleSeries = series;
} else if (klineType === 'renko') {
const series = mainChart.addCandlestickSeries({
upColor: '#28a745',
downColor: '#dc3545',
borderVisible: false,
wickUpColor: '#28a745',
wickDownColor: '#dc3545',
});
const bricks = buildRenkoFromCandles(candles);
series.setData(bricks);
tvWidget.series.renkoSeries = series;
} else if (klineType === 'heikin') {
const series = mainChart.addCandlestickSeries({
upColor: '#28a745',
downColor: '#dc3545',
borderVisible: false,
wickUpColor: '#28a745',
wickDownColor: '#dc3545',
});
const hk = buildHeikinFromCandles(candles);
series.setData(hk);
tvWidget.series.heikinSeries = series;
} else if (klineType === 'bar') {
const series = mainChart.addBarSeries({
upColor: '#28a745',
downColor: '#dc3545',
thinBars: false
});
series.setData(candles);
tvWidget.series.barSeries = series;
} else if (klineType === 'line') {
const series = mainChart.addLineSeries({
color: '#2962FF',
lineWidth: 2,
crosshairMarkerVisible: true,
lastValueVisible: true,
priceLineVisible: true,
});
const lineData = candles.map(c => ({ time: c.time, value: c.close }));
series.setData(lineData);
tvWidget.series.lineSeries = series;
} else if (klineType === 'area') {
const series = mainChart.addAreaSeries({
topColor: 'rgba(41, 98, 255, 0.4)',
bottomColor: 'rgba(41, 98, 255, 0.0)',
lineColor: '#2962FF',
lineWidth: 2,
});
const areaData = candles.map(c => ({ time: c.time, value: c.close }));
series.setData(areaData);
tvWidget.series.areaSeries = series;
} else if (klineType === 'baseline') {
const series = mainChart.addBaselineSeries({
baseValue: { type: 'price', price: candles.length ? candles[candles.length - 1].close : 0 },
topLineColor: '#26a69a',
bottomLineColor: '#ef5350',
topFillColor1: 'rgba(38, 166, 154, 0.28)',
topFillColor2: 'rgba(38, 166, 154, 0.05)',
bottomFillColor1: 'rgba(239, 83, 80, 0.28)',
bottomFillColor2: 'rgba(239, 83, 80, 0.05)'
});
const baseData = candles.map(c => ({ time: c.time, value: c.close }));
series.setData(baseData);
tvWidget.series.baselineSeries = series;
} else if (klineType === 'klc') {
// KLC显示模式 - 使用蜡烛线显示KLC数据
const series = mainChart.addCandlestickSeries({
upColor: '#28a745',
downColor: '#dc3545',
borderVisible: false,
wickUpColor: '#28a745',
wickDownColor: '#dc3545',
});
// 使用KLC数据创建蜡烛图
const klcCandles = buildKLCFromAnalysis(currentData);
series.setData(klcCandles);
tvWidget.series.klcSeries = series;
}
})();
// 转换成交量数据 - 与K线周期一致
let volumes = [];
const volumeDataSource = useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (useElementPeriod ? currentData.element_kline_data : currentData.kline_data);
console.log('成交量数据源选择:', klinePeriodLabel);
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(40, 167, 69, 0.5)' : 'rgba(220, 53, 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 = [];
const atrKlineDataSource = useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (useElementPeriod ? currentData.element_kline_data : currentData.kline_data);
const atrDataSource = useSubSubPeriod ? (currentData.sub_sub_atr || currentData.atr) : (useElementPeriod ? (currentData.element_atr || currentData.atr) : currentData.atr);
console.log('ATR数据源选择:', klinePeriodLabel);
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 图已移除,不再绘制(保留占位但彻底禁用)
if (FEATURES.legacyMacd && 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 = useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (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(40, 167, 69, 0.5)' : 'rgba(220, 53, 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;
}
// 添加ChanMACD图表
console.log('ChanMACD图表创建条件检查:', {
showMacd: showMacd,
chanMacdChart: !!chanMacdChart,
hasMacd: !!currentData.macd,
hasKlineData: !!currentData.kline_data,
isArray: Array.isArray(currentData.kline_data)
});
// 在创建 ChanMACD 前,确保一次性同步 U 显示开关到全局(默认不显示)
if (typeof window.showUOnMain === 'undefined') {
window.showUOnMain = $('#toggleUOnMain').is(':checked');
}
if (typeof window.showUOnElement === 'undefined') {
window.showUOnElement = $('#toggleUOnElement').is(':checked');
}
if (typeof window.showUOnSubSub === 'undefined') {
window.showUOnSubSub = $('#toggleUOnSubSub').is(':checked');
}
if (showMacd && chanMacdChart && ((useSubSubPeriod && currentData.sub_sub_macd) || (useElementPeriod && currentData.element_macd) || currentData.macd) && (useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (useElementPeriod ? currentData.element_kline_data : currentData.kline_data))) {
console.log('✅ 开始创建 ChanMACD 系列');
// 创建ChanMACD线系列
const chanMacdLineSeries = chanMacdChart.addLineSeries({
color: '#2962FF',
lineWidth: 1,
title: 'ChanMACD',
lastValueVisible: false,
priceLineVisible: false,
});
// 创建ChanMACD信号线系列
const chanMacdSignalSeries = chanMacdChart.addLineSeries({
color: '#FF6B6B',
lineWidth: 1,
title: 'ChanSignal',
lastValueVisible: false,
priceLineVisible: false,
});
// 创建ChanMACD柱状图系列
const chanMacdHistSeries = chanMacdChart.addHistogramSeries({
color: '#26a69a',
title: 'ChanHistogram',
priceFormat: {
type: 'price',
precision: 4,
},
});
// 设置ChanMACD图表的字体大小
chanMacdChart.applyOptions({
layout: {
fontSize: 10, // 设置更小的字体大小
},
rightPriceScale: {
fontSize: 10, // 设置右侧价格轴的字体大小
},
timeScale: {
fontSize: 10, // 设置时间轴的字体大小
},
});
// 使用与主图一致的数据源(小周期开启时使用小周期MACD与K线)
const klineDataSource = useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (useElementPeriod ? currentData.element_kline_data : currentData.kline_data);
const macdDataSource = useSubSubPeriod ? (currentData.sub_sub_macd || currentData.macd) : (useElementPeriod ? (currentData.element_macd || currentData.macd) : currentData.macd);
// 准备ChanMACD数据
const chanMacdData = [];
const chanSignalData = [];
const chanHistData = [];
console.log('ChanMACD数据源检查:', {
klineDataSourceLength: klineDataSource.length,
macdDataSource: !!macdDataSource,
macdLength: macdDataSource ? macdDataSource.macd.length : 0
});
console.log('ChanMACD数据源检查:', {
klineDataSourceLength: klineDataSource.length,
macdDataSource: !!macdDataSource,
macdLength: macdDataSource ? macdDataSource.macd.length : 0
});
for (let i = 0; i < klineDataSource.length; i++) {
const kline = klineDataSource[i];
if (kline && kline.date &&
i < macdDataSource.macd.length &&
macdDataSource.macd[i] !== null && macdDataSource.macd[i] !== undefined) {
// 使用与K线完全相同的时间戳计算方式
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
chanMacdData.push({
time: timestamp,
value: macdDataSource.macd[i]
});
chanSignalData.push({
time: timestamp,
value: macdDataSource.signal[i]
});
chanHistData.push({
time: timestamp,
value: macdDataSource.histogram[i],
color: macdDataSource.histogram[i] >= 0 ? 'rgba(40, 167, 69, 0.5)' : 'rgba(220, 53, 69, 0.5)'
});
}
}
console.log('ChanMACD数据处理完成:', {
chanMacdDataLength: chanMacdData.length,
chanSignalDataLength: chanSignalData.length,
chanHistDataLength: chanHistData.length,
sampleData: chanMacdData.length > 0 ? chanMacdData[0] : null
});
// 设置ChanMACD数据
console.log('ChanMACD数据长度:', chanMacdData.length, chanSignalData.length, chanHistData.length);
if (chanMacdData.length > 0) {
chanMacdLineSeries.setData(chanMacdData);
chanMacdSignalSeries.setData(chanSignalData);
chanMacdHistSeries.setData(chanHistData);
console.log('✅ ChanMACD数据设置成功');
} else {
console.warn('⚠️ ChanMACD数据为空,无法设置数据');
}
// 保存到tvWidget
tvWidget.series.chanMacdLineSeries = chanMacdLineSeries;
tvWidget.series.chanMacdSignalSeries = chanMacdSignalSeries;
tvWidget.series.chanMacdHistSeries = chanMacdHistSeries;
console.log('✅ ChanMACD图表系列已保存到tvWidget');
// 添加ChanMACD分析标注
// 根据主/次周期开关与各自的"显示U"独立控制
const cm = useSubSubPeriod ? (currentData.sub_sub_chan_macd || currentData.chan_macd) : (useElementPeriod ? (currentData.element_chan_macd || currentData.chan_macd) : currentData.chan_macd);
// 默认不显示,必须用户勾选对应复选框
const allowU = useSubSubPeriod ? !!window.showUOnSubSub : (useElementPeriod ? !!window.showUOnElement : !!window.showUOnMain);
if (cm && allowU) {
console.log('添加ChanMACD分析标注:', {
segListLength: cm.seg_list ? cm.seg_list.length : 0,
unittfListLength: cm.unittf_list ? cm.unittf_list.length : 0,
histsetListLength: cm.histset_list ? cm.histset_list.length : 0
});
// 详细检查段数据
if (cm.seg_list && cm.seg_list.length > 0) {
console.log('段数据详情:', cm.seg_list.slice(0, 3)); // 显示前3个段
} else {
console.log('⚠️ 段数据为空或不存在');
}
addAllChanMacdMarkers(
cm.seg_list || [],
cm.unittf_list || [],
cm.histset_list || [],
{
high_position_list: cm.high_position_list || [],
high_empty_list: cm.high_empty_list || [],
low_position_list: cm.low_position_list || [],
low_empty_list: cm.low_empty_list || [],
return_zero_list: cm.return_zero_list || [],
cross0_up_list: cm.cross0_up_list || [],
cross0_down_list: cm.cross0_down_list || []
}
);
// 同时从主/次周期的 klu_list 提取 SD/CD 标记,分别使用不同样式
try {
const mainCm = currentData.chan_macd || {};
const elementCm = currentData.element_chan_macd || {};
const mainMarkers = [];
const elementMarkers = [];
// 基于时间构建 MACD 值映射,便于按时间快速获取对应的 MACD 值
const buildMacdTimeMap = (macdObj, klineArr) => {
const map = new Map();
if (!macdObj || !klineArr || !Array.isArray(klineArr)) return map;
for (let i = 0; i < klineArr.length; i++) {
const k = klineArr[i];
if (!k || !k.date) continue;
const t = Math.floor(new Date(k.date).getTime() / 1000);
const val = (macdObj.macd && macdObj.macd[i] !== undefined && macdObj.macd[i] !== null) ? macdObj.macd[i] : null;
map.set(t, val);
}
return map;
};
const mainMacdMap = buildMacdTimeMap(currentData.macd, currentData.kline_data);
const elementMacdMap = buildMacdTimeMap(
(currentData.element_macd || currentData.macd),
(currentData.element_kline_data || currentData.kline_data)
);
// 主周期 U 标记(蓝/橙,与原样式一致)
if (window.showUOnMain && Array.isArray(mainCm.klu_list)) {
mainCm.klu_list.forEach((item) => {
if (!item || !item.time) return;
const ts = Math.floor(new Date(item.time).getTime() / 1000);
if (isNaN(ts)) return;
if (Number(item.separate_div) > 0) {
const macdVal = mainMacdMap.get(ts);
const posSd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'aboveBar';
mainMarkers.push({ time: ts, position: posSd, color: '#03a9f4', shape: 'arrowUp', text: `SD${Number(item.separate_div)}`, size: 0.6 });
}
if (item.continue_div === true) {
const macdVal = mainMacdMap.get(ts);
const posCd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'belowBar';
mainMarkers.push({ time: ts, position: posCd, color: '#ff9800', shape: 'arrowDown', text: 'CD', size: 0.6 });
}
if (item.near0_return && Number(item.near0_return) > 0) {
mainMarkers.push({ time: ts, position: 'belowBar', color: '#8bc34a', shape: 'circle', text: `${Number(item.near0_return)}`, size: 0.6 });
}
});
}
// 次周期 U 标记(使用不同配色以区分)
if (window.showUOnElement && Array.isArray(elementCm.klu_list)) {
elementCm.klu_list.forEach((item) => {
if (!item || !item.time) return;
const ts = Math.floor(new Date(item.time).getTime() / 1000);
if (isNaN(ts)) return;
if (Number(item.separate_div) > 0) {
const macdVal = elementMacdMap.get(ts);
const posSd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'aboveBar';
elementMarkers.push({ time: ts, position: posSd, color: '#9c27b0', shape: 'arrowUp', text: `SD${Number(item.separate_div)}`, size: 0.6 });
}
if (item.continue_div === true) {
const macdVal = elementMacdMap.get(ts);
const posCd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'belowBar';
elementMarkers.push({ time: ts, position: posCd, color: '#4caf50', shape: 'arrowDown', text: 'CD', size: 0.6 });
}
if (item.near0_return && Number(item.near0_return) > 0) {
elementMarkers.push({ time: ts, position: 'belowBar', color: '#009688', shape: 'circle', text: `${Number(item.near0_return)}`, size: 0.6 });
}
});
}
const subSubCm = currentData.sub_sub_chan_macd || {};
const subSubMarkers = [];
const subSubMacdMap = buildMacdTimeMap(currentData.macd, currentData.kline_data);
if (window.showUOnSubSub && Array.isArray(subSubCm.klu_list)) {
subSubCm.klu_list.forEach((item) => {
if (!item || !item.time) return;
const ts = Math.floor(new Date(item.time).getTime() / 1000);
if (isNaN(ts)) return;
if (Number(item.separate_div) > 0) {
const macdVal = subSubMacdMap.get(ts);
const posSd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'aboveBar';
subSubMarkers.push({ time: ts, position: posSd, color: '#00897b', shape: 'arrowUp', text: `SD${Number(item.separate_div)}`, size: 0.6 });
}
if (item.continue_div === true) {
const macdVal = subSubMacdMap.get(ts);
const posCd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'belowBar';
subSubMarkers.push({ time: ts, position: posCd, color: '#26a69a', shape: 'arrowDown', text: 'CD', size: 0.6 });
}
if (item.near0_return && Number(item.near0_return) > 0) {
subSubMarkers.push({ time: ts, position: 'belowBar', color: '#00695c', shape: 'circle', text: `${Number(item.near0_return)}`, size: 0.6 });
}
});
}
window.kluDivMarkersSubSub = subSubMarkers;
// 保存到全局,供主图合并标记使用
window.kluDivMarkersMain = mainMarkers;
window.kluDivMarkersElement = elementMarkers;
} catch (e) {
console.warn('处理 KLU 背驰标记出错:', e);
window.kluDivMarkersMain = [];
window.kluDivMarkersElement = [];
window.kluDivMarkersSubSub = [];
}
} else {
console.log('⚠️ 没有ChanMACD分析数据');
// 无数据时清空本次的 KLU 背驰标记
window.kluDivMarkersMain = [];
window.kluDivMarkersElement = [];
window.kluDivMarkersSubSub = [];
}
} else {
console.log('⚠️ ChanMACD图表创建条件不满足');
}
// 独立于当前显示周期:计算主/次周期 SD/CD 标记(用于主图合并显示)
try {
const mainCmAll = currentData.chan_macd || {};
const elementCmAll = currentData.element_chan_macd || {};
const mainMarkersAll = [];
const elementMarkersAll = [];
// 构建 MACD 时间映射,用于依据 MACD 正负决定 SD/CD 的显示上下位置
const buildMacdTimeMapAll = (macdObj, klineArr) => {
const map = new Map();
if (!macdObj || !klineArr || !Array.isArray(klineArr)) return map;
for (let i = 0; i < klineArr.length; i++) {
const k = klineArr[i];
if (!k || !k.date) continue;
const t = Math.floor(new Date(k.date).getTime() / 1000);
const val = (macdObj.macd && macdObj.macd[i] !== undefined && macdObj.macd[i] !== null) ? macdObj.macd[i] : null;
map.set(t, val);
}
return map;
};
const mainMacdMapAll = buildMacdTimeMapAll(currentData.macd, currentData.kline_data);
const elementMacdMapAll = buildMacdTimeMapAll(
(currentData.element_macd || currentData.macd),
(currentData.element_kline_data || currentData.kline_data)
);
if ((typeof window.showUOnMain === 'undefined' ? false : window.showUOnMain) && Array.isArray(mainCmAll.klu_list)) {
mainCmAll.klu_list.forEach((item) => {
if (!item || !item.time) return;
const ts = Math.floor(new Date(item.time).getTime() / 1000);
if (isNaN(ts)) return;
if (Number(item.separate_div) > 0) {
const macdVal = mainMacdMapAll.get(ts);
const posSd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'aboveBar';
mainMarkersAll.push({ time: ts, position: posSd, color: '#03a9f4', shape: 'arrowUp', text: `SD${Number(item.separate_div)}`, size: 0.6 });
}
if (item.continue_div === true) {
const macdVal = mainMacdMapAll.get(ts);
const posCd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'belowBar';
mainMarkersAll.push({ time: ts, position: posCd, color: '#ff9800', shape: 'arrowDown', text: 'CD', size: 0.6 });
}
if (item.near0_return && Number(item.near0_return) > 0) {
mainMarkersAll.push({ time: ts, position: 'belowBar', color: '#8bc34a', shape: 'circle', text: `${Number(item.near0_return)}`, size: 0.6 });
}
});
}
if ((typeof window.showUOnElement === 'undefined' ? false : window.showUOnElement) && Array.isArray(elementCmAll.klu_list)) {
elementCmAll.klu_list.forEach((item) => {
if (!item || !item.time) return;
const ts = Math.floor(new Date(item.time).getTime() / 1000);
if (isNaN(ts)) return;
if (Number(item.separate_div) > 0) {
const macdVal = elementMacdMapAll.get(ts);
const posSd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'aboveBar';
elementMarkersAll.push({ time: ts, position: posSd, color: '#9c27b0', shape: 'arrowUp', text: `SD${Number(item.separate_div)}`, size: 0.6 });
}
if (item.continue_div === true) {
const macdVal = elementMacdMapAll.get(ts);
const posCd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'belowBar';
elementMarkersAll.push({ time: ts, position: posCd, color: '#4caf50', shape: 'arrowDown', text: 'CD', size: 0.6 });
}
if (item.near0_return && Number(item.near0_return) > 0) {
elementMarkersAll.push({ time: ts, position: 'belowBar', color: '#009688', shape: 'circle', text: `${Number(item.near0_return)}`, size: 0.6 });
}
});
}
window.kluDivMarkersMain = mainMarkersAll;
window.kluDivMarkersElement = elementMarkersAll;
const subSubCmAll = currentData.sub_sub_chan_macd || {};
const subSubMarkersAll = [];
if (window.showUOnSubSub && Array.isArray(subSubCmAll.klu_list)) {
subSubCmAll.klu_list.forEach((item) => {
if (!item || !item.time) return;
const ts = Math.floor(new Date(item.time).getTime() / 1000);
if (isNaN(ts)) return;
if (Number(item.separate_div) > 0) {
subSubMarkersAll.push({ time: ts, position: 'aboveBar', color: '#00897b', shape: 'arrowUp', text: `SD${Number(item.separate_div)}`, size: 0.6 });
}
if (item.continue_div === true) {
subSubMarkersAll.push({ time: ts, position: 'belowBar', color: '#26a69a', shape: 'arrowDown', text: 'CD', size: 0.6 });
}
if (item.near0_return && Number(item.near0_return) > 0) {
subSubMarkersAll.push({ time: ts, position: 'belowBar', color: '#00695c', shape: 'circle', text: `${Number(item.near0_return)}`, size: 0.6 });
}
});
}
window.kluDivMarkersSubSub = subSubMarkersAll;
} catch (e) {
console.warn('独立计算 KLU 背驰标记出错:', e);
window.kluDivMarkersMain = [];
window.kluDivMarkersElement = [];
window.kluDivMarkersSubSub = [];
}
// 实现三图联动滚动
// 同步图表的时间范围
function syncCharts(sourceChart, sourceContainer) {
// 防止无限循环 - 使用更精确的检查
if (syncInProgress) {
console.log('🔄 同步正在进行中,跳过此次同步');
return;
}
syncInProgress = true;
console.log('🚀 开始同步图表,来源:',
sourceChart === mainChart ? '主图' :
sourceChart === volumeChart ? '成交量图' :
sourceChart === atrChart ? 'ATR图' :
sourceChart === macdChart ? 'MACD图' :
sourceChart === chanMacdChart ? 'ChanMACD图' : '未知图表');
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);
}
}
// 同步ChanMACD图
if (showMacd && chanMacdChart && sourceChart !== chanMacdChart && chanMacdChart.timeScale) {
try {
chanMacdChart.timeScale().setVisibleLogicalRange(logicalRange);
console.log('✅ ChanMACD图同步完成');
} catch (e) {
console.error('❌ ChanMACD图同步失败:', 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,
chanmacd: 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图' :
chart === macdChart ? 'MACD图' :
chart === chanMacdChart ? 'ChanMACD图' : '未知图表');
// 确定当前图表类型
const chartType = chart === mainChart ? 'main' :
chart === volumeChart ? 'volume' :
chart === atrChart ? 'atr' :
chart === macdChart ? 'macd' :
chart === chanMacdChart ? 'chanmacd' : 'unknown';
// 使用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);
}
if (showMacd && chanMacdChart) {
addChartSyncEvents(chanMacdChartContainer, chanMacdChart);
}
// 窗口大小变化时重绘图表
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
});
}
// 调整ChanMACD图大小
if (showMacd && chanMacdChart && chanMacdChartContainer) {
chanMacdChart.applyOptions({
width: chanMacdChartContainer.clientWidth,
height: chanMacdChartContainer.clientHeight
});
}
// 重新同步 - 使用主图作为同步源
setTimeout(() => {
if (mainChart) {
syncCharts(mainChart, mainChartContainer);
}
}, 200);
});
// 显示笔的绘制 - 分别处理主周期、次周期和次次周期
if ($('#showMainBi').is(':checked') || $('#showElementBi').is(':checked') || $('#showSubSubBi').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);
}
});
}
// 次次周期笔
if ($('#showSubSubBi').is(':checked') && currentData.sub_sub_bi_list && currentData.sub_sub_bi_list.length > 0) {
tvWidget.series.subSubBiSeries = [];
tvWidget.series.subSubUncompletedBiSeries = [];
currentData.sub_sub_bi_list.forEach(function(bi) {
try {
const startTime = Math.floor(new Date(bi.start_time).getTime() / 1000);
const endTime = bi.end_time ? Math.floor(new Date(bi.end_time).getTime() / 1000) : 0;
if (isNaN(startTime) || !endTime) return;
const startPrice = parseFloat(bi.start_price);
const endPrice = parseFloat(bi.end_price);
if (isNaN(startPrice) || isNaN(endPrice)) return;
biLines.push({
startTime: startTime, endTime: endTime, startPrice: startPrice, endPrice: endPrice,
color: bi.direction === 1 ? '#00897b' : '#26a69a', lineWidth: 1, lineStyle: 0
});
tvWidget.series.subSubBiSeries.push({ time: startTime, value: startPrice, color: '#00897b', lineWidth: 1 });
} catch (e) { console.error('次次周期笔处理出错:', e); }
});
}
// 次次周期未完成笔
if ($('#showSubSubBi').is(':checked') && currentData.sub_sub_uncompleted_bi_list && currentData.sub_sub_uncompleted_bi_list.length > 0) {
if (!tvWidget.series.subSubBiSeries) tvWidget.series.subSubBiSeries = [];
if (!tvWidget.series.subSubUncompletedBiSeries) tvWidget.series.subSubUncompletedBiSeries = [];
const klineData = currentData.kline_data || [];
const lastTime = klineData.length ? Math.floor(new Date(klineData[klineData.length-1].date).getTime() / 1000) : 0;
currentData.sub_sub_uncompleted_bi_list.forEach(function(bi) {
try {
const startTime = Math.floor(new Date(bi.start_time).getTime() / 1000);
if (isNaN(startTime) || !lastTime) return;
const startPrice = parseFloat(bi.start_price);
if (isNaN(startPrice)) return;
const lastK = klineData[klineData.length-1];
const endPrice = bi.direction === 1 ? parseFloat(lastK.high) : parseFloat(lastK.low);
biLines.push({
startTime: startTime, endTime: lastTime, startPrice: startPrice, endPrice: endPrice,
color: '#00695c', lineWidth: 1, lineStyle: 2
});
tvWidget.series.subSubUncompletedBiSeries.push({ time: startTime, value: startPrice, color: '#00695c', lineWidth: 1, lineStyle: 2 });
} catch (e) { console.error('次次周期未完成笔处理出错:', e); }
});
}
// 添加所有笔到图表
// 1m 等小周期叠加到大周期时,笔数量会非常大;每条笔创建一个 series 会导致主图渲染退化
// 这里限制绘制数量,优先保留最新笔,避免把主K线和其他元素“挤没”
const MAX_BI_LINE_SERIES = 800;
if (biLines.length > MAX_BI_LINE_SERIES) {
console.warn(`BI线条过多(${biLines.length}),仅绘制最新 ${MAX_BI_LINE_SERIES} 条以保障主图稳定`);
}
const linesToDraw = biLines.length > MAX_BI_LINE_SERIES
? biLines.slice(-MAX_BI_LINE_SERIES)
: biLines;
linesToDraw.forEach(line => {
try {
if (!Number.isFinite(line.startTime) || !Number.isFinite(line.endTime)) return;
if (!Number.isFinite(line.startPrice) || !Number.isFinite(line.endPrice)) return;
if (line.endTime <= line.startTime) return;
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 }
]);
} catch (e) {
console.warn('绘制BI线段失败,已跳过单条异常数据:', e);
}
});
} else {
console.log('绘制笔 - 已禁用');
}
// 显示线段的绘制 - 分别处理主周期、次周期和次次周期
if ($('#showMainSeg').is(':checked') || $('#showElementSeg').is(':checked') || $('#showSubSubSeg').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);
}
});
}
// 次次周期线段
if ($('#showSubSubSeg').is(':checked') && currentData.sub_sub_seg_list && currentData.sub_sub_seg_list.length > 0) {
tvWidget.series.subSubSegSeries = [];
tvWidget.series.subSubUncompletedSegSeries = [];
currentData.sub_sub_seg_list.forEach(function(seg) {
try {
const startTime = Math.floor(new Date(seg.start_time).getTime() / 1000);
const endTime = seg.end_time ? Math.floor(new Date(seg.end_time).getTime() / 1000) : 0;
if (isNaN(startTime) || !endTime) return;
const startPrice = parseFloat(seg.start_price);
const endPrice = parseFloat(seg.end_price);
if (isNaN(startPrice) || isNaN(endPrice)) return;
segLines.push({
startTime: startTime, endTime: endTime, startPrice: startPrice, endPrice: endPrice,
color: seg.direction === 1 ? '#00897b' : '#26a69a', lineWidth: 2, lineStyle: 0
});
tvWidget.series.subSubSegSeries.push({ time: startTime, value: startPrice, color: '#00897b', lineWidth: 2 });
} catch (e) { console.error('次次周期线段处理出错:', e); }
});
}
// 次次周期未完成线段
if ($('#showSubSubSeg').is(':checked') && currentData.sub_sub_uncompleted_seg_list && currentData.sub_sub_uncompleted_seg_list.length > 0) {
if (!tvWidget.series.subSubUncompletedSegSeries) tvWidget.series.subSubUncompletedSegSeries = [];
const klineDataSeg = currentData.kline_data || [];
const lastTimeSeg = klineDataSeg.length ? Math.floor(new Date(klineDataSeg[klineDataSeg.length-1].date).getTime() / 1000) : 0;
currentData.sub_sub_uncompleted_seg_list.forEach(function(seg) {
try {
const startTime = Math.floor(new Date(seg.start_time).getTime() / 1000);
if (isNaN(startTime) || !lastTimeSeg) return;
const startPrice = parseFloat(seg.start_price);
if (isNaN(startPrice)) return;
let endTime = lastTimeSeg, endPrice;
if (seg.end_time && seg.end_price) {
endTime = Math.floor(new Date(seg.end_time).getTime() / 1000);
endPrice = parseFloat(seg.end_price);
} else {
const lastK = klineDataSeg[klineDataSeg.length-1];
endPrice = seg.direction === 1 ? parseFloat(lastK.high) : parseFloat(lastK.low);
}
segLines.push({
startTime: startTime, endTime: endTime, startPrice: startPrice, endPrice: endPrice,
color: '#00695c', lineWidth: 2, lineStyle: 2
});
tvWidget.series.subSubUncompletedSegSeries.push({ time: startTime, value: startPrice, color: '#00695c', lineWidth: 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('绘制线段 - 已禁用');
}
// 显示中枢的绘制 - 分别处理主周期、次周期和次次周期(包含BI中枢,沿用同样样式与开关)
if ($('#showMainZs').is(':checked') || $('#showElementZs').is(':checked') || $('#showSubSubZs').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);
}
});
}
// 次次周期SEG中枢
if ($('#showSubSubZs').is(':checked') && currentData.sub_sub_zs_list && currentData.sub_sub_zs_list.length > 0) {
const subSubZsColor = '#00897b';
currentData.sub_sub_zs_list.forEach(function(zs) {
try {
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
const endTime = zs.end_time ? Math.floor(new Date(zs.end_time).getTime() / 1000) : 0;
if (isNaN(startTime) || !endTime) 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)) return;
mainChart.addLineSeries({ color: subSubZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
mainChart.addLineSeries({ color: subSubZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
mainChart.addLineSeries({ color: subSubZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
mainChart.addLineSeries({ color: subSubZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]);
if (!isNaN(gg) && gg > 0) mainChart.addLineSeries({ color: subSubZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
if (!isNaN(dd) && dd > 0) mainChart.addLineSeries({ color: subSubZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
} catch (e) { console.error('次次周期中枢处理出错:', e); }
});
}
} else {
console.log('绘制中枢 - 已禁用');
}
// BI中枢(已完成)- 使用独立的BI开关
if ($('#showMainBiZs').is(':checked') && currentData.bi_zs_list && currentData.bi_zs_list.length > 0) {
try {
console.log(`绘制主周期BI中枢数据,共${currentData.bi_zs_list.length}条`);
} catch (e) {}
currentData.bi_zs_list.forEach(function(zs) {
try {
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
const endTime = zs.end_time ? Math.floor(new Date(zs.end_time).getTime() / 1000) : Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
if (isNaN(startTime) || isNaN(endTime)) { 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)) { return; }
const color = '#F1C40F';
const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
const rightSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
rightSeries.setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]);
if (!isNaN(gg) && gg > 0) {
const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
}
if (!isNaN(dd) && dd > 0) {
const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
}
} catch (e) { console.error('主周期BI中枢处理出错:', e); }
});
}
if ($('#showSubSubBiZs').is(':checked') && currentData.sub_sub_bi_zs_list && currentData.sub_sub_bi_zs_list.length > 0) {
currentData.sub_sub_bi_zs_list.forEach(function(zs) {
try {
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
const kd = currentData.kline_data || [];
const endTime = zs.end_time ? Math.floor(new Date(zs.end_time).getTime() / 1000) : (kd.length ? Math.floor(new Date(kd[kd.length-1].date).getTime() / 1000) : 0);
if (isNaN(startTime) || !endTime) 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)) return;
const color = '#00897b';
mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]);
if (!isNaN(gg) && gg > 0) mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
if (!isNaN(dd) && dd > 0) mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
} catch (e) { console.error('次次周期BI中枢处理出错:', e); }
});
}
if ($('#showElementBiZs').is(':checked') && currentData.element_bi_zs_list && currentData.element_bi_zs_list.length > 0) {
try {
console.log(`绘制次周期BI中枢数据,共${currentData.element_bi_zs_list.length}条`);
} catch (e) {}
currentData.element_bi_zs_list.forEach(function(zs) {
try {
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
const endTime = zs.end_time ? Math.floor(new Date(zs.end_time).getTime() / 1000) : Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
if (isNaN(startTime) || isNaN(endTime)) { 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)) { return; }
const color = '#3f51b5';
const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
const rightSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
rightSeries.setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]);
if (!isNaN(gg) && gg > 0) {
const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
}
if (!isNaN(dd) && dd > 0) {
const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
}
} catch (e) { console.error('次周期BI中枢处理出错:', e); }
});
}
// 显示未完成中枢 - 分别处理主周期、次周期和次次周期
if ($('#showMainZs').is(':checked') || $('#showElementZs').is(':checked') || $('#showSubSubZs').is(':checked') || $('#showSubSubBiZs').is(':checked')) {
console.log('绘制未完成中枢 - 已启用');
// 显示BI中枢绘制(沿用中枢样式)
if ($('#showMainBiZs').is(':checked') || $('#showElementBiZs').is(':checked') || $('#showSubSubBiZs').is(':checked')) {
console.log('绘制BI中枢 - 已启用');
// 主周期 BI 中枢
console.log('主BI开关:', $('#showMainBiZs').is(':checked'), '数据长度:', currentData.bi_zs_list ? currentData.bi_zs_list.length : 0);
if ($('#showMainBiZs').is(':checked') && currentData.bi_zs_list && currentData.bi_zs_list.length > 0) {
console.log(`绘制主周期BI中枢数据,共${currentData.bi_zs_list.length}条`);
currentData.bi_zs_list.forEach(function(zs) {
try {
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
const endTime = zs.end_time ? Math.floor(new Date(zs.end_time).getTime() / 1000) : Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
if (isNaN(startTime) || isNaN(endTime)) { return; }
const zg = parseFloat(zs.zg), zd = parseFloat(zs.zd), gg = parseFloat(zs.gg), dd = parseFloat(zs.dd);
if (isNaN(zg) || isNaN(zd)) { return; }
const color = '#9C27B0'; // 主周期BI中枢颜色(紫色)
const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
const rightSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
rightSeries.setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]);
if (!isNaN(gg) && gg > 0) {
const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
}
if (!isNaN(dd) && dd > 0) {
const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
}
} catch (e) { console.error('主周期BI中枢处理出错:', e); }
});
}
// 主周期 未完成 BI 中枢
console.log('主未完成BI长度:', currentData.uncompleted_bi_zs_list ? currentData.uncompleted_bi_zs_list.length : 0);
if ($('#showMainBiZs').is(':checked') && currentData.uncompleted_bi_zs_list && currentData.uncompleted_bi_zs_list.length > 0) {
console.log(`绘制主周期未完成BI中枢数据,共${currentData.uncompleted_bi_zs_list.length}条`);
currentData.uncompleted_bi_zs_list.forEach(function(zs) {
try {
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
if (isNaN(startTime) || isNaN(endTime)) { return; }
const zg = parseFloat(zs.zg), zd = parseFloat(zs.zd), gg = parseFloat(zs.gg), dd = parseFloat(zs.dd);
if (isNaN(zg) || isNaN(zd)) { return; }
const color = '#9C27B0';
const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
if (!isNaN(gg) && gg > 0) {
const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
}
if (!isNaN(dd) && dd > 0) {
const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
}
} catch (e) { console.error('主周期未完成BI中枢处理出错:', e); }
});
}
// 次周期 BI 中枢
console.log('次BI开关:', $('#showElementBiZs').is(':checked'), '数据长度:', currentData.element_bi_zs_list ? currentData.element_bi_zs_list.length : 0);
if ($('#showElementBiZs').is(':checked') && currentData.element_bi_zs_list && currentData.element_bi_zs_list.length > 0) {
console.log(`绘制次周期BI中枢数据,共${currentData.element_bi_zs_list.length}条`);
currentData.element_bi_zs_list.forEach(function(zs) {
try {
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
const endTime = zs.end_time ? Math.floor(new Date(zs.end_time).getTime() / 1000) : Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
if (isNaN(startTime) || isNaN(endTime)) { return; }
const zg = parseFloat(zs.zg), zd = parseFloat(zs.zd), gg = parseFloat(zs.gg), dd = parseFloat(zs.dd);
if (isNaN(zg) || isNaN(zd)) { return; }
const color = '#8BC34A'; // 次周期BI中枢颜色(绿)
const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
const rightSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
rightSeries.setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]);
if (!isNaN(gg) && gg > 0) {
const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
}
if (!isNaN(dd) && dd > 0) {
const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
}
} catch (e) { console.error('次周期BI中枢处理出错:', e); }
});
}
// 次周期 未完成 BI 中枢
console.log('次未完成BI长度:', currentData.element_uncompleted_bi_zs_list ? currentData.element_uncompleted_bi_zs_list.length : 0);
if ($('#showElementBiZs').is(':checked') && currentData.element_uncompleted_bi_zs_list && currentData.element_uncompleted_bi_zs_list.length > 0) {
console.log(`绘制次周期未完成BI中枢数据,共${currentData.element_uncompleted_bi_zs_list.length}条`);
currentData.element_uncompleted_bi_zs_list.forEach(function(zs) {
try {
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
if (isNaN(startTime) || isNaN(endTime)) { return; }
const zg = parseFloat(zs.zg), zd = parseFloat(zs.zd), gg = parseFloat(zs.gg), dd = parseFloat(zs.dd);
if (isNaN(zg) || isNaN(zd)) { return; }
const color = '#8BC34A';
const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
if (!isNaN(gg) && gg > 0) {
const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
}
if (!isNaN(dd) && dd > 0) {
const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
}
} catch (e) { console.error('次周期未完成BI中枢处理出错:', e); }
});
}
// 次次周期 未完成 BI 中枢
if ($('#showSubSubBiZs').is(':checked') && currentData.sub_sub_uncompleted_bi_zs_list && currentData.sub_sub_uncompleted_bi_zs_list.length > 0) {
const kdBi = currentData.kline_data || [];
const endTimeBi = kdBi.length ? Math.floor(new Date(kdBi[kdBi.length-1].date).getTime() / 1000) : 0;
currentData.sub_sub_uncompleted_bi_zs_list.forEach(function(zs) {
try {
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
if (isNaN(startTime) || !endTimeBi) return;
const zg = parseFloat(zs.zg), zd = parseFloat(zs.zd), gg = parseFloat(zs.gg), dd = parseFloat(zs.dd);
if (isNaN(zg) || isNaN(zd)) return;
const color = '#00897b';
mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zg }, { time: endTimeBi, value: zg }]);
mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: endTimeBi, value: zd }]);
mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
if (!isNaN(gg) && gg > 0) mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: gg }, { time: endTimeBi, value: gg }]);
if (!isNaN(dd) && dd > 0) mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: dd }, { time: endTimeBi, value: dd }]);
} catch (e) { console.error('次次周期未完成BI中枢处理出错:', e); }
});
}
}
// 主周期未完成中枢
if ($('#showMainZs').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 ($('#showElementZs').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);
}
});
}
// 次次周期未完成SEG中枢
if ($('#showSubSubZs').is(':checked') && currentData.sub_sub_uncompleted_zs_list && currentData.sub_sub_uncompleted_zs_list.length > 0) {
const kdZs = currentData.kline_data || [];
const endTimeZs = kdZs.length ? Math.floor(new Date(kdZs[kdZs.length-1].date).getTime() / 1000) : 0;
const subSubUZsColor = '#00897b';
currentData.sub_sub_uncompleted_zs_list.forEach(function(zs) {
try {
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
if (isNaN(startTime) || !endTimeZs) 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)) return;
mainChart.addLineSeries({ color: subSubUZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zg }, { time: endTimeZs, value: zg }]);
mainChart.addLineSeries({ color: subSubUZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: endTimeZs, value: zd }]);
mainChart.addLineSeries({ color: subSubUZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
if (!isNaN(gg) && gg > 0) mainChart.addLineSeries({ color: subSubUZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: gg }, { time: endTimeZs, value: gg }]);
if (!isNaN(dd) && dd > 0) mainChart.addLineSeries({ color: subSubUZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: dd }, { time: endTimeZs, value: dd }]);
} catch (e) { console.error('次次周期未完成中枢处理出错:', e); }
});
}
} else {
console.log('绘制未完成中枢 - 已禁用');
}
// 未完成BI中枢 - 使用独立的BI开关
if ($('#showMainBiZs').is(':checked') && currentData.uncompleted_bi_zs_list && currentData.uncompleted_bi_zs_list.length > 0) {
try { console.log(`绘制主周期未完成BI中枢数据,共${currentData.uncompleted_bi_zs_list.length}条`); } catch (e) {}
currentData.uncompleted_bi_zs_list.forEach(function(zs) {
try {
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
if (isNaN(startTime) || isNaN(endTime)) { 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)) { return; }
const color = '#F1C40F';
const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
if (!isNaN(gg) && gg > 0) {
const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
}
if (!isNaN(dd) && dd > 0) {
const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
}
} catch (e) { console.error('主周期未完成BI中枢处理出错:', e); }
});
}
// 次周期 未完成 BI 中枢
if ($('#showElementBiZs').is(':checked') && currentData.element_uncompleted_bi_zs_list && currentData.element_uncompleted_bi_zs_list.length > 0) {
try { console.log(`绘制次周期未完成BI中枢数据,共${currentData.element_uncompleted_bi_zs_list.length}条`); } catch (e) {}
currentData.element_uncompleted_bi_zs_list.forEach(function(zs) {
try {
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
if (isNaN(startTime) || isNaN(endTime)) { 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)) { return; }
const color = '#3f51b5';
const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
if (!isNaN(gg) && gg > 0) {
const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
}
if (!isNaN(dd) && dd > 0) {
const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
}
} catch (e) { console.error('次周期未完成BI中枢处理出错:', e); }
});
}
// 添加买卖点标记(新版:基于 bsp_list / element_bsp_list / sub_sub_bsp_list,按与 KLC 分型相同方式合并到主图标记)
if ($('#showMainBsp').is(':checked') || $('#showElementBsp').is(':checked') || $('#showSubSubBsp').is(':checked')) {
console.log('绘制买卖点(BSP) - 已启用');
// BSP 样式定义
const BSP_STYLE = {
'BSP1_BUY': { color: '#FF1744', text: 'B1', position: 'belowBar', size: 0.5 },
'BSP2_BUY': { color: '#F50057', text: 'B2', position: 'belowBar', size: 0.5 },
'BSP3_BUY': { color: '#D500F9', text: 'B3', position: 'belowBar', size: 0.5 },
'BSP1_SELL': { color: '#00E676', text: 'S1', position: 'aboveBar', size: 0.5 },
'BSP2_SELL': { color: '#00B0FF', text: 'S2', position: 'aboveBar', size: 0.5 },
'BSP3_SELL': { color: '#8B4513', text: 'S3', position: 'aboveBar', size: 0.5 },
};
const getBspStyleKey = (bsp) => {
// 统一 BSP key
// - type 可能是 "BSP1"/"BSP2"/"BSP3"
// - 也可能是后端给的 "B1"/"B2"/"B3" 或 "S1"/"S2"/"S3"
// 最终都映射为 "BSP1_BUY" / "BSP1_SELL" 这类 key,方便复用现有样式定义
let type = (bsp.type || '').toUpperCase();
const dir = (bsp.dir || '').toUpperCase();
// 若是 "B1" / "B2" / "B3" 或 "S1" / "S2" / "S3" 形式,则提取数字并映射成 "BSP{n}"
const simpleMatch = type.match(/^([BS])(\d)$/);
if (simpleMatch) {
const n = simpleMatch[2]; // "1" / "2" / "3"
type = 'BSP' + n;
}
return type + '_' + dir;
};
// 收集所有 BSP 标记
const allBspMarkers = [];
// 主周期买卖点
// 兼容不同字段命名:优先使用 bsp_list,若不存在则尝试 bsp
const mainBspList = currentData.bsp_list || currentData.bsp || [];
// 调试:打印前几条主周期 BSP 的 key,方便排查样式不匹配问题
if (mainBspList.length > 0) {
console.log(
'主周期 BSP 示例 (前5条):',
mainBspList.slice(0, 5).map(b => ({
raw_type: b.type,
raw_dir: b.dir,
key: getBspStyleKey(b)
}))
);
}
if ($('#showMainBsp').is(':checked') && mainBspList.length > 0) {
console.log(`绘制主周期买卖点,共${mainBspList.length}条`);
mainBspList.forEach(function(bsp) {
try {
const ts = Math.floor(new Date(bsp.time).getTime() / 1000);
if (isNaN(ts)) return;
const key = getBspStyleKey(bsp);
const style = BSP_STYLE[key] || { color: '#999', shape: 'circle', text: '?', position: 'inBar' };
const sureText = bsp.is_sure ? '' : '?';
allBspMarkers.push({
time: ts,
position: style.position,
color: style.color,
shape: style.shape,
text: style.text + sureText,
size: 2
});
} catch (e) {
console.error('主周期BSP处理出错:', e);
}
});
}
// 次周期买卖点
// 兼容不同字段命名:优先使用 element_bsp_list,若不存在则尝试 element_bsp
const elementBspList = currentData.element_bsp_list || currentData.element_bsp || [];
// 调试:打印前几条次周期 BSP 的 key
if (elementBspList.length > 0) {
console.log(
'次周期 BSP 示例 (前5条):',
elementBspList.slice(0, 5).map(b => ({
raw_type: b.type,
raw_dir: b.dir,
key: getBspStyleKey(b)
}))
);
}
if ($('#showElementBsp').is(':checked') && elementBspList.length > 0) {
console.log(`绘制次周期买卖点,共${elementBspList.length}条`);
elementBspList.forEach(function(bsp) {
try {
const ts = Math.floor(new Date(bsp.time).getTime() / 1000);
if (isNaN(ts)) return;
const key = getBspStyleKey(bsp);
const style = BSP_STYLE[key] || { color: '#999', shape: 'circle', text: '?', position: 'inBar' };
const sureText = bsp.is_sure ? '' : '?';
// 次周期使用稍小的标记和不同前缀以区分
allBspMarkers.push({
time: ts,
position: style.position,
color: style.color,
shape: style.shape,
text: 'e' + style.text + sureText,
size: 1
});
} catch (e) {
console.error('次周期BSP处理出错:', e);
}
});
}
// 次次周期买卖点
const subSubBspList = currentData.sub_sub_bsp_list || [];
if ($('#showSubSubBsp').is(':checked') && subSubBspList.length > 0) {
subSubBspList.forEach(function(bsp) {
try {
const ts = Math.floor(new Date(bsp.time).getTime() / 1000);
if (isNaN(ts)) return;
const key = getBspStyleKey(bsp);
const style = BSP_STYLE[key] || { color: '#999', shape: 'circle', text: '?', position: 'inBar' };
const sureText = bsp.is_sure ? '' : '?';
allBspMarkers.push({
time: ts,
position: style.position,
color: '#00897b',
shape: style.shape,
text: 's' + (style.text || '?') + sureText,
size: 1
});
} catch (e) {
console.error('次次周期BSP处理出错:', e);
}
});
}
// 将 BSP 标记挂到全局,后面与 KLC 分型等标记一起合并到主系列上
if (allBspMarkers.length > 0) {
// 按时间排序(lightweight-charts 要求标记按时间升序)
allBspMarkers.sort((a, b) => a.time - b.time);
window.bspMarkers = allBspMarkers;
console.log(`准备合并 ${allBspMarkers.length} 个BSP标记到主图标记中`);
} else {
window.bspMarkers = [];
}
} else {
// 关闭 BSP 显示时,清空全局 BSP 标记
window.bspMarkers = [];
}
// 添加买卖点标记(旧版,保留兼容)
// 这里为了与主面板上的「买卖点」开关保持一致,
// 同时响应顶部的 `#showMainBsp` 复选框
if ($('#showTradePoints').is(':checked') || $('#showMainBsp').is(':checked')) {
console.log('绘制买卖点 - 已启用(来源: showTradePoints / showMainBsp');
// 优先使用小周期数据,如果不存在则使用主周期数据
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,
color: 'transparent',
title: '买点'
});
// 使用主K线的收盘价作为基准数据,保证买点标记与价格在同一纵轴范围
if (Array.isArray(candles) && candles.length > 0) {
const baseData = candles.map(c => ({ time: c.time, value: c.close }));
buyMarkersSeries.setData(baseData);
} else {
// 兜底:至少一个数据点,避免报错
buyMarkersSeries.setData([{ time: buyMarkers[0].time, value: buyMarkers[0].price || 0 }]);
}
try {
// 设置买点标记:文字在价格上方,仅显示文字不显示形状
buyMarkersSeries.setMarkers(
buyMarkers.map(marker => {
// 使用实际价格位置,买点显示在K线上方
return {
...marker,
position: 'aboveBar', // 买点:价格上方
price: marker.price,
// 隐藏形状,仅保留文字
size: 0,
color: 'rgba(0, 0, 0, 0)'
};
})
);
console.log(`成功添加 ${buyMarkers.length} 个买点标记`);
} catch (e) {
console.error("设置买点标记时出错:", e);
}
}
if (sellMarkers.length > 0) {
const sellMarkersSeries = mainChart.addLineSeries({
lastValueVisible: false,
priceLineVisible: false,
lineVisible: false,
color: 'transparent',
title: '卖点'
});
// 使用主K线的收盘价作为基准数据,保证卖点标记与价格在同一纵轴范围
if (Array.isArray(candles) && candles.length > 0) {
const baseData = candles.map(c => ({ time: c.time, value: c.close }));
sellMarkersSeries.setData(baseData);
} else {
// 兜底:至少一个数据点,避免报错
sellMarkersSeries.setData([{ time: sellMarkers[0].time, value: sellMarkers[0].price || 0 }]);
}
try {
// 设置卖点标记:文字在价格下方,仅显示文字不显示形状
sellMarkersSeries.setMarkers(
sellMarkers.map(marker => {
// 使用实际价格位置,卖点显示在K线下方
return {
...marker,
position: 'belowBar', // 卖点:价格下方
price: marker.price,
// 隐藏形状,仅保留文字
size: 0,
color: 'rgba(0, 0, 0, 0)'
};
})
);
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 existingChanMacdLines = document.querySelectorAll('.chanmacd-crosshair-line');
existingChanMacdLines.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);
}
}
// 如果有ChanMACD图,也在ChanMACD图上绘制垂直线
if (showMacd && chanMacdChart && chanMacdChartContainer) {
const chanMacdTimeCoordinate = chanMacdChart.timeScale().timeToCoordinate(param.time);
if (chanMacdTimeCoordinate !== null) {
const chanMacdChartRect = chanMacdChartContainer.getBoundingClientRect();
console.log('ChanMACD图表位置:', {
left: chanMacdChartRect.left,
top: chanMacdChartRect.top,
width: chanMacdChartRect.width,
height: chanMacdChartRect.height,
timeCoordinate: chanMacdTimeCoordinate
});
const chanMacdLine = document.createElement('div');
chanMacdLine.className = 'chanmacd-crosshair-line';
chanMacdLine.style.position = 'fixed';
chanMacdLine.style.left = (chanMacdChartRect.left + chanMacdTimeCoordinate) + 'px';
chanMacdLine.style.top = chanMacdChartRect.top + 'px';
chanMacdLine.style.width = '1px';
chanMacdLine.style.height = chanMacdChartRect.height + 'px';
chanMacdLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)';
chanMacdLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)';
chanMacdLine.style.pointerEvents = 'none';
chanMacdLine.style.zIndex = '1000';
document.body.appendChild(chanMacdLine);
console.log('ChanMACD垂直线已创建,位置:', chanMacdLine.style.left, chanMacdLine.style.top);
} else {
console.log('ChanMACD时间坐标为空');
}
} else {
console.log('ChanMACD图表条件不满足:', {
showMacd: showMacd,
hasChanMacdChart: !!chanMacdChart,
hasChanMacdChartContainer: !!chanMacdChartContainer
});
}
}
} 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());
const existingChanMacdLines = document.querySelectorAll('.chanmacd-crosshair-line');
existingChanMacdLines.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.renkoSeries && param.seriesPrices.get(tvWidget.series.renkoSeries)) {
const price = param.seriesPrices.get(tvWidget.series.renkoSeries);
priceInfo = `价格: ${price.toFixed(2)}`;
} else if (tvWidget.series.heikinSeries && param.seriesPrices.get(tvWidget.series.heikinSeries)) {
const price = param.seriesPrices.get(tvWidget.series.heikinSeries);
priceInfo = `价格: ${price.toFixed(2)}`;
} else if (tvWidget.series.barSeries && param.seriesPrices.get(tvWidget.series.barSeries)) {
const price = param.seriesPrices.get(tvWidget.series.barSeries);
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)}`;
} else if (tvWidget.series.areaSeries && param.seriesPrices.get(tvWidget.series.areaSeries)) {
const price = param.seriesPrices.get(tvWidget.series.areaSeries);
priceInfo = `价格: ${price.toFixed(2)}`;
} else if (tvWidget.series.baselineSeries && param.seriesPrices.get(tvWidget.series.baselineSeries)) {
const price = param.seriesPrices.get(tvWidget.series.baselineSeries);
priceInfo = `价格: ${price.toFixed(2)}`;
}
// 如果没有蜡烛图系列价格,尝试从区域图系列获取
else if (tvWidget.series.areaSeries && param.seriesPrices.get(tvWidget.series.areaSeries)) {
const price = param.seriesPrices.get(tvWidget.series.areaSeries);
priceInfo = `价格: ${price.toFixed(2)}`;
}
// 如果没有蜡烛图系列价格,尝试从基线图系列获取
else if (tvWidget.series.baselineSeries && param.seriesPrices.get(tvWidget.series.baselineSeries)) {
const price = param.seriesPrices.get(tvWidget.series.baselineSeries);
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("11", "").replace("21", "").replace("31", "").replace("41", "").replace("51", "").replace("01", "");
// 添加标记配置
const markerConfig = {
time: timestamp,
position: fx.is_bottom ? 'belowBar' : 'aboveBar',
color: strengthColor,
shape: 'triangle',
text: displayText,
size: 2 // 调整尺寸,强分型稍大,普通分型更小
};
allMainFxMarkers.push(markerConfig);
// 画虚线分型框(根据 start/end + high/low
if (fx.start_time && fx.end_time && fx.high !== null && fx.high !== undefined && fx.low !== null && fx.low !== undefined) {
const startTs = Math.floor(new Date(fx.start_time).getTime() / 1000);
const endTs = Math.floor(new Date(fx.end_time).getTime() / 1000);
const high = parseFloat(fx.high);
const low = parseFloat(fx.low);
if (!isNaN(startTs) && !isNaN(endTs) && !isNaN(high) && !isNaN(low)) {
const boxHigh = Math.max(high, low);
const boxLow = Math.min(high, low);
const boxColor = strengthColor;
const topSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2, // 虚线
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
topSeries.setData([{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]);
const bottomSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2, // 虚线
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
bottomSeries.setData([{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]);
const leftSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2, // 虚线
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
// 左边竖线:同一 time 上下两个点(和你已有ZS绘制写法保持一致)
leftSeries.setData([{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]);
const rightSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2, // 虚线
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
rightSeries.setData([{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]);
if (!tvWidget.series.mainKlcFxBoxSeries) tvWidget.series.mainKlcFxBoxSeries = [];
tvWidget.series.mainKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries);
}
}
// 创建分型标记对象,包含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("11", "");
// 添加标记配置
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) ||
($('#showSubSubKlcFxType').is(':checked') && currentData.sub_sub_klc_fx_info && currentData.sub_sub_klc_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("11", "").replace("21", "").replace("3", "").replace("41", "").replace("51", "").replace("0", "");
// 小周期分型标记配置
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);
// 画虚线分型框(小周期)
if (fx.start_time && fx.end_time && fx.high !== null && fx.high !== undefined && fx.low !== null && fx.low !== undefined) {
const startTs = Math.floor(new Date(fx.start_time).getTime() / 1000);
const endTs = Math.floor(new Date(fx.end_time).getTime() / 1000);
const high = parseFloat(fx.high);
const low = parseFloat(fx.low);
if (!isNaN(startTs) && !isNaN(endTs) && !isNaN(high) && !isNaN(low)) {
const boxHigh = Math.max(high, low);
const boxLow = Math.min(high, low);
const boxColor = strengthColor;
const topSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
topSeries.setData([{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]);
const bottomSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
bottomSeries.setData([{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]);
const leftSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
leftSeries.setData([{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]);
const rightSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
rightSeries.setData([{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]);
if (!tvWidget.series.elementKlcFxBoxSeries) tvWidget.series.elementKlcFxBoxSeries = [];
tvWidget.series.elementKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries);
}
}
// 创建小周期分型标记对象,包含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("11", "");
// 小周期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);
}
});
}
// 次次周期KLC分型
if ($('#showSubSubKlcFxType').is(':checked') && currentData.sub_sub_klc_fx_info && currentData.sub_sub_klc_fx_info.length > 0) {
currentData.sub_sub_klc_fx_info.forEach(function(fx) {
try {
const timestamp = Math.floor(new Date(fx.time).getTime() / 1000);
const price = parseFloat(fx.price);
if (isNaN(timestamp) || isNaN(price)) return;
const strengthColor = '#00897b';
let displayText = (fx.fx_type || '').replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("3", "").replace("41", "").replace("51", "").replace("0", "");
const markerConfig = {
time: timestamp,
position: fx.is_bottom ? 'belowBar' : 'aboveBar',
color: strengthColor,
shape: 'triangle',
text: displayText,
size: (fx.is_strong_fx ? 0.6 : 0.5)
};
allElementFxMarkers.push(markerConfig);
// 画虚线分型框(次次周期)
if (fx.start_time && fx.end_time && fx.high !== null && fx.high !== undefined && fx.low !== null && fx.low !== undefined) {
const startTs = Math.floor(new Date(fx.start_time).getTime() / 1000);
const endTs = Math.floor(new Date(fx.end_time).getTime() / 1000);
const high = parseFloat(fx.high);
const low = parseFloat(fx.low);
if (!isNaN(startTs) && !isNaN(endTs) && !isNaN(high) && !isNaN(low)) {
const boxHigh = Math.max(high, low);
const boxLow = Math.min(high, low);
const boxColor = strengthColor;
const topSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
topSeries.setData([{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]);
const bottomSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
bottomSeries.setData([{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]);
const leftSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
leftSeries.setData([{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]);
const rightSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
rightSeries.setData([{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]);
if (!tvWidget.series.subSubKlcFxBoxSeries) tvWidget.series.subSubKlcFxBoxSeries = [];
tvWidget.series.subSubKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries);
}
}
} catch (e) { console.error('绘制次次周期KLC分型标记出错:', e); }
});
}
// 将小周期分型标记添加到全局markers中以支持tooltip功能
if (window.fxMarkers) {
window.fxMarkers = [...window.fxMarkers, ...elementFxMarkers];
} else {
window.fxMarkers = elementFxMarkers;
}
// 基于后端提供的 KLC 趋势生成标记(不进行任何计算)
let klcTrendMarkers = [];
try {
if (currentData.klc_trend && currentData.klc_trend.length > 0) {
console.log('KLC趋势点数量:', currentData.klc_trend.length, currentData.klc_trend.slice(0, 3));
// 当前图表的bar时间集合(秒)用于对齐标记到最近的K线
const seriesTimes = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set();
const nearestTime = (target) => {
if (!Array.isArray(candles) || candles.length === 0) return target;
// 简单线性查找(数据量通常可接受),必要时可替换为二分
let best = candles[0].time;
let bestDiff = Math.abs(best - target);
for (let i = 1; i < candles.length; i++) {
const t = candles[i].time;
const d = Math.abs(t - target);
if (d < bestDiff) { best = t; bestDiff = d; }
}
return best;
};
klcTrendMarkers = currentData.klc_trend.map(t => {
const ts = Math.floor(new Date(t.time).getTime() / 1000);
const trendRaw = (t.trend || '').toString().toUpperCase();
let timeAligned = seriesTimes.has(ts) ? ts : nearestTime(ts);
let marker = { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'square', size: 0.8 };
if (trendRaw === 'UP') {
marker = { time: timeAligned, position: 'aboveBar', color: '#00C853', shape: 'arrowUp', size: 0.5 };
} else if (trendRaw === 'DOWN') {
marker = { time: timeAligned, position: 'belowBar', color: '#D32F2F', shape: 'arrowDown', size: 0.5 };
} else if (trendRaw === 'FLAT') {
marker = { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'circle', size: 0.8 };
} else {
// UNKNOWN 或其他
marker = { time: timeAligned, position: 'inBar', color: '#2196F3', shape: 'square', size: 0.8 };
}
return marker;
});
console.log('KLC趋势标记(对齐后)示例:', klcTrendMarkers.slice(0, 5));
}
} catch (e) {
klcTrendMarkers = [];
}
// 暴露到全局以便调试或后续合并
window.klcTrendMarkers = klcTrendMarkers;
// 无论当前显示主/小周期,只要勾选对应Trend,就叠加出来
let trendMarkersToUse = [];
if ($('#showMainTrend').is(':checked')) {
trendMarkersToUse = trendMarkersToUse.concat(window.klcTrendMarkers || []);
}
if ($('#showElementTrend').is(':checked') && currentData.element_klc_trend) {
const candlesTimes = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set();
const nearestTime = (target) => {
if (!Array.isArray(candles) || candles.length === 0) return target;
let best = candles[0].time, bestDiff = Math.abs(best - target);
for (let i = 1; i < candles.length; i++) {
const t = candles[i].time, d = Math.abs(t - target);
if (d < bestDiff) { best = t; bestDiff = d; }
}
return best;
};
const elementMarkers = currentData.element_klc_trend.map(t => {
const ts = Math.floor(new Date(t.time).getTime() / 1000);
const trendRaw = (t.trend || '').toString().toUpperCase();
const timeAligned = candlesTimes.has(ts) ? ts : nearestTime(ts);
if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: '#00C853', shape: 'arrowUp', size: 0.5 };
if (trendRaw === 'DOWN') return { time: timeAligned, position: 'belowBar', color: '#D32F2F', shape: 'arrowDown', size: 0.5 };
if (trendRaw === 'FLAT') return { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'circle', size: 0.8 };
return { time: timeAligned, position: 'inBar', color: '#2196F3', shape: 'square', size: 0.8 };
});
trendMarkersToUse = trendMarkersToUse.concat(elementMarkers);
}
if ($('#showSubSubTrend').is(':checked') && currentData.sub_sub_klc_trend && currentData.sub_sub_klc_trend.length > 0) {
const candlesTimesSs = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set();
const nearestTimeSs = (target) => {
if (!Array.isArray(candles) || candles.length === 0) return target;
let best = candles[0].time, bestDiff = Math.abs(best - target);
for (let i = 1; i < candles.length; i++) {
const t = candles[i].time, d = Math.abs(t - target);
if (d < bestDiff) { best = t; bestDiff = d; }
}
return best;
};
const subSubColor = '#00897b';
const subSubMarkers = currentData.sub_sub_klc_trend.map(t => {
const ts = Math.floor(new Date(t.time).getTime() / 1000);
const trendRaw = (t.trend || '').toString().toUpperCase();
const timeAligned = candlesTimesSs.has(ts) ? ts : nearestTimeSs(ts);
if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: subSubColor, shape: 'arrowUp', size: 0.4 };
if (trendRaw === 'DOWN') return { time: timeAligned, position: 'belowBar', color: subSubColor, shape: 'arrowDown', size: 0.4 };
if (trendRaw === 'FLAT') return { time: timeAligned, position: 'inBar', color: subSubColor, shape: 'circle', size: 0.5 };
return { time: timeAligned, position: 'inBar', color: subSubColor, shape: 'square', size: 0.5 };
});
trendMarkersToUse = trendMarkersToUse.concat(subSubMarkers);
}
// 合并标记并设置
const combinedMarkers = [
...(window.mainFxMarkers || []),
...allElementFxMarkers,
...(window.kluDivMarkersMain || []),
...(window.kluDivMarkersElement || []),
...(window.kluDivMarkersSubSub || []),
...trendMarkersToUse,
...(window.bspMarkers || [])
];
if (combinedMarkers.length > 0) {
console.log(
'合并设置', combinedMarkers.length, '个标记(主周期分型:',
(window.mainFxMarkers || []).length,
'个,小周期分型:', allElementFxMarkers.length,
'个,UnitTF:', (window.unittfMarkers || []).length,
'个,BSP标记:', (window.bspMarkers || []).length,
'个)'
);
// 根据当前主系列类型设置标记
const klineType = ($('#klineType').val() || (showOriginalKline ? 'candlestick' : 'line'));
let targetSeries = null;
if (klineType === 'candlestick') targetSeries = tvWidget.series.candleSeries;
else if (klineType === 'renko') targetSeries = tvWidget.series.renkoSeries;
else if (klineType === 'heikin') targetSeries = tvWidget.series.heikinSeries;
else if (klineType === 'bar') targetSeries = tvWidget.series.barSeries;
else if (klineType === 'line') targetSeries = tvWidget.series.lineSeries;
else if (klineType === 'area') targetSeries = tvWidget.series.areaSeries;
else if (klineType === 'baseline') targetSeries = tvWidget.series.baselineSeries;
else if (klineType === 'klc') targetSeries = tvWidget.series.klcSeries;
if (targetSeries) {
try {
targetSeries.setMarkers(combinedMarkers);
} catch (e) {
console.warn('设置主系列标记失败(可能series已释放):', e);
}
} else {
console.log('未找到主数据系列,无法设置标记');
}
}
} else {
console.log('绘制小周期分型标记 - 已禁用或无数据');
// 计算并缓存KLC趋势标记(即使未启用小周期分型,也应显示趋势)
try {
let klcTrendMarkers = [];
if (currentData.klc_trend && currentData.klc_trend.length > 0) {
console.log('KLC趋势点数量:', currentData.klc_trend.length, currentData.klc_trend.slice(0, 3));
const seriesTimes = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set();
const nearestTime = (target) => {
if (!Array.isArray(candles) || candles.length === 0) return target;
let best = candles[0].time;
let bestDiff = Math.abs(best - target);
for (let i = 1; i < candles.length; i++) {
const t = candles[i].time;
const d = Math.abs(t - target);
if (d < bestDiff) { best = t; bestDiff = d; }
}
return best;
};
klcTrendMarkers = currentData.klc_trend.map(t => {
const ts = Math.floor(new Date(t.time).getTime() / 1000);
const trendRaw = (t.trend || '').toString().toUpperCase();
const timeAligned = seriesTimes.has(ts) ? ts : nearestTime(ts);
if (trendRaw === 'UP') {
return { time: timeAligned, position: 'aboveBar', color: '#00C853', shape: 'arrowUp', size: 0.5 };
} else if (trendRaw === 'DOWN') {
return { time: timeAligned, position: 'belowBar', color: '#D32F2F', shape: 'arrowDown', size: 0.5 };
} else if (trendRaw === 'FLAT') {
return { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'circle', size: 0.8 };
} else {
return { time: timeAligned, position: 'inBar', color: '#2196F3', shape: 'square', size: 0.8 };
}
});
console.log('KLC趋势标记(对齐后)示例:', klcTrendMarkers.slice(0, 5));
}
window.klcTrendMarkers = klcTrendMarkers;
} catch (e) {
window.klcTrendMarkers = [];
}
// 与上方一致:勾选哪个Trend就显示哪个
let trendMarkersToUse = [];
if ($('#showMainTrend').is(':checked')) {
trendMarkersToUse = trendMarkersToUse.concat(window.klcTrendMarkers || []);
}
if ($('#showElementTrend').is(':checked') && currentData.element_klc_trend) {
const candlesTimes = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set();
const nearestTime = (target) => {
if (!Array.isArray(candles) || candles.length === 0) return target;
let best = candles[0].time, bestDiff = Math.abs(best - target);
for (let i = 1; i < candles.length; i++) {
const t = candles[i].time, d = Math.abs(t - target);
if (d < bestDiff) { best = t; bestDiff = d; }
}
return best;
};
const elementMarkers = currentData.element_klc_trend.map(t => {
const ts = Math.floor(new Date(t.time).getTime() / 1000);
const trendRaw = (t.trend || '').toString().toUpperCase();
const timeAligned = candlesTimes.has(ts) ? ts : nearestTime(ts);
if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: '#00C853', shape: 'arrowUp', size: 0.5 };
if (trendRaw === 'DOWN') return { time: timeAligned, position: 'belowBar', color: '#D32F2F', shape: 'arrowDown', size: 0.5 };
if (trendRaw === 'FLAT') return { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'circle', size: 0.8 };
return { time: timeAligned, position: 'inBar', color: '#2196F3', shape: 'square', size: 0.8 };
});
trendMarkersToUse = trendMarkersToUse.concat(elementMarkers);
}
if ($('#showSubSubTrend').is(':checked') && currentData.sub_sub_klc_trend && currentData.sub_sub_klc_trend.length > 0) {
const candlesTimesSs2 = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set();
const nearestTimeSs2 = (target) => {
if (!Array.isArray(candles) || candles.length === 0) return target;
let best = candles[0].time, bestDiff = Math.abs(best - target);
for (let i = 1; i < candles.length; i++) {
const t = candles[i].time, d = Math.abs(t - target);
if (d < bestDiff) { best = t; bestDiff = d; }
}
return best;
};
const subSubColor2 = '#00897b';
const subSubMarkers2 = currentData.sub_sub_klc_trend.map(t => {
const ts = Math.floor(new Date(t.time).getTime() / 1000);
const trendRaw = (t.trend || '').toString().toUpperCase();
const timeAligned = candlesTimesSs2.has(ts) ? ts : nearestTimeSs2(ts);
if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: subSubColor2, shape: 'arrowUp', size: 0.4 };
if (trendRaw === 'DOWN') return { time: timeAligned, position: 'belowBar', color: subSubColor2, shape: 'arrowDown', size: 0.4 };
if (trendRaw === 'FLAT') return { time: timeAligned, position: 'inBar', color: subSubColor2, shape: 'circle', size: 0.5 };
return { time: timeAligned, position: 'inBar', color: subSubColor2, shape: 'square', size: 0.5 };
});
trendMarkersToUse = trendMarkersToUse.concat(subSubMarkers2);
}
// 这里的 onlyMainAndU 实际上是「最终要挂到主K线上」的一组标记
// 之前没有把 window.bspMarkers 合进去,导致上面已经合并了 BSP 标记,
// 但在这里再次调用 setMarkers 时把 BSP 覆盖掉了,从而前端看不到买卖点。
// 修复:把 BSP 标记一并合并进来。
const onlyMainAndU = [
...(window.mainFxMarkers || []),
...(window.kluDivMarkersMain || []),
...(window.kluDivMarkersElement || []),
...(window.kluDivMarkersSubSub || []),
...trendMarkersToUse,
...(window.bspMarkers || [])
];
if (onlyMainAndU.length > 0) {
console.log('仅设置', onlyMainAndU.length, '个主周期/UnitTF标记(主周期分型:', (window.mainFxMarkers || []).length, 'UnitTF:', (window.unittfMarkers || []).length, '');
// 根据当前主系列类型设置标记
const klineType2 = ($('#klineType').val() || (showOriginalKline ? 'candlestick' : 'line'));
let targetSeries2 = null;
if (klineType2 === 'candlestick') targetSeries2 = tvWidget.series.candleSeries;
else if (klineType2 === 'renko') targetSeries2 = tvWidget.series.renkoSeries;
else if (klineType2 === 'heikin') targetSeries2 = tvWidget.series.heikinSeries;
else if (klineType2 === 'bar') targetSeries2 = tvWidget.series.barSeries;
else if (klineType2 === 'line') targetSeries2 = tvWidget.series.lineSeries;
else if (klineType2 === 'area') targetSeries2 = tvWidget.series.areaSeries;
else if (klineType2 === 'baseline') targetSeries2 = tvWidget.series.baselineSeries;
else if (klineType2 === 'klc') targetSeries2 = tvWidget.series.klcSeries;
if (targetSeries2) {
try {
targetSeries2.setMarkers(onlyMainAndU);
} catch (e) {
console.warn('设置主系列标记失败(可能series已释放):', e);
}
} else {
console.log('未找到主数据系列,无法设置标记');
}
} else {
console.log('没有分型标记需要显示,清空图表标记');
// 清空图表上的所有主系列标记
const klineType3 = ($('#klineType').val() || (showOriginalKline ? 'candlestick' : 'line'));
let targetSeries3 = null;
if (klineType3 === 'candlestick') targetSeries3 = tvWidget.series.candleSeries;
else if (klineType3 === 'renko') targetSeries3 = tvWidget.series.renkoSeries;
else if (klineType3 === 'heikin') targetSeries3 = tvWidget.series.heikinSeries;
else if (klineType3 === 'bar') targetSeries3 = tvWidget.series.barSeries;
else if (klineType3 === 'line') targetSeries3 = tvWidget.series.lineSeries;
else if (klineType3 === 'area') targetSeries3 = tvWidget.series.areaSeries;
else if (klineType3 === 'baseline') targetSeries3 = tvWidget.series.baselineSeries;
else if (klineType3 === 'klc') targetSeries3 = tvWidget.series.klcSeries;
if (targetSeries3) {
try {
targetSeries3.setMarkers([]);
} catch (e) {
console.warn('清空主系列标记失败(可能series已释放):', e);
}
}
}
}
// 同步所有图表的时间轴配置
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();
// 仅在没有待恢复视图时,设置默认可见范围
const totalBars = candles ? candles.length : 0;
const visibleBarsCount = 200;
const hasPendingRestoreView = !!window._pendingRestoreView;
if (!hasPendingRestoreView) {
// 显示最近 200 根K线而非全部挤压(避免K线过多时重叠)
if (totalBars > visibleBarsCount) {
const rangeFrom = totalBars - visibleBarsCount;
const rangeTo = totalBars + 12;
mainChart.timeScale().setVisibleLogicalRange({ from: rangeFrom, to: rangeTo });
} else {
mainChart.timeScale().fitContent();
}
}
// 立即同步其他图表到主图表的范围
setTimeout(() => {
const logRange = mainChart.timeScale().getVisibleLogicalRange();
if (logRange) {
console.log('🔧 同步可见范围:', logRange);
volumeChart.timeScale().setVisibleLogicalRange(logRange);
atrChart.timeScale().setVisibleLogicalRange(logRange);
if (showMacd && macdChart) {
macdChart.timeScale().setVisibleLogicalRange(logRange);
}
if (showMacd && chanMacdChart) {
chanMacdChart.timeScale().setVisibleLogicalRange(logRange);
}
console.log('🔧 时间轴同步完成');
}
}, 50);
// 保存图表对象
tvWidget.mainChart = mainChart;
tvWidget.volumeChart = volumeChart;
tvWidget.atrChart = atrChart;
tvWidget.macdChart = macdChart;
tvWidget.chanMacdChart = chanMacdChart;
tvWidget.state.isInitialized = true;
// 注册窗口卸载时释放资源,避免GPU内存泄漏
window.onbeforeunload = function() {
try {
if (tvWidget && tvWidget.state && tvWidget.state.isInitialized) {
if (tvWidget.mainChart && typeof tvWidget.mainChart.remove === 'function') tvWidget.mainChart.remove();
if (tvWidget.volumeChart && typeof tvWidget.volumeChart.remove === 'function') tvWidget.volumeChart.remove();
if (tvWidget.macdChart && typeof tvWidget.macdChart.remove === 'function') tvWidget.macdChart.remove();
if (tvWidget.chanMacdChart && typeof tvWidget.chanMacdChart.remove === 'function') tvWidget.chanMacdChart.remove();
if (tvWidget.atrChart && typeof tvWidget.atrChart.remove === 'function') tvWidget.atrChart.remove();
}
} catch (e) {}
};
// 初始化默认均线/布林带配置(仅在首次初始化时)
if (!hasInitializedDefaultMAs && movingAverages.length === 0) {
console.log('初始化默认均线与布林带指标');
if (typeof maIdCounter !== 'number' || !Number.isFinite(maIdCounter)) {
maIdCounter = 0;
}
if (typeof bbIdCounter !== 'number' || !Number.isFinite(bbIdCounter)) {
bbIdCounter = 0;
}
const defaultMAs = [
{ type: 'EMA', length: 13, color: '#800080', name: 'EMA13', visible: true }, // 紫色
{ type: 'EMA', length: 26, color: '#FF8C00', name: 'EMA26', visible: true }, // 橙色
{ type: 'EMA', length: 52, color: '#000000', name: 'EMA52', visible: false }, // 黑色
{ type: 'EMA', length: 104, color: '#1E90FF', name: 'EMA104', visible: false }, // 蓝色
{ type: 'EMA', length: 156, color: '#F700FF', name: 'EMA156', visible: false } // 粉色
];
defaultMAs.forEach(ma => {
const config = {
id: ++maIdCounter,
type: ma.type,
length: ma.length,
source: 'close',
smoothType: 'none',
smoothLength: 3,
lineWidth: 1, // 1px线宽
lineStyle: 0, // 实线
color: ma.color,
visible: ma.visible
};
movingAverages.push(config);
console.log(`添加默认${ma.name}:`, ma.color);
});
if (bollingerBands.length === 0) {
const defaultBB = {
id: ++bbIdCounter,
type: 'Bollinger Bands',
length: 20,
upperMultiplier: 2,
lowerMultiplier: 2,
source: 'close',
lineWidth: 1,
lineStyle: 0,
upperColor: '#ff6b6b',
middleColor: '#ffffff',
lowerColor: '#ff6b6b',
visible: false
};
bollingerBands.push(defaultBB);
console.log('添加默认布林带: BB(20, 2, 2)');
}
console.log('默认指标配置完成,当前均线数量', movingAverages.length, '布林带数量', bollingerBands.length);
hasInitializedDefaultMAs = true;
}
// 添加均线到图表
addMovingAveragesToChart(candles);
// 添加布林带到图表
addBollingerBandsToChart(candles);
// 更新技术指标面板显示
updateIndicatorPanel();
// 绑定同步事件
bindSyncEvents(mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, chanMacdChartContainer, mainChart, volumeChart, atrChart, macdChart, chanMacdChart, showMacd);
// 最终确保所有图表时间轴对齐(同时恢复刷新前保存的缩放/位置)
setTimeout(() => {
const allCharts = [mainChart, volumeChart, atrChart];
if (showMacd && macdChart) allCharts.push(macdChart);
if (showMacd && chanMacdChart) allCharts.push(chanMacdChart);
// 检查是否有待恢复的视图(缩放 + 位置)
const pending = window._pendingRestoreView;
window._pendingRestoreView = null;
if (pending) {
// 恢复刷新前的缩放和位置(优先可见范围/逻辑范围,最后回退到滚动位置)
console.log('📌 恢复图表视图:', JSON.stringify(pending));
restoreChartViewState(allCharts, pending);
} else {
// 无保存视图,正常同步主图到子图
const visibleRange = mainChart.timeScale().getVisibleRange();
if (visibleRange) {
console.log('🔧 最终同步可见范围:', visibleRange);
[volumeChart, atrChart].concat(
showMacd && macdChart ? [macdChart] : [],
showMacd && chanMacdChart ? [chanMacdChart] : []
).forEach(c => {
try { c.timeScale().setVisibleRange(visibleRange); } catch(e) {}
});
}
}
console.log('🔧 最终时间轴对齐完成');
}, 150);
// 只有在时间输入框都为空时才设置图表默认时间范围
if (!$('#start_time').val() && !$('#end_time').val()) {
setDefaultTimeRange();
}
// 添加买卖点提示
// 初始化 tooltip 与 U 显示状态
window.showUOnMain = $('#toggleUOnMain').is(':checked');
window.showUOnElement = $('#toggleUOnElement').is(':checked');
setupTooltip(mainChart, [], [], mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, chanMacdChartContainer, volumeChart, atrChart, macdChart, chanMacdChart, showMacd);
// 更新EMA52显示
if (currentData) {
updateEMA52Display(currentData);
}
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 useSubSubPeriod = $('#subSubPeriodKline').is(':checked') &&
currentData.sub_sub_timeframe &&
currentData.sub_sub_kline_data &&
Array.isArray(currentData.sub_sub_kline_data);
const useElementPeriod = !useSubSubPeriod &&
$('#elementPeriodKline').is(':checked') &&
currentData.element_timeframe &&
currentData.element_kline_data &&
Array.isArray(currentData.element_kline_data);
// 转换K线数据
let candles = [];
if (useSubSubPeriod) {
console.log('使用次次周期K线数据');
candles = currentData.sub_sub_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 (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),
};
});
}
// 更新主系列数据(根据klineType)
const klineType = ($('#klineType').val() || (showOriginalKline ? 'candlestick' : 'line'));
if (klineType === 'candlestick' && tvWidget.series.candleSeries) {
tvWidget.series.candleSeries.setData(candles);
} else if (klineType === 'renko' && tvWidget.series.renkoSeries) {
const bricks = buildRenkoFromCandles(candles);
tvWidget.series.renkoSeries.setData(bricks);
} else if (klineType === 'heikin' && tvWidget.series.heikinSeries) {
const hk = buildHeikinFromCandles(candles);
tvWidget.series.heikinSeries.setData(hk);
} else if (klineType === 'bar' && tvWidget.series.barSeries) {
tvWidget.series.barSeries.setData(candles);
} else if (klineType === 'line' && tvWidget.series.lineSeries) {
const lineData = candles.map(c => ({ time: c.time, value: c.close }));
tvWidget.series.lineSeries.setData(lineData);
} else if (klineType === 'area' && tvWidget.series.areaSeries) {
const areaData = candles.map(c => ({ time: c.time, value: c.close }));
tvWidget.series.areaSeries.setData(areaData);
} else if (klineType === 'baseline' && tvWidget.series.baselineSeries) {
const baseData = candles.map(c => ({ time: c.time, value: c.close }));
tvWidget.series.baselineSeries.setData(baseData);
} else if (klineType === 'klc' && tvWidget.series.klcSeries) {
const klcCandles = buildKLCFromAnalysis(currentData);
tvWidget.series.klcSeries.setData(klcCandles);
}
// 更新均线数据
addMovingAveragesToChart(candles);
// 更新布林带数据
addBollingerBandsToChart(candles);
// 更新成交量数据
let volumes = [];
if (useSubSubPeriod && currentData.sub_sub_kline_data && Array.isArray(currentData.sub_sub_kline_data)) {
volumes = currentData.sub_sub_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(40, 167, 69, 0.5)' : 'rgba(220, 53, 69, 0.5)',
};
});
} else 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(40, 167, 69, 0.5)' : 'rgba(220, 53, 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(40, 167, 69, 0.5)' : 'rgba(220, 53, 69, 0.5)',
};
});
}
if (tvWidget.series.volumeSeries) {
tvWidget.series.volumeSeries.setData(volumes);
}
// 更新ATR数据
if (tvWidget.series.atrLineSeries) {
const atrData = [];
const atrDataSource = useSubSubPeriod ?
(currentData.sub_sub_atr || currentData.atr) :
(useElementPeriod ? (currentData.element_atr || currentData.atr) : currentData.atr);
if (atrDataSource && Array.isArray(atrDataSource)) {
const klineDataSource = useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (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(40, 167, 69, 0.5)' : 'rgba(220, 53, 69, 0.5)'
});
}
}
tvWidget.series.macdLineSeries.setData(macdData);
tvWidget.series.signalLineSeries.setData(signalData);
tvWidget.series.histogramSeries.setData(histogramData);
}
// 更新 ChanMACD 数据与自定义标注
if (tvWidget.series.chanMacdLineSeries && ((useSubSubPeriod && currentData.sub_sub_macd) || (useElementPeriod && currentData.element_macd) || currentData.macd) && (useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (useElementPeriod ? currentData.element_kline_data : currentData.kline_data))) {
const klineDataSource = useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (useElementPeriod ? currentData.element_kline_data : currentData.kline_data);
const macdDataSource = useSubSubPeriod ? (currentData.sub_sub_macd || currentData.macd) : (useElementPeriod ? (currentData.element_macd || currentData.macd) : currentData.macd);
if (macdDataSource && macdDataSource.macd && macdDataSource.signal && macdDataSource.histogram) {
const chanMacdData = [];
const chanSignalData = [];
const chanHistData = [];
for (let i = 0; i < klineDataSource.length; i++) {
const kline = klineDataSource[i];
if (kline && kline.date && i < macdDataSource.macd.length && macdDataSource.macd[i] !== null && macdDataSource.macd[i] !== undefined) {
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
chanMacdData.push({ time: timestamp, value: macdDataSource.macd[i] });
chanSignalData.push({ time: timestamp, value: macdDataSource.signal[i] });
chanHistData.push({ time: timestamp, value: macdDataSource.histogram[i], color: macdDataSource.histogram[i] >= 0 ? 'rgba(40, 167, 69, 0.5)' : 'rgba(220, 53, 69, 0.5)' });
}
}
if (chanMacdData.length > 0) {
tvWidget.series.chanMacdLineSeries.setData(chanMacdData);
tvWidget.series.chanMacdSignalSeries.setData(chanSignalData);
tvWidget.series.chanMacdHistSeries.setData(chanHistData);
}
}
// 重新应用自定义标注(段/UnitTF/HistSet/状态点)
try {
if (typeof clearChanMacdMarkers === 'function') clearChanMacdMarkers();
const cm = useSubSubPeriod ? (currentData.sub_sub_chan_macd || currentData.chan_macd) : (useElementPeriod ? (currentData.element_chan_macd || currentData.chan_macd) : currentData.chan_macd);
const allowU = useSubSubPeriod ? !!window.showUOnSubSub : (useElementPeriod ? !!window.showUOnElement : !!window.showUOnMain);
if (cm && allowU) {
addAllChanMacdMarkers(
cm.seg_list || [],
cm.unittf_list || [],
cm.histset_list || [],
{
high_position_list: cm.high_position_list || [],
high_empty_list: cm.high_empty_list || [],
low_position_list: cm.low_position_list || [],
low_empty_list: cm.low_empty_list || [],
return_zero_list: cm.return_zero_list || [],
cross0_up_list: cm.cross0_up_list || [],
cross0_down_list: cm.cross0_down_list || []
}
);
}
} catch (e) {
console.warn('更新ChanMACD标注失败:', e);
}
}
// 重新显示笔、线段和中枢等图形
redrawFractalElements();
// 更新EMA52显示
updateEMA52Display(currentData);
// 恢复之前的可视范围 - 优先使用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);
if (tvWidget.chanMacdChart) tvWidget.chanMacdChart.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);
if (tvWidget.chanMacdChart) tvWidget.chanMacdChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange);
}
}
console.log('增量更新图表完成');
} catch (e) {
console.error('增量更新图表错误,回退到完全重绘:', e);
// 出错时回退到完全重绘
initTradingView($('#symbol').val(), $('#timeframe').val());
}
}
function bindSyncEvents(mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, chanMacdChartContainer, mainChart, volumeChart, atrChart, macdChart, chanMacdChart, showMacd) {
// 清理上一轮绑定的事件监听器,防止累积
if (window._bindSyncCleanups) {
window._bindSyncCleanups.forEach(fn => { try { fn(); } catch(e) {} });
}
window._bindSyncCleanups = [];
let syncInProgress = false;
// 用于跟踪所有图表的拖动状态 - 在函数内部定义以确保作用域正确
let localDragStates = {
main: false,
volume: false,
atr: false,
macd: false,
chanmacd: false
};
// 同步图表的时间范围
function syncCharts(sourceChart, sourceContainer) {
if (syncInProgress) return;
syncInProgress = true;
try {
if (sourceChart && sourceChart.timeScale) {
const logicalRange = sourceChart.timeScale().getVisibleLogicalRange();
if (logicalRange && logicalRange.from !== undefined && logicalRange.to !== undefined) {
if (sourceChart !== mainChart && mainChart && mainChart.timeScale) {
try { mainChart.timeScale().setVisibleLogicalRange(logicalRange); } catch (e) {}
}
if (sourceChart !== volumeChart && volumeChart && volumeChart.timeScale) {
try { volumeChart.timeScale().setVisibleLogicalRange(logicalRange); } catch (e) {}
}
if (sourceChart !== atrChart && atrChart && atrChart.timeScale) {
try { atrChart.timeScale().setVisibleLogicalRange(logicalRange); } catch (e) {}
}
if (showMacd && macdChart && sourceChart !== macdChart && macdChart.timeScale) {
try { macdChart.timeScale().setVisibleLogicalRange(logicalRange); } catch (e) {}
}
if (showMacd && chanMacdChart && sourceChart !== chanMacdChart && chanMacdChart.timeScale) {
try { chanMacdChart.timeScale().setVisibleLogicalRange(logicalRange); } catch (e) {}
}
if (tvWidget && tvWidget.state) {
tvWidget.state.logicalRange = logicalRange;
try { tvWidget.state.visibleRange = sourceChart.timeScale().getVisibleRange(); } catch (e) {}
}
}
}
} catch (e) {
console.error('同步图表出错:', e);
}
setTimeout(() => { syncInProgress = false; }, 1);
}
// 为每个图表添加事件监听
const addChartSyncEvents = (chartContainer, chart) => {
const chartType = chart === mainChart ? 'main' :
chart === volumeChart ? 'volume' :
chart === atrChart ? 'atr' :
chart === macdChart ? 'macd' :
chart === chanMacdChart ? 'chanmacd' : 'unknown';
const timeRangeHandler = () => {
if (!syncInProgress) {
syncCharts(chart, chartContainer);
}
};
chart.timeScale().subscribeVisibleTimeRangeChange(timeRangeHandler);
window._bindSyncCleanups.push(() => {
try { chart.timeScale().unsubscribeVisibleTimeRangeChange(timeRangeHandler); } catch(e) {}
});
let isScrolling = false;
const mousedownHandler = () => { localDragStates[chartType] = true; };
const mouseupHandler = () => { localDragStates[chartType] = false; };
const mouseleaveHandler = () => { localDragStates[chartType] = false; };
const wheelHandler = () => {
if (!isScrolling) {
isScrolling = true;
setTimeout(() => {
if (!syncInProgress) {
syncCharts(chart, chartContainer);
}
isScrolling = false;
}, 50);
}
};
chartContainer.addEventListener('mousedown', mousedownHandler);
chartContainer.addEventListener('mouseup', mouseupHandler);
chartContainer.addEventListener('mouseleave', mouseleaveHandler);
chartContainer.addEventListener('wheel', wheelHandler);
window._bindSyncCleanups.push(() => {
chartContainer.removeEventListener('mousedown', mousedownHandler);
chartContainer.removeEventListener('mouseup', mouseupHandler);
chartContainer.removeEventListener('mouseleave', mouseleaveHandler);
chartContainer.removeEventListener('wheel', wheelHandler);
});
};
// 添加事件监听
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);
}
if (showMacd && chanMacdChartContainer && chanMacdChart) {
addChartSyncEvents(chanMacdChartContainer, chanMacdChart);
}
// 窗口大小变化时重绘图表 — 使用可清理的方式注册
const resizeHandler = () => {
if (mainChart && mainChartContainer) {
mainChart.applyOptions({ width: mainChartContainer.clientWidth, height: mainChartContainer.clientHeight });
}
if (volumeChart && volumeChartContainer) {
volumeChart.applyOptions({ width: volumeChartContainer.clientWidth, height: volumeChartContainer.clientHeight });
}
if (atrChart && atrChartContainer) {
atrChart.applyOptions({ width: atrChartContainer.clientWidth, height: atrChartContainer.clientHeight });
}
if (showMacd && macdChart && macdChartContainer) {
macdChart.applyOptions({ width: macdChartContainer.clientWidth, height: macdChartContainer.clientHeight });
}
if (showMacd && chanMacdChart && chanMacdChartContainer) {
chanMacdChart.applyOptions({ width: chanMacdChartContainer.clientWidth, height: chanMacdChartContainer.clientHeight });
}
setTimeout(() => { if (mainChart) syncCharts(mainChart, mainChartContainer); }, 200);
};
window.addEventListener('resize', resizeHandler);
window._bindSyncCleanups.push(() => { window.removeEventListener('resize', resizeHandler); });
}
function setupTooltip(mainChart, buyMarkers = [], sellMarkers = [], mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, chanMacdChartContainer, volumeChart, atrChart, macdChart, chanMacdChart, showMacd) {
// 清理上一轮 tooltip 的事件订阅
if (window._tooltipCleanups) {
window._tooltipCleanups.forEach(fn => { try { fn(); } catch(e) {} });
}
window._tooltipCleanups = [];
window.debugMode = true;
// 初始化 U 显示状态(主/次周期分开控制)
const isShowUMain = $('#toggleUOnMain').is(':checked');
const isShowUElement = $('#toggleUOnElement').is(':checked');
window.showUOnMain = isShowUMain;
window.showUOnElement = isShowUElement;
if (!isShowUMain && !isShowUElement) {
// 隐藏时清空子图上的 U 标记
if (tvWidget.series && tvWidget.series.chanMacdLineSeries) {
try { tvWidget.series.chanMacdLineSeries.setMarkers([]); } catch (e) {}
}
if (tvWidget.series && tvWidget.series.chanMacdSignalSeries) {
try { tvWidget.series.chanMacdSignalSeries.setMarkers([]); } catch (e) {}
}
}
// 添加买卖点悬浮提示元素
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) {
const crosshairHandler = (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 existingChanMacdLines = document.querySelectorAll('.chanmacd-crosshair-line');
existingChanMacdLines.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);
}
}
// 如果有ChanMACD图,也在ChanMACD图上绘制垂直线
if (showMacd && chanMacdChart && chanMacdChartContainer) {
const chanMacdTimeCoordinate = chanMacdChart.timeScale().timeToCoordinate(param.time);
if (chanMacdTimeCoordinate !== null) {
const chanMacdChartRect = chanMacdChartContainer.getBoundingClientRect();
const chanMacdLine = document.createElement('div');
chanMacdLine.className = 'chanmacd-crosshair-line';
chanMacdLine.style.position = 'fixed';
chanMacdLine.style.left = (chanMacdChartRect.left + chanMacdTimeCoordinate) + 'px';
chanMacdLine.style.top = chanMacdChartRect.top + 'px';
chanMacdLine.style.width = '1px';
chanMacdLine.style.height = chanMacdChartRect.height + 'px';
chanMacdLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)';
chanMacdLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)';
chanMacdLine.style.pointerEvents = 'none';
chanMacdLine.style.zIndex = '1000';
document.body.appendChild(chanMacdLine);
}
}
}
} 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());
const existingChanMacdLines = document.querySelectorAll('.chanmacd-crosshair-line');
existingChanMacdLines.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.renkoSeries && param.seriesPrices.get(tvWidget.series.renkoSeries)) {
const price = param.seriesPrices.get(tvWidget.series.renkoSeries);
priceInfo = `价格: ${price.toFixed(2)}`;
} else if (tvWidget.series.heikinSeries && param.seriesPrices.get(tvWidget.series.heikinSeries)) {
const price = param.seriesPrices.get(tvWidget.series.heikinSeries);
priceInfo = `价格: ${price.toFixed(2)}`;
} else if (tvWidget.series.barSeries && param.seriesPrices.get(tvWidget.series.barSeries)) {
const price = param.seriesPrices.get(tvWidget.series.barSeries);
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)}`;
} else if (tvWidget.series.areaSeries && param.seriesPrices.get(tvWidget.series.areaSeries)) {
const price = param.seriesPrices.get(tvWidget.series.areaSeries);
priceInfo = `价格: ${price.toFixed(2)}`;
} else if (tvWidget.series.baselineSeries && param.seriesPrices.get(tvWidget.series.baselineSeries)) {
const price = param.seriesPrices.get(tvWidget.series.baselineSeries);
priceInfo = `价格: ${price.toFixed(2)}`;
}
// 如果没有蜡烛图系列价格,尝试从区域图系列获取
else if (tvWidget.series.areaSeries && param.seriesPrices.get(tvWidget.series.areaSeries)) {
const price = param.seriesPrices.get(tvWidget.series.areaSeries);
priceInfo = `价格: ${price.toFixed(2)}`;
}
// 如果没有蜡烛图系列价格,尝试从基线图系列获取
else if (tvWidget.series.baselineSeries && param.seriesPrices.get(tvWidget.series.baselineSeries)) {
const price = param.seriesPrices.get(tvWidget.series.baselineSeries);
priceInfo = `价格: ${price.toFixed(2)}`;
}
}
// 显示自定义时区工具提示,包含价格信息
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.subscribeCrosshairMove(crosshairHandler);
window._tooltipCleanups.push(() => {
try { mainChart.unsubscribeCrosshairMove(crosshairHandler); } catch(e) {}
});
// 处理图表缩放、平移等事件,隐藏提示
const hideTooltipHandler = () => {
tooltipElement.style.display = 'none';
crosshairTooltip.style.display = 'none';
};
mainChart.timeScale().subscribeVisibleTimeRangeChange(hideTooltipHandler);
window._tooltipCleanups.push(() => {
try { mainChart.timeScale().unsubscribeVisibleTimeRangeChange(hideTooltipHandler); } catch(e) {}
});
}
}
// 辅助函数:使用指定时区格式化时间戳
function formatTimeWithTimezone(timestamp, timezone) {
try {
return new Date(timestamp).toLocaleString('zh-CN', {
timeZone: timezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
} catch (e) {
console.error('时区格式化错误:', e);
return new Date(timestamp).toLocaleString();
}
}
function updateTables(currentData) {
// 检查数据有效性
if (!currentData) {
console.error('updateTables: 传入的数据为空');
return;
}
const data = currentData;
const useSubSubPeriod = $('#subSubPeriodKline').is(':checked');
const useElementPeriod = $('#elementPeriodKline').is(':checked');
const periodLabel = useSubSubPeriod ? '次次周期' : (useElementPeriod ? '小周期' : '主周期');
console.log('数据表显示周期选择:', periodLabel);
// 笔数据表更新
if (tables.bi) {
tables.bi.clear().destroy();
}
let biData, biSource;
if (useSubSubPeriod && data.sub_sub_bi_list && data.sub_sub_bi_list.length > 0) {
biData = data.sub_sub_bi_list;
biSource = '次次周期';
} else 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 (useSubSubPeriod && data.sub_sub_seg_list && data.sub_sub_seg_list.length > 0) {
segData = data.sub_sub_seg_list;
uncompletedSegData = data.sub_sub_uncompleted_seg_list || [];
segSource = '次次周期';
} else 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 (useSubSubPeriod && data.sub_sub_zs_list && data.sub_sub_zs_list.length > 0) {
zsData = data.sub_sub_zs_list;
zsSource = '次次周期';
} else 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 (useSubSubPeriod && data.sub_sub_bsp_list && data.sub_sub_bsp_list.length > 0) {
tradePointsData = data.sub_sub_bsp_list;
tradePointsSource = '次次周期';
} else if (useElementPeriod && (data.element_trade_points && data.element_trade_points.length > 0 || data.element_bsp_list && data.element_bsp_list.length > 0)) {
tradePointsData = data.element_trade_points || data.element_bsp_list;
tradePointsSource = '小周期';
} else {
tradePointsData = data.trade_points || data.bsp_list;
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 = useSubSubPeriod ? '次次周期' : (useElementPeriod ? '小周期' : '主周期');
const timeframe = useSubSubPeriod && data.sub_sub_timeframe ? data.sub_sub_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 (useSubSubPeriod && data.sub_sub_kline_data && data.sub_sub_kline_data.length > 0) {
klineData = data.sub_sub_kline_data;
klineSource = '次次周期';
} else 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 (useSubSubPeriod && data.sub_sub_uncompleted_zs_list && data.sub_sub_uncompleted_zs_list.length > 0) {
uncompletedZsData = data.sub_sub_uncompleted_zs_list;
uncompletedZsSource = '次次周期';
} else 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 useSubSubPeriod = $('#subSubPeriodKline').is(':checked');
const useElementPeriod = $('#elementPeriodKline').is(':checked');
const mainTimeframe = $('#timeframe').val();
const elementTimeframe = data.element_timeframe || mainTimeframe;
const subSubTimeframe = data.sub_sub_timeframe || elementTimeframe;
$('#kline-tab, #macd-tab').off('click').on('click', function() {
$('.data-source-info').show();
if (useSubSubPeriod && data.sub_sub_kline_data && data.sub_sub_kline_data.length > 0) {
$('#dataSourceText').html(`当前显示的是<strong>次次周期 (${subSubTimeframe})</strong> 数据`);
} else 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 (useSubSubPeriod && data.sub_sub_bi_list && data.sub_sub_bi_list.length > 0) {
$('#dataSourceText').html(`当前显示的是<strong>次次周期 (${subSubTimeframe})</strong> 笔数据`);
} else 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 (useSubSubPeriod && data.sub_sub_seg_list && data.sub_sub_seg_list.length > 0) {
$('#dataSourceText').html(`当前显示的是<strong>次次周期 (${subSubTimeframe})</strong> 线段数据`);
} else 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 (useSubSubPeriod && data.sub_sub_zs_list && data.sub_sub_zs_list.length > 0) {
$('#dataSourceText').html(`当前显示的是<strong>次次周期 (${subSubTimeframe})</strong> 中枢数据`);
} else 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 (useSubSubPeriod && data.sub_sub_bsp_list && data.sub_sub_bsp_list.length > 0) {
$('#dataSourceText').html(`当前显示的是<strong>次次周期 (${subSubTimeframe})</strong> 买卖点数据`);
} else 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');
const mainDefault = window.DEFAULT_MAIN_TIMEFRAME || $('#timeframe option:first').val();
const elementDefault = window.DEFAULT_ELEMENT_TIMEFRAME || $('#elementTimeframe option:first').val();
if (mainDefault) {
$('#timeframe').val(mainDefault);
}
if (elementDefault) {
$('#elementTimeframe').val(elementDefault);
}
// 测试打印时区偏移量
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 {
// 清理EMA52系列
clearEMA52Series();
// 销毁主图表及其关联的线系列
tvWidget.mainChart = null;
tvWidget.volumeChart = null;
tvWidget.atrChart = null;
tvWidget.macdChart = null;
// 重置系列数据
tvWidget.series = {
candleSeries: null,
lineSeries: null,
barSeries: null,
areaSeries: null,
baselineSeries: null,
renkoSeries: null,
volumeSeries: null,
atrLineSeries: null,
macdLineSeries: null,
signalLineSeries: null,
histogramSeries: null,
mainBiSeries: [],
mainSegSeries: [],
mainZsSeries: [],
mainUncompletedZsSeries: [],
elementBiSeries: [],
elementSegSeries: [],
elementZsSeries: [],
elementUncompletedZsSeries: [],
tradePointSeries: [],
mainBollingerSeries: [],
elementBollingerSeries: [],
maSeries: [], // 添加均线系列
bbSeries: [], // 添加布林带系列
ema52Series: [] // 添加EMA52系列数组
};
} 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);
// 添加页面滚动事件监听器,清除十字线延长线
$(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());
const existingChanMacdLines = document.querySelectorAll('.chanmacd-crosshair-line');
existingChanMacdLines.forEach(line => line.remove());
} catch (e) {
console.debug('清除滚动中的十字线时出错:', e);
}
});
});
});
// 自动刷新相关变量
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) return;
const mainChart = tvWidget.mainChart;
const logicalRange = mainChart.timeScale().getVisibleLogicalRange();
const visibleRange = mainChart.timeScale().getVisibleRange();
// 确保使用主周期的K线和MACD数据
if (currentData.original_kline_data) {
currentData.kline_data = currentData.original_kline_data;
}
if (currentData.original_macd) {
currentData.macd = currentData.original_macd;
}
// 清除冗余引用,帮助GC回收
delete currentData.original_kline_data;
delete currentData.original_macd;
initTradingView($('#symbol').val(), $('#timeframe').val());
setTimeout(() => {
if (tvWidget && tvWidget.mainChart) {
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);
if (tvWidget.chanMacdChart) tvWidget.chanMacdChart.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);
if (tvWidget.chanMacdChart) tvWidget.chanMacdChart.timeScale().setVisibleRange(visibleRange);
}
}
}, 200);
}
// 只更新分形元素(笔、线段、中枢)的表格数据
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;
}
if (data.element_timeframe) {
$('#elementTimeframe').val(data.element_timeframe);
}
// 保存当前缩放(barSpacing)和滚动位置(scrollPosition)到 window
// tvWidget 会在 initTradingView 内被重建,所以必须存到 window 上
if (tvWidget && tvWidget.mainChart) {
try {
window._pendingRestoreView = captureChartViewState(tvWidget.mainChart);
console.log('📌 保存图表视图:', JSON.stringify(window._pendingRestoreView));
} catch (e) {
console.warn('保存图表视图失败:', e);
window._pendingRestoreView = null;
}
}
initTradingView($('#symbol').val(), $('#timeframe').val());
// 更新表格数据
updateTables(data);
if (currentData && currentData.ema52_dict) {
updateEMA52Display(currentData);
}
}
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);
});
// 绑定布林带显示变更事件
$('#showMainBollinger').change(function() {
updateChartDisplay();
});
$('#showElementBollinger').change(function() {
updateChartDisplay();
});
// 绑定K线周期切换
$('input[name="klinePeriod"]').change(function() {
refreshChart(currentData);
});
// 绑定主图U显示开关
$('#toggleUOnMain').change(function() {
window.showUOnMain = $('#toggleUOnMain').is(':checked');
refreshChartOnly();
});
// 次周期 U 显示开关
$('#toggleUOnElement').change(function() {
window.showUOnElement = $('#toggleUOnElement').is(':checked');
refreshChartOnly();
});
// 买卖点显示开关
$('#showMainBsp').change(function() {
updateChartDisplay();
});
$('#showElementBsp').change(function() {
updateChartDisplay();
});
// 在控制台输出当前显示状态
console.log('当前显示状态:', {
'showOriginalKline': $('#showOriginalKline').is(':checked'),
'showMainBi': $('#showMainBi').is(':checked'),
'showMainSeg': $('#showMainSeg').is(':checked'),
'showMainZs': $('#showMainZs').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)
})
// 获取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股交易时间状态更新器已启动');
}
// 均线系统全局变量
var movingAverages = []; // 存储所有均线配置
var maIdCounter = 0; // 均线ID计数器
// 布林带系统全局变量
var bollingerBands = []; // 存储所有布林带配置
var bbIdCounter = 0; // 布林带ID计数器
// 清理EMA52系列
function clearEMA52Series() {
// 清理所有EMA52相关系列(包括线系列和标记系列)
if (tvWidget && tvWidget.series && tvWidget.series.ema52Series) {
tvWidget.series.ema52Series.forEach(series => {
try {
if (tvWidget.mainChart) {
tvWidget.mainChart.removeSeries(series);
}
} catch (e) {
console.warn('移除EMA52系列失败:', e);
}
});
tvWidget.series.ema52Series = [];
}
console.log('✅ EMA52系列已清理');
}
// 存储上次的EMA52数据,用于比较
let lastEMA52Data = null;
// 更新EMA52显示
function updateEMA52Display(data) {
console.log('更新EMA52显示', data.ema52_dict);
// 检查是否有EMA52数据
if (!data.ema52_dict || Object.keys(data.ema52_dict).length === 0) {
// 即使没有数据也要清理之前的系列
clearEMA52Series();
lastEMA52Data = null;
return;
}
// 检查数据是否与上次相同,如果相同则跳过更新
const currentDataStr = JSON.stringify(data.ema52_dict);
if (lastEMA52Data === currentDataStr) {
console.log('EMA52数据未变化,跳过更新');
return;
}
// 清除之前的EMA52线系列
clearEMA52Series();
// 保存当前数据
lastEMA52Data = currentDataStr;
// 定义时间周期的显示顺序和颜色
const timeframeColors = {
'1m': '#FF0000', // 红色
'3m': '#FF6600', // 橙红色
'5m': '#FF9900', // 橙色
'10m': '#DDAA00', // 黄色
'15m': '#FFCC00', // 黄色
'30m': '#99FF00', // 黄绿色
'1h': '#00FF00', // 绿色
'2h': '#00FF99', // 青绿色
'4h': '#00FFFF', // 青色
'6h': '#0099FF', // 蓝青色
'8h': '#0066FF', // 蓝色
'12h': '#3300FF', // 蓝紫色
'16h': '#6600FF', // 紫色
'1d': '#9900FF', // 紫红色
'2d': '#CC00FF', // 品红色
'3d': '#FF00CC', // 粉红色
};
// 按照预定义顺序排列时间周期
const orderedTimeframes = ['1m', '3m', '5m', '10m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '16h', '1d', '2d', '3d'];
// 获取主图表容器
const chartContainer = document.getElementById('tradingview_chart');
if (!chartContainer || !tvWidget.mainChart) {
return;
}
orderedTimeframes.forEach(timeframe => {
if (data.ema52_dict[timeframe] !== undefined && data.ema52_dict[timeframe] !== null) {
const value = data.ema52_dict[timeframe];
const color = timeframeColors[timeframe] || '#800080';
// 添加虚线到主图表
if (tvWidget.mainChart) {
try {
// 使用LightweightCharts的addLineSeries创建虚线
const lineSeries = tvWidget.mainChart.addLineSeries({
color: color,
lineWidth: 1,
lineStyle: 2, // 虚线样式
title: ``,
lastValueVisible: false, // 不在价格标尺显示数值
priceLineVisible: false, // 不显示默认价格线
crosshairMarkerVisible: false,
priceFormat: {
type: 'price',
precision: 2,
minMove: 0.01,
},
priceScaleId: 'right',
});
// 创建横线数据(使用K线数据的时间范围)
if (data.kline_data && data.kline_data.length > 0) {
const firstKline = data.kline_data[0];
const lastKline = data.kline_data[data.kline_data.length - 1];
const startTime = Math.floor(new Date(firstKline.date).getTime() / 1000);
const endTime = Math.floor(new Date(lastKline.date).getTime() / 1000);
const lineData = [
{ time: startTime, value: value },
{ time: endTime, value: value }
];
lineSeries.setData(lineData);
// 使用标记在K线右侧显示文字
if (data.kline_data && data.kline_data.length > 0) {
const lastKline = data.kline_data[data.kline_data.length - 1];
const lastTime = Math.floor(new Date(lastKline.date).getTime() / 1000);
// 计算时间间隔(用于右偏移)
let timeInterval = 60; // 默认1分钟
if (data.kline_data.length > 1) {
const secondLastKline = data.kline_data[data.kline_data.length - 2];
const secondLastTime = Math.floor(new Date(secondLastKline.date).getTime() / 1000);
timeInterval = lastTime - secondLastTime;
}
// 创建右偏移的时间点(在最后一根K线右边)
const rightOffsetTime = lastTime + timeInterval*100;
// 创建一个新的线系列用于显示文字标记
const markerSeries = tvWidget.mainChart.addLineSeries({
color: 'transparent',
lineWidth: 0,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
priceScaleId: 'right',
});
// 添加一个透明的数据点在右偏移位置
markerSeries.setData([{
time: rightOffsetTime,
value: value
}]);
// 在右偏移位置添加文字标记
markerSeries.setMarkers([{
time: rightOffsetTime,
position: 'inBar',
color: color,
shape: 'square',
text: ` ${timeframe} ${value.toFixed(2)}`,
size: 1,
}]);
// 保存标记系列引用
if (!tvWidget.series.ema52Series) {
tvWidget.series.ema52Series = [];
}
tvWidget.series.ema52Series.push(markerSeries);
}
}
// 保存系列引用以便后续清理
if (!tvWidget.series.ema52Series) {
tvWidget.series.ema52Series = [];
}
tvWidget.series.ema52Series.push(lineSeries);
} catch (e) {
console.warn('添加EMA52虚线失败:', timeframe, e);
}
}
}
});
}
// 获取随机颜色
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('🔄 按钮已重置为添加模式,等待用户主动点击"添加均线"按钮');
}
// 点击弹窗外部关闭(MA
$(document).on('click', '#maConfigModal', function(e) {
if (e.target === this) {
hideMAConfig();
}
});
// 添加均线
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();
// 统一刷新(带重试,确保图表就绪)
refreshIndicatorsNowOrLater(5);
}
// 计算均线数据
function calculateMA() {
if (window.App && window.App.Indicators && typeof window.App.Indicators.calculateMA === 'function') {
return window.App.Indicators.calculateMA.apply(null, arguments);
}
console.warn('calculateMA 未就绪,返回空数组');
return [];
}
// 应用平滑处理
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);
}
// 刷新面板图标与数值
try { updateIndicatorPanel(); } catch(e) {}
} 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);
}
// 刷新面板显示
try { updateIndicatorPanel(); } catch(e) {}
}
// 删除均线
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);
}
// 刷新技术指标面板并关闭可能残留的配置弹窗
try { updateIndicatorPanel(); } catch(e) {}
try { if ($('#maConfigModal').is(':visible')) { hideMAConfig(); } } catch(e) {}
}
// 获取当前K线数据的辅助函数(与基础显示的主/小/次次周期保持一致)
function getCurrentCandleData() {
if (!currentData || !currentData.kline_data) {
return [];
}
const useSubSubPeriod = $('#subSubPeriodKline').is(':checked') &&
currentData.sub_sub_kline_data &&
Array.isArray(currentData.sub_sub_kline_data);
const useElementPeriod = !useSubSubPeriod &&
$('#elementPeriodKline').is(':checked') &&
currentData.element_kline_data &&
Array.isArray(currentData.element_kline_data);
let source = currentData.kline_data;
if (useSubSubPeriod) {
source = currentData.sub_sub_kline_data;
} else if (useElementPeriod) {
source = currentData.element_kline_data;
}
const candles = source.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;
}
// 从蜡烛数据生成 Heikin-Ashi(平均K
function buildHeikinFromCandles(candles) {
if (!Array.isArray(candles) || candles.length === 0) return [];
const result = [];
let prevHaClose = (candles[0].open + candles[0].high + candles[0].low + candles[0].close) / 4;
let prevHaOpen = (candles[0].open + candles[0].close) / 2;
const firstHaHigh = Math.max(candles[0].high, prevHaOpen, prevHaClose);
const firstHaLow = Math.min(candles[0].low, prevHaOpen, prevHaClose);
result.push({ time: candles[0].time, open: prevHaOpen, high: firstHaHigh, low: firstHaLow, close: prevHaClose });
for (let i = 1; i < candles.length; i++) {
const c = candles[i];
const haClose = (c.open + c.high + c.low + c.close) / 4;
const haOpen = (prevHaOpen + prevHaClose) / 2;
const haHigh = Math.max(c.high, haOpen, haClose);
const haLow = Math.min(c.low, haOpen, haClose);
result.push({ time: c.time, open: haOpen, high: haHigh, low: haLow, close: haClose });
prevHaOpen = haOpen;
prevHaClose = haClose;
}
return result;
}
// 从分析数据生成KLC蜡烛数据
function buildKLCFromAnalysis(data) {
if (!data) return [];
// 根据基础显示的K线周期选择:次次周期 / 小周期 / 主周期
const useSubSubPeriod = $('#subSubPeriodKline').is(':checked') &&
data.sub_sub_klc_list &&
Array.isArray(data.sub_sub_klc_list);
const useElementPeriod = !useSubSubPeriod &&
$('#elementPeriodKline').is(':checked') &&
data.element_klc_list &&
Array.isArray(data.element_klc_list);
const klcList = useSubSubPeriod
? data.sub_sub_klc_list
: (useElementPeriod ? data.element_klc_list : data.klc_list);
if (!klcList || !Array.isArray(klcList)) return [];
const klcCandles = [];
// 遍历KLC列表,转换为蜡烛数据格式
klcList.forEach(klc => {
if (!klc || !klc.date) return;
const date = new Date(klc.date);
const timestamp = date.getTime() / 1000;
const candle = {
time: timestamp,
open: klc.open || 0,
high: klc.high || 0,
low: klc.low || 0,
close: klc.close || 0
};
klcCandles.push(candle);
});
return klcCandles;
}
// 计算默认砖大小(优先使用ATR的最新非零值,否则按收盘价的0.5%)
function computeDefaultBrickSize(candles) {
try {
const useElementPeriod = $('#elementPeriodKline').is(':checked') && currentData.element_kline_data && Array.isArray(currentData.element_kline_data);
const atrArr = useElementPeriod ? (currentData.element_atr || currentData.atr) : currentData.atr;
if (Array.isArray(atrArr) && atrArr.length > 0) {
for (let i = atrArr.length - 1; i >= 0; i--) {
const v = parseFloat(atrArr[i]);
if (!isNaN(v) && v > 0) {
return v;
}
}
}
} catch(e) {}
const last = candles && candles.length ? candles[candles.length - 1].close : 0;
const size = last * 0.005; // 默认0.5%
return size > 0 ? size : 1;
}
// 从蜡烛数据生成Renko砖(返回伪K线数据用于CandlestickSeries显示)
function buildRenkoFromCandles(candles) {
if (!Array.isArray(candles) || candles.length === 0) return [];
const tryBuild = (size) => {
const bricks = [];
let lastClose = candles[0].close;
for (let i = 1; i < candles.length; i++) {
// 确保同一根K内多砖的时间唯一且递增
let nextTime = candles[i].time;
const price = candles[i].close;
let diff = price - lastClose;
// 向上生成砖
while (diff >= size) {
const open = lastClose;
const close = lastClose + size;
bricks.push({ time: nextTime, open: open, high: close, low: open, close: close });
lastClose = close;
diff = price - lastClose;
nextTime += 1; // 递增1秒,保证唯一
}
// 向下生成砖
while (diff <= -size) {
const open = lastClose;
const close = lastClose - size;
bricks.push({ time: nextTime, open: open, high: open, low: close, close: close });
lastClose = close;
diff = price - lastClose;
nextTime += 1; // 递增1秒,保证唯一
}
}
return bricks;
};
// 计算初始砖大小,若无砖则逐步缩小后重试,避免不显示
let brickSize = computeDefaultBrickSize(candles);
let result = tryBuild(brickSize);
let attempts = 0;
while (result.length === 0 && attempts < 4) {
brickSize *= 0.5; // 缩小一半后重试
result = tryBuild(brickSize);
attempts += 1;
}
return result;
}
// 指标统一刷新(若图表未就绪则自动重试)
function refreshIndicatorsNowOrLater(retries) {
try {
if (tvWidget && tvWidget.mainChart && currentData && (currentData.kline_data || currentData.element_kline_data)) {
const candles = getCurrentCandleData();
if (candles && candles.length > 0) {
addMovingAveragesToChart(candles);
addBollingerBandsToChart(candles);
try { updateIndicatorPanel(); } catch(e) {}
return;
}
}
} catch(e) {}
if (retries && retries > 0) {
setTimeout(function(){ refreshIndicatorsNowOrLater(retries - 1); }, 200);
}
}
// 添加均线到图表
function addMovingAveragesToChart() {
if (window.App && window.App.Charts && typeof window.App.Charts.addMovingAveragesToChart === 'function') {
return window.App.Charts.addMovingAveragesToChart.apply(null, arguments);
}
console.warn('addMovingAveragesToChart 未就绪');
}
// 监听平滑类型变化(保留,UI 逻辑)
$(document).on('change', '#maSmoothType', function() {
const isNone = $(this).val() === 'none';
$('#maSmoothLength').prop('disabled', isNone);
});
// 重复定义的 showBBConfig 已移除(合并到上方唯一实现)
// 隐藏布林带配置弹窗
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();
// 统一刷新(带重试,确保图表就绪)
refreshIndicatorsNowOrLater(5);
}
// 计算布林带数据
function calculateBB() {
if (window.App && window.App.Indicators && typeof window.App.Indicators.calculateBB === 'function') {
return window.App.Indicators.calculateBB.apply(null, arguments);
}
console.warn('calculateBB 未就绪,返回空数组');
return [];
}
// 更新布林带面板(兼容旧代码)
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); // 同时更新均线
}
// 刷新面板图标与数值
try { updateIndicatorPanel(); } catch(e) {}
} 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);
}
// 刷新面板显示
try { updateIndicatorPanel(); } catch(e) {}
// 刷新面板显示
try { updateIndicatorPanel(); } catch(e) {}
}
// 删除布林带
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);
}
// 刷新技术指标面板并关闭可能残留的配置弹窗
try { updateIndicatorPanel(); } catch(e) {}
try { if ($('#bbConfigModal').is(':visible')) { hideBBConfig(); } } catch(e) {}
}
// 添加布林带到图表
function addBollingerBandsToChart() {
if (window.App && window.App.Charts && typeof window.App.Charts.addBollingerBandsToChart === 'function') {
return window.App.Charts.addBollingerBandsToChart.apply(null, arguments);
}
console.warn('addBollingerBandsToChart 未就绪');
}
// 更新均线预览
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);
// 添加页面滚动事件监听器,清除十字线延长线
$(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());
const existingChanMacdLines = document.querySelectorAll('.chanmacd-crosshair-line');
existingChanMacdLines.forEach(line => line.remove());
} catch (e) {
console.debug('清除滚动中的十字线时出错:', e);
}
});
});
// ====== ChanMACD图表相关函数 ======
// 清除ChanMACD标注
function clearChanMacdMarkers() {
// 清除所有系列的标记
if (tvWidget.series.chanMacdLineSeries) {
tvWidget.series.chanMacdLineSeries.setMarkers([]);
}
if (tvWidget.series.chanMacdSignalSeries) {
tvWidget.series.chanMacdSignalSeries.setMarkers([]);
}
if (tvWidget.series.chanMacdHistSeries) {
tvWidget.series.chanMacdHistSeries.setMarkers([]);
}
// 清空全局UnitTF标记,避免旧数据残留影响主图合并
window.unittfMarkers = [];
}
// 添加所有ChanMACD标记
function addAllChanMacdMarkers(segList, unittfList, histsetList, stateMarkers) {
const macdMarkers = [];
const signalMarkers = [];
const histMarkers = [];
const boundaryMarkers = [];
const uTooltipMarkers = [];
// 添加段标记到MACD线
console.log('处理段标记,段数量:', segList.length);
segList.forEach((seg, index) => {
console.log(`段${index}:`, {
start_time: seg.start_time,
end_time: seg.end_time,
seg_dir: seg.seg_dir,
has_start: !!seg.start_time,
has_end: !!seg.end_time
});
if (!seg.start_time) {
console.log(`段${index}没有开始时间,跳过`);
return;
}
const startTime = new Date(seg.start_time).getTime() / 1000;
console.log(`段${index}开始时间戳:`, startTime);
macdMarkers.push({
time: startTime,
position: 'aboveBar',
color: seg.seg_dir === 'ABOVE' ? '#e91e63' : '#4caf50',
shape: 'square',
text: `S${index}`,
size: 0.5
});
if (seg.end_time) {
const endTime = new Date(seg.end_time).getTime() / 1000;
console.log(`段${index}结束时间戳:`, endTime);
macdMarkers.push({
time: endTime,
position: 'aboveBar',
color: seg.seg_dir === 'ABOVE' ? '#e91e63' : '#4caf50',
shape: 'square',
text: `S${index}E`,
size: 0.5
});
}
});
console.log('生成的段标记数量:', macdMarkers.length);
// 添加UnitTF标记(用于U
console.log('DEBUG: U 源数据条数:', Array.isArray(unittfList) ? unittfList.length : 'not array');
unittfList.forEach((unittf, index) => {
if (!unittf.start_time || unittf.invalid) return;
const startTime = new Date(unittf.start_time).getTime() / 1000;
if (index === 0) {
console.log('DEBUG: U0 示例:', unittf);
}
const startMarker = {
time: startTime,
position: unittf.dir > 0 ? 'aboveBar' : 'belowBar',
color: unittf.dir > 0 ? '#ff9800' : '#9c27b0',
shape: 'circle',
text: `U${index}`,
size: 0.5
};
signalMarkers.push(startMarker);
// tooltip(开始)
uTooltipMarkers.push({
time: startTime,
tooltip: `<div style="color: ${startMarker.color}; font-weight: bold;">
UnitTF(${unittf.dir > 0 ? '正区' : '负区'}) 开始<br>
峰值: ${unittf.peak_abs ?? '-'} 长度: ${unittf.length ?? '-'}<br>
类型: ${unittf.start_type ?? '-'}<br>
时间: ${unittf.start_time}
</div>`
});
if (unittf.end_time) {
const endTime = new Date(unittf.end_time).getTime() / 1000;
const endMarker = {
time: endTime,
position: unittf.dir > 0 ? 'aboveBar' : 'belowBar',
color: unittf.dir > 0 ? '#ff9800' : '#9c27b0',
shape: 'circle',
text: `U${index}E`,
size: 0.5
};
signalMarkers.push(endMarker);
// tooltip(结束)
uTooltipMarkers.push({
time: endTime,
tooltip: `<div style="color: ${endMarker.color}; font-weight: bold;">
UnitTF(${unittf.dir > 0 ? '正区' : '负区'}) 结束<br>
峰值: ${unittf.peak_abs ?? '-'} 长度: ${unittf.length ?? '-'}<br>
类型: ${unittf.end_type ?? '-'}<br>
时间: ${unittf.end_time}
</div>`
});
}
});
// 识别"U 结束与新 U 开始同一根K"的边界,并显示合成标记(即使前一个U是 invalid 也显示边界)
for (let i = 0; i + 1 < unittfList.length; i++) {
const cur = unittfList[i];
const nxt = unittfList[i + 1];
if (!cur.end_time || !nxt.start_time) continue;
const tEnd = new Date(cur.end_time).getTime();
const tStart = new Date(nxt.start_time).getTime();
if (!isNaN(tEnd) && tEnd === tStart) {
const ts = Math.floor(tEnd / 1000);
const color = nxt.dir > 0 ? '#ffb74d' : '#ba68c8';
const marker = {
time: ts,
position: nxt.dir > 0 ? 'aboveBar' : 'belowBar',
color: color,
shape: 'square',
text: 'U↔',
size: 0.6
};
boundaryMarkers.push(marker);
uTooltipMarkers.push({
time: ts,
tooltip: `<div style="color: ${color}; font-weight: bold;">\n U 结束 + 新 U 开始 (边界)<br>\n 结束方向: ${cur.dir > 0 ? '正区' : '负区'} → 新方向: ${nxt.dir > 0 ? '正区' : '负区'}<br>\n 时间: ${nxt.start_time}\n </div>`
});
}
}
// 添加HistSet标记到Histogram
histsetList.forEach((histset, index) => {
if (!histset.start_time) return;
const startTime = new Date(histset.start_time).getTime() / 1000;
if (false) {
histMarkers.push({
time: startTime,
position: histset.histset_dir === 'ABOVE' ? 'aboveBar' : 'belowBar',
color: histset.histset_dir === 'ABOVE' ? '#4caf50' : '#f44336',
shape: 'arrowUp',
text: `H${index}`,
size: 0.5
});
if (histset.end_time) {
const endTime = new Date(histset.end_time).getTime() / 1000;
histMarkers.push({
time: endTime,
position: histset.histset_dir === 'ABOVE' ? 'aboveBar' : 'belowBar',
color: histset.histset_dir === 'ABOVE' ? '#4caf50' : '#f44336',
shape: 'arrowDown',
text: `H${index}E`,
size: 0.5
});
}
}
});
// 设置所有标记
console.log('设置段标记到图表,标记数量:', macdMarkers.length);
if (tvWidget.series.chanMacdLineSeries && macdMarkers.length > 0) {
tvWidget.series.chanMacdLineSeries.setMarkers(macdMarkers);
console.log('✅ 段标记已设置到chanMacdLineSeries');
} else {
console.log('⚠️ 无法设置段标记:', {
hasSeries: !!tvWidget.series.chanMacdLineSeries,
markersLength: macdMarkers.length
});
}
// 保存到全局,供主图与分型一起统一合并绘制(仅在开关开启时)
console.log('DEBUG: U 标记数量:', signalMarkers.length);
const allowUMerge = (window.showUOnMain && window.showUOnElement);
window.unittfMarkers = allowUMerge ? [...signalMarkers, ...boundaryMarkers] : [];
if (uTooltipMarkers.length > 0) {
if (window.fxMarkers) {
window.fxMarkers = [ ...window.fxMarkers, ...uTooltipMarkers ];
} else {
window.fxMarkers = uTooltipMarkers;
}
}
// 同时在ChanMACD的Signal子图上标注U
if (tvWidget.series.chanMacdSignalSeries && (signalMarkers.length > 0 || boundaryMarkers.length > 0)) {
tvWidget.series.chanMacdSignalSeries.setMarkers([...signalMarkers, ...boundaryMarkers]);
}
if (tvWidget.series.chanMacdHistSeries && histMarkers.length > 0) {
tvWidget.series.chanMacdHistSeries.setMarkers(histMarkers);
}
// 添加状态标记
if (stateMarkers) {
addStateMarkers(stateMarkers);
}
}
// 添加状态标记
function addStateMarkers(stateMarkers) {
const stateMarkersList = [];
const stateTooltips = [];
// 调试信息
console.log('DEBUG: 状态标记数据:', stateMarkers);
console.log('DEBUG: 高位列表长度:', stateMarkers.high_position_list ? stateMarkers.high_position_list.length : 0);
console.log('DEBUG: 低位列表长度:', stateMarkers.low_position_list ? stateMarkers.low_position_list.length : 0);
console.log('DEBUG: 高位空列表长度:', stateMarkers.high_empty_list ? stateMarkers.high_empty_list.length : 0);
console.log('DEBUG: 低位空列表长度:', stateMarkers.low_empty_list ? stateMarkers.low_empty_list.length : 0);
// HP/HPE:仅使用高位术语(正负两侧统一展示为HP/HPE)
const hpHeTemp = [];
let hpIdx = 0; // 高位峰值计数
let heIdx = 0; // 高位空(HPE)计数
(stateMarkers.high_position_list || []).forEach(m => {
if (!m.time) return;
const t = new Date(m.time).getTime()/1000;
const color = '#e91e63';
hpHeTemp.push({ t, position: 'belowBar', color, text: `HP${hpIdx}` });
stateTooltips.push({
time: t,
tooltip: `<div style="color:${color};font-weight:bold;">HP${hpIdx} 峰值<br>MACD:${(m.macd??'').toFixed?.(4)||m.macd}<br>SIGNAL:${(m.signal??'').toFixed?.(4)||m.signal}<br>HIST:${(m.macdhist??'').toFixed?.(4)||m.macdhist}</div>`
});
hpIdx++;
});
(stateMarkers.low_position_list || []).forEach(m => {
if (!m.time) return;
const t = new Date(m.time).getTime()/1000;
const color = '#4caf50';
// 低位峰值也统一标记为 HP(按需求不使用 LP)
hpHeTemp.push({ t, position: 'aboveBar', color, text: `HP${hpIdx}` });
stateTooltips.push({
time: t,
tooltip: `<div style=\"color:${color};font-weight:bold;\">HP${hpIdx} 峰值(正区)<br>MACD:${(m.macd??'').toFixed?.(4)||m.macd}<br>SIGNAL:${(m.signal??'').toFixed?.(4)||m.signal}<br>HIST:${(m.macdhist??'').toFixed?.(4)||m.macdhist}</div>`
});
hpIdx++;
});
(stateMarkers.high_empty_list || []).forEach(m => {
if (!m.time) return;
const t = new Date(m.time).getTime()/1000;
const color = '#ff9800';
hpHeTemp.push({ t, position: 'belowBar', color, text: `HPE${heIdx}` });
stateTooltips.push({
time: t,
tooltip: `<div style="color:${color};font-weight:bold;">HPE${heIdx} 黄白交叉<br>MACD:${(m.macd??'').toFixed?.(4)||m.macd}<br>SIGNAL:${(m.signal??'').toFixed?.(4)||m.signal}<br>HIST:${(m.macdhist??'').toFixed?.(4)||m.macdhist}</div>`
});
heIdx++;
});
(stateMarkers.low_empty_list || []).forEach(m => {
if (!m.time) return;
const t = new Date(m.time).getTime()/1000;
const color = '#17a2b8';
// 低位空也统一标记为 HPE(按需求不使用 LPE)
hpHeTemp.push({ t, position: 'aboveBar', color, text: `HPE${heIdx}` });
stateTooltips.push({
time: t,
tooltip: `<div style=\"color:${color};font-weight:bold;\">HPE${heIdx} 黄白交叉(正区)<br>MACD:${(m.macd??'').toFixed?.(4)||m.macd}<br>SIGNAL:${(m.signal??'').toFixed?.(4)||m.signal}<br>HIST:${(m.macdhist??'').toFixed?.(4)||m.macdhist}</div>`
});
heIdx++;
});
hpHeTemp.sort((a,b)=>a.t-b.t).forEach(it => {
stateMarkersList.push({
time: it.t,
position: it.position,
color: it.color,
shape: 'diamond',
text: it.text,
size: 0.65
});
});
// 设置状态标记到 MACD 线
if (tvWidget.series.chanMacdLineSeries && stateMarkersList.length > 0) {
tvWidget.series.chanMacdLineSeries.setMarkers(stateMarkersList);
console.log('✅ 状态标记已设置到MACD线,数量:', stateMarkersList.length);
}
// 将状态标记的 tooltip 合并入全局,主图悬浮可见
if (stateTooltips.length > 0) {
if (window.fxMarkers) {
window.fxMarkers = [...window.fxMarkers, ...stateTooltips];
} else {
window.fxMarkers = stateTooltips;
}
}
}
// 保留原函数用于向后兼容(但不使用)
function addChanMacdSegMarkers(segList) {
const markers = [];
segList.forEach((seg, index) => {
if (!seg.start_time) return;
const startTime = new Date(seg.start_time).getTime() / 1000;
if (false){
// 添加起点标记
markers.push({
time: startTime,
position: 'aboveBar',
color: seg.seg_dir === 'ABOVE' ? '#e91e63' : '#4caf50',
shape: 'square',
text: `S${index}`,
size: 0.5
});
// 如果有结束时间,添加结束标记
if (seg.end_time) {
const endTime = new Date(seg.end_time).getTime() / 1000;
markers.push({
time: endTime,
position: 'aboveBar',
color: seg.seg_dir === 'ABOVE' ? '#e91e63' : '#4caf50',
shape: 'square',
text: `S${index}E`,
size: 0.5
});
}
}
});
// 设置标记到MACD线上
if (tvWidget.series.chanMacdLineSeries && markers.length > 0) {
tvWidget.series.chanMacdLineSeries.setMarkers(markers);
}
}
// 添加UnitTF标注
function addChanMacdUnitTFMarkers(unittfList) {
const markers = [];
unittfList.forEach((unittf, index) => {
if (!unittf.start_time || unittf.invalid) return;
const startTime = new Date(unittf.start_time).getTime() / 1000;
// 添加起点标记
markers.push({
time: startTime,
position: 'belowBar',
color: unittf.dir > 0 ? '#ff9800' : '#9c27b0',
shape: 'circle',
text: `U${index}`,
size: 0.5
});
// 如果有结束时间,添加结束标记
if (unittf.end_time) {
const endTime = new Date(unittf.end_time).getTime() / 1000;
markers.push({
time: endTime,
position: 'belowBar',
color: unittf.dir > 0 ? '#ff9800' : '#9c27b0',
shape: 'circle',
text: `U${index}E`,
size: 0.5
});
}
});
// 设置标记到信号线上
if (tvWidget.series.chanMacdSignalSeries && markers.length > 0) {
tvWidget.series.chanMacdSignalSeries.setMarkers(markers);
}
}
// 添加HistSet标注
function addChanMacdHistSetMarkers(histsetList) {
const markers = [];
histsetList.forEach((histset, index) => {
if (!histset.start_time) return;
const startTime = new Date(histset.start_time).getTime() / 1000;
// 添加起点标记
markers.push({
time: startTime,
position: histset.histset_dir === 'ABOVE' ? 'aboveBar' : 'belowBar',
color: histset.histset_dir === 'ABOVE' ? '#4caf50' : '#f44336',
shape: 'arrowUp',
text: `H${index}`,
size: 0.5
});
// 如果有结束时间,添加结束标记
if (histset.end_time) {
const endTime = new Date(histset.end_time).getTime() / 1000;
markers.push({
time: endTime,
position: histset.histset_dir === 'ABOVE' ? 'aboveBar' : 'belowBar',
color: histset.histset_dir === 'ABOVE' ? '#4caf50' : '#f44336',
shape: 'arrowDown',
text: `H${index}E`,
size: 0.5
});
}
});
// 设置标记到柱状图上
if (tvWidget.series.chanMacdHistSeries && markers.length > 0) {
tvWidget.series.chanMacdHistSeries.setMarkers(markers);
}
}
</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>
<!-- MACD参数配置弹窗 -->
<div id="macdConfigModal" class="macd-config-modal">
<div class="macd-config-content">
<div class="macd-config-title">ChanMACD 参数设置</div>
<div class="macd-config-form">
<div class="macd-config-group">
<label class="macd-config-label">快线周期 (Fast)</label>
<input type="number" id="macdFastPeriod" class="macd-config-input" value="13" min="2" max="200">
<div class="macd-config-hint">默认: 13</div>
</div>
<div class="macd-config-group">
<label class="macd-config-label">慢线周期 (Slow)</label>
<input type="number" id="macdSlowPeriod" class="macd-config-input" value="26" min="2" max="500">
<div class="macd-config-hint">默认: 26</div>
</div>
<div class="macd-config-group">
<label class="macd-config-label">信号线周期 (Signal)</label>
<input type="number" id="macdSignalPeriod" class="macd-config-input" value="9" min="2" max="200">
<div class="macd-config-hint">默认: 9</div>
</div>
</div>
<div class="macd-config-actions">
<button class="macd-config-btn macd-config-btn-secondary" onclick="hideMacdConfig()">取消</button>
<button class="macd-config-btn macd-config-btn-secondary" onclick="resetMacdConfig()">恢复默认</button>
<button class="macd-config-btn macd-config-btn-primary" onclick="saveMacdConfig()">应用</button>
</div>
</div>
</div>
</body>
</html>