10627 lines
563 KiB
HTML
10627 lines
563 KiB
HTML
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>缠论分析系统</title>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css" rel="stylesheet">
|
||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||
<script src="https://cdn.datatables.net/1.11.5/js/jquery.dataTables.min.js"></script>
|
||
<link href="https://cdn.datatables.net/1.11.5/css/jquery.dataTables.min.css" rel="stylesheet">
|
||
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||
<!-- TradingView Widget BEGIN -->
|
||
<script src="https://cdn.jsdelivr.net/npm/lightweight-charts@4.0.1/dist/lightweight-charts.standalone.production.js"></script>
|
||
<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); }
|
||
});
|
||
}
|
||
|
||
// 添加所有笔到图表
|
||
biLines.forEach(line => {
|
||
const lineSeries = mainChart.addLineSeries({
|
||
color: line.color,
|
||
lineWidth: line.lineWidth,
|
||
lineStyle: line.lineStyle || 0, // 支持虚线样式
|
||
lastValueVisible: false,
|
||
priceLineVisible: false,
|
||
});
|
||
|
||
lineSeries.setData([
|
||
{ time: line.startTime, value: line.startPrice },
|
||
{ time: line.endTime, value: line.endPrice }
|
||
]);
|
||
});
|
||
} else {
|
||
console.log('绘制笔 - 已禁用');
|
||
}
|
||
// 显示线段的绘制 - 分别处理主周期、次周期和次次周期
|
||
if ($('#showMainSeg').is(':checked') || $('#showElementSeg').is(':checked') || $('#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')) {
|
||
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: '#000000', name: 'EMA26', visible: true }, // 黑色
|
||
{ type: 'EMA', length: 52, color: '#FF8C00', 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> |