Change layout
This commit is contained in:
+570
-31
@@ -18,7 +18,7 @@
|
||||
body {
|
||||
font-family: "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
padding: 10px;
|
||||
background-color: #f8f9fa;
|
||||
color: #333;
|
||||
}
|
||||
@@ -26,18 +26,17 @@
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
padding: 5px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
margin-bottom: 0px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
.controls {
|
||||
margin-bottom: 20px;
|
||||
padding: 20px;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||
@@ -50,7 +49,7 @@
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
margin-top: 20px;
|
||||
margin-top: 10px;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
@@ -74,10 +73,10 @@
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.data-container {
|
||||
margin-top: 30px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.nav-tabs {
|
||||
margin-bottom: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
@@ -215,12 +214,22 @@
|
||||
margin-left: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* 数据回放相关样式 */
|
||||
#replayStatus {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
#replayProgress {
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1 class="my-4">缠论分析系统</h1>
|
||||
<h5 class="my-1">缠论分析系统</h5>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
@@ -266,7 +275,7 @@
|
||||
</div>
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-5">
|
||||
<div class="col-md-7">
|
||||
<div class="d-flex align-items-center">
|
||||
<label class="form-label me-3 mb-0">基础显示:</label>
|
||||
<div class="form-check form-check-inline">
|
||||
@@ -286,7 +295,7 @@
|
||||
</div>
|
||||
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showVolume" checked>
|
||||
<input class="form-check-input" type="checkbox" id="showVolume">
|
||||
<label class="form-check-label" for="showVolume">成交量</label>
|
||||
</div>
|
||||
|
||||
@@ -294,11 +303,6 @@
|
||||
<input class="form-check-input" type="checkbox" id="showMacd" checked>
|
||||
<label class="form-check-label" for="showMacd">MACD</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showTradePoints" checked>
|
||||
<label class="form-check-label" for="showTradePoints">买卖点</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center mt-2">
|
||||
<label class="form-label me-3 mb-0">主周期:</label>
|
||||
@@ -307,30 +311,30 @@
|
||||
<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" checked>
|
||||
<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" checked>
|
||||
<input class="form-check-input" type="checkbox" id="showMainZs">
|
||||
<label class="form-check-label" for="showMainZs">中枢</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showMainUncompletedZs" checked>
|
||||
<input class="form-check-input" type="checkbox" id="showMainUncompletedZs">
|
||||
<label class="form-check-label" for="showMainUncompletedZs">未完成中枢</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showMainMacdDiv" checked>
|
||||
<input class="form-check-input" type="checkbox" id="showMainMacdDiv">
|
||||
<label class="form-check-label" for="showMainMacdDiv">MACD背离</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showKlcFxType">
|
||||
<label class="form-check-label" for="showKlcFxType">分型类型</label>
|
||||
<input class="form-check-input" type="checkbox" id="showKlcFxType" checked>
|
||||
<label class="form-check-label" for="showKlcFxType">分型</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center mt-2">
|
||||
<label class="form-label me-3 mb-0">次周期:</label>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showElementBi" checked>
|
||||
<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">
|
||||
@@ -338,24 +342,24 @@
|
||||
<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" checked>
|
||||
<input class="form-check-input" type="checkbox" id="showElementZs">
|
||||
<label class="form-check-label" for="showElementZs">中枢</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showElementUncompletedZs" checked>
|
||||
<input class="form-check-input" type="checkbox" id="showElementUncompletedZs">
|
||||
<label class="form-check-label" for="showElementUncompletedZs">未完成中枢</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showElementMacdDiv" checked>
|
||||
<input class="form-check-input" type="checkbox" id="showElementMacdDiv">
|
||||
<label class="form-check-label" for="showElementMacdDiv">MACD背离</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showElementKlcFxType" checked>
|
||||
<label class="form-check-label" for="showElementKlcFxType">分型类型</label>
|
||||
<input class="form-check-input" type="checkbox" id="showElementKlcFxType">
|
||||
<label class="form-check-label" for="showElementKlcFxType">分型</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-5">
|
||||
<div class="d-flex align-items-center">
|
||||
<label for="elementTimeframe" class="form-label me-3 mb-0">次级别时间周期:</label>
|
||||
<select id="elementTimeframe" class="form-select form-select-sm" style="width: auto;">
|
||||
@@ -386,6 +390,33 @@
|
||||
<span id="nextRefreshTime" class="ms-2 text-muted" style="display:none;font-size:0.85rem;"></span>
|
||||
<div id="refreshLoadingSpinner" class="loading-spinner ms-2" style="display:none;"></div>
|
||||
</div>
|
||||
|
||||
<!-- 添加数据回放控制面板 -->
|
||||
<div class="d-flex align-items-center mt-2">
|
||||
<label for="replayInterval" class="form-label me-3 mb-0">数据回放:</label>
|
||||
<select id="replayInterval" class="form-select form-select-sm me-2" style="width: auto;">
|
||||
<option value="0.5">0.5秒</option>
|
||||
<option value="1" selected>1秒</option>
|
||||
<option value="2">2秒</option>
|
||||
<option value="3">3秒</option>
|
||||
<option value="5">5秒</option>
|
||||
</select>
|
||||
<div class="btn-group me-2">
|
||||
<button id="startReplay" class="btn btn-sm btn-success">
|
||||
<i class="bi bi-play-fill"></i> 开始回放
|
||||
</button>
|
||||
<button id="pauseReplay" class="btn btn-sm btn-warning" style="display:none;">
|
||||
<i class="bi bi-pause-fill"></i> 暂停
|
||||
</button>
|
||||
<button id="stopReplay" class="btn btn-sm btn-danger" style="display:none;">
|
||||
<i class="bi bi-stop-fill"></i> 停止
|
||||
</button>
|
||||
</div>
|
||||
<span id="replayStatus" class="ms-2 text-muted" style="font-size:0.85rem;"></span>
|
||||
<div id="replayProgress" class="progress ms-2" style="width: 100px; height: 8px; display: none;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3871,6 +3902,514 @@
|
||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl)
|
||||
})
|
||||
|
||||
// 数据回放相关变量
|
||||
let replayTimer = null;
|
||||
let replayData = null;
|
||||
let currentReplayIndex = 0;
|
||||
let totalReplaySteps = 0;
|
||||
let isReplaying = false;
|
||||
let replayStartTime = null;
|
||||
let replayEndTime = null;
|
||||
|
||||
// 数据回放功能初始化
|
||||
$(document).ready(function() {
|
||||
// 开始回放按钮
|
||||
$('#startReplay').click(function() {
|
||||
startDataReplay();
|
||||
});
|
||||
|
||||
// 暂停回放按钮
|
||||
$('#pauseReplay').click(function() {
|
||||
pauseDataReplay();
|
||||
});
|
||||
|
||||
// 停止回放按钮
|
||||
$('#stopReplay').click(function() {
|
||||
stopDataReplay();
|
||||
});
|
||||
});
|
||||
|
||||
// 开始数据回放
|
||||
function startDataReplay() {
|
||||
// 如果自动刷新开启,需要先关闭
|
||||
if ($('#autoRefresh').is(':checked')) {
|
||||
$('#autoRefresh').prop('checked', false).change();
|
||||
}
|
||||
|
||||
// 检查是否设置了时间范围
|
||||
if (!$('#start_time').val() || !$('#end_time').val()) {
|
||||
alert('请先设置开始时间和结束时间');
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果已经在回放中,且是暂停状态,则继续回放
|
||||
if (isReplaying && replayTimer === null) {
|
||||
continueDataReplay();
|
||||
return;
|
||||
}
|
||||
|
||||
// 停止任何现有的回放
|
||||
stopDataReplay();
|
||||
|
||||
// 获取回放参数
|
||||
replayStartTime = new Date($('#start_time').val()).getTime();
|
||||
replayEndTime = new Date($('#end_time').val()).getTime();
|
||||
|
||||
// 检查时间范围是否有效
|
||||
if (replayStartTime >= replayEndTime) {
|
||||
alert('开始时间必须早于结束时间');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取当前参数
|
||||
const symbol = $('#symbol').val() || 'SOL/USDT:USDT';
|
||||
const timeframe = $('#timeframe').val() || '5m';
|
||||
const timezone = $('#timezone').val() || 'Asia/Shanghai';
|
||||
const elementTimeframe = $('#elementTimeframe').val() || '1m';
|
||||
|
||||
// 显示回放状态
|
||||
$('#replayStatus').text('正在加载数据...');
|
||||
$('#replayProgress').show();
|
||||
$('.progress-bar').css('width', '0%');
|
||||
|
||||
// 切换按钮状态
|
||||
$('#startReplay').hide();
|
||||
$('#stopReplay').show();
|
||||
|
||||
// 尝试使用当前已有数据
|
||||
if (currentData && currentData.kline_data && currentData.kline_data.length > 0) {
|
||||
const currentStart = new Date(currentData.kline_data[0].date).getTime();
|
||||
const currentEnd = new Date(currentData.kline_data[currentData.kline_data.length - 1].date).getTime();
|
||||
|
||||
// 检查当前数据是否覆盖所需的时间范围
|
||||
if (currentStart <= replayStartTime && currentEnd >= replayEndTime) {
|
||||
console.log('使用当前数据进行回放,无需重新请求');
|
||||
replayData = currentData;
|
||||
initReplay();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 请求整个时间范围的数据
|
||||
$.ajax({
|
||||
url: '/api/analyze',
|
||||
data: {
|
||||
symbol: symbol,
|
||||
timeframe: timeframe,
|
||||
timezone: timezone,
|
||||
element_timeframe: elementTimeframe,
|
||||
start_time: replayStartTime,
|
||||
end_time: replayEndTime,
|
||||
elements_only: false
|
||||
},
|
||||
success: function(data) {
|
||||
// 检查是否有有效数据
|
||||
if (!data || !data.kline_data || data.kline_data.length === 0) {
|
||||
$('#replayStatus').text('服务器返回的数据无效');
|
||||
$('#replayProgress').hide();
|
||||
$('#startReplay').show();
|
||||
$('#stopReplay').hide();
|
||||
console.error('服务器返回的数据无效:', data);
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存完整数据
|
||||
replayData = data;
|
||||
|
||||
// 初始化回放
|
||||
initReplay();
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
$('#replayStatus').text('加载数据失败');
|
||||
$('#replayProgress').hide();
|
||||
$('#startReplay').show();
|
||||
$('#stopReplay').hide();
|
||||
alert('加载数据失败: ' + (jqXHR.responseJSON?.error || errorThrown));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化回放
|
||||
function initReplay() {
|
||||
if (!replayData || !replayData.kline_data || replayData.kline_data.length === 0) {
|
||||
$('#replayStatus').text('没有可回放的数据');
|
||||
$('#replayProgress').hide();
|
||||
$('#startReplay').show();
|
||||
$('#stopReplay').hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置回放变量
|
||||
isReplaying = true;
|
||||
currentReplayIndex = 0;
|
||||
totalReplaySteps = replayData.kline_data.length;
|
||||
|
||||
// 更新回放状态
|
||||
$('#replayStatus').text(`准备回放 (0/${totalReplaySteps})`);
|
||||
$('.progress-bar').css('width', '0%');
|
||||
|
||||
// 准备初始数据
|
||||
prepareInitialReplayData();
|
||||
|
||||
// 开始回放
|
||||
continueDataReplay();
|
||||
}
|
||||
|
||||
// 准备初始回放数据
|
||||
function prepareInitialReplayData() {
|
||||
// 创建初始数据集,只包含第一根K线
|
||||
const initialData = $.extend(true, {}, replayData);
|
||||
|
||||
// 确保第一根K线数据有效
|
||||
if (!replayData.kline_data || replayData.kline_data.length === 0) {
|
||||
console.error('无有效K线数据用于回放');
|
||||
$('#replayStatus').text('无有效数据用于回放');
|
||||
$('#replayProgress').hide();
|
||||
return;
|
||||
}
|
||||
|
||||
initialData.kline_data = [replayData.kline_data[0]];
|
||||
|
||||
// 确保初始K线数据的所有字段都有值
|
||||
const firstKline = initialData.kline_data[0];
|
||||
if (firstKline) {
|
||||
// 确保K线数据的每个字段都是有效的数值
|
||||
['open', 'high', 'low', 'close', 'volume'].forEach(field => {
|
||||
if (firstKline[field] === null || firstKline[field] === undefined || isNaN(firstKline[field])) {
|
||||
console.warn(`K线数据的${field}字段无效,设置为默认值`);
|
||||
firstKline[field] = field === 'volume' ? 0 : firstKline.close || 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 清空其他数据列表,因为还没有处理
|
||||
initialData.bi_list = [];
|
||||
initialData.seg_list = [];
|
||||
initialData.zs_list = [];
|
||||
initialData.uncompleted_zs_list = [];
|
||||
initialData.element_bi_list = [];
|
||||
initialData.element_seg_list = [];
|
||||
initialData.element_zs_list = [];
|
||||
initialData.element_uncompleted_zs_list = [];
|
||||
initialData.macd_divergence = [];
|
||||
initialData.klc_fx_type = [];
|
||||
|
||||
// 初始化MACD数据
|
||||
if (initialData.macd && initialData.macd.length > 0) {
|
||||
initialData.macd = [initialData.macd[0]];
|
||||
|
||||
// 确保MACD数据有效
|
||||
const firstMacd = initialData.macd[0];
|
||||
if (firstMacd) {
|
||||
['dif', 'dea', 'macd'].forEach(field => {
|
||||
if (firstMacd[field] === null || firstMacd[field] === undefined || isNaN(firstMacd[field])) {
|
||||
console.warn(`MACD数据的${field}字段无效,设置为0`);
|
||||
firstMacd[field] = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 更新图表,固定坐标轴范围
|
||||
currentData = initialData;
|
||||
|
||||
// 清除当前图表
|
||||
if (tvWidget.state.isInitialized) {
|
||||
// 保存当前的图表可见范围以供后续使用
|
||||
try {
|
||||
tvWidget.state.visibleRange = tvWidget.mainChart.timeScale().getVisibleRange();
|
||||
tvWidget.state.logicalRange = tvWidget.mainChart.timeScale().getVisibleLogicalRange();
|
||||
} catch (e) {
|
||||
console.error('保存图表范围失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新图表
|
||||
refreshChart(initialData);
|
||||
|
||||
// 固定y轴范围
|
||||
fixYAxisRange();
|
||||
}
|
||||
|
||||
// 固定y轴范围,保持图表不因数据变化而变化
|
||||
function fixYAxisRange() {
|
||||
if (!tvWidget.mainChart) return;
|
||||
|
||||
// 获取全部数据的最高价和最低价,为了适当的范围
|
||||
let minPrice = Infinity;
|
||||
let maxPrice = -Infinity;
|
||||
|
||||
// 计算整个数据集的价格范围
|
||||
replayData.kline_data.forEach(kline => {
|
||||
const high = parseFloat(kline.high);
|
||||
const low = parseFloat(kline.low);
|
||||
|
||||
if (high > maxPrice) maxPrice = high;
|
||||
if (low < minPrice) minPrice = low;
|
||||
});
|
||||
|
||||
// 添加一些边距(约5%)
|
||||
const priceRange = maxPrice - minPrice;
|
||||
maxPrice += priceRange * 0.05;
|
||||
minPrice -= priceRange * 0.05;
|
||||
|
||||
// 修复: 使用正确的API设置固定价格范围
|
||||
try {
|
||||
// 禁用自动缩放
|
||||
tvWidget.mainChart.priceScale().applyOptions({
|
||||
autoScale: false,
|
||||
scaleMargins: {
|
||||
top: 0.1,
|
||||
bottom: 0.2
|
||||
},
|
||||
// 直接在options中设置最小和最大值
|
||||
minimumValue: minPrice,
|
||||
maximumValue: maxPrice
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('设置Y轴范围失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 继续回放
|
||||
function continueDataReplay() {
|
||||
if (!isReplaying || !replayData) return;
|
||||
|
||||
// 获取回放速度
|
||||
const interval = parseFloat($('#replayInterval').val()) * 1000 || 1000;
|
||||
|
||||
// 切换按钮状态
|
||||
$('#startReplay').hide();
|
||||
$('#pauseReplay').show();
|
||||
$('#stopReplay').show();
|
||||
|
||||
// 开始回放定时器
|
||||
replayTimer = setInterval(replayNextStep, interval);
|
||||
}
|
||||
|
||||
// 回放下一步
|
||||
function replayNextStep() {
|
||||
if (!isReplaying || currentReplayIndex >= totalReplaySteps) {
|
||||
stopDataReplay();
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新回放状态
|
||||
currentReplayIndex++;
|
||||
updateReplayStatus();
|
||||
|
||||
// 创建截止到当前索引的数据子集
|
||||
const subsetData = createDataSubset(currentReplayIndex);
|
||||
|
||||
// 更新图表
|
||||
currentData = subsetData;
|
||||
refreshChart(subsetData);
|
||||
|
||||
// 固定Y轴范围
|
||||
fixYAxisRange();
|
||||
|
||||
// 保持最新数据可见
|
||||
keepLatestDataVisible();
|
||||
|
||||
// 如果到达最后一步,停止回放
|
||||
if (currentReplayIndex >= totalReplaySteps) {
|
||||
$('#replayStatus').text('回放完成');
|
||||
pauseDataReplay();
|
||||
}
|
||||
}
|
||||
|
||||
// 保持最新数据可见
|
||||
function keepLatestDataVisible() {
|
||||
if (!tvWidget.mainChart) return;
|
||||
|
||||
try {
|
||||
// 获取当前数据的最后一个时间点
|
||||
if (currentData && currentData.kline_data && currentData.kline_data.length > 0) {
|
||||
const lastBar = currentData.kline_data[currentData.kline_data.length - 1];
|
||||
const lastTime = Math.floor(new Date(lastBar.date).getTime() / 1000);
|
||||
|
||||
// 计算时间范围
|
||||
const barCount = Math.min(50, currentData.kline_data.length); // 显示最近的50根K线或所有K线
|
||||
const firstVisibleIndex = Math.max(0, currentData.kline_data.length - barCount);
|
||||
const firstVisibleBar = currentData.kline_data[firstVisibleIndex];
|
||||
const firstTime = Math.floor(new Date(firstVisibleBar.date).getTime() / 1000);
|
||||
|
||||
// 设置时间轴范围
|
||||
tvWidget.mainChart.timeScale().setVisibleRange({
|
||||
from: firstTime,
|
||||
to: lastTime
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('保持最新数据可见失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建数据子集
|
||||
function createDataSubset(endIndex) {
|
||||
// 创建数据副本
|
||||
const subsetData = $.extend(true, {}, replayData);
|
||||
|
||||
// 截取K线数据
|
||||
subsetData.kline_data = replayData.kline_data.slice(0, endIndex);
|
||||
|
||||
// 验证K线数据
|
||||
subsetData.kline_data.forEach((kline, index) => {
|
||||
['open', 'high', 'low', 'close', 'volume'].forEach(field => {
|
||||
if (kline[field] === null || kline[field] === undefined || isNaN(kline[field])) {
|
||||
console.warn(`K线数据[${index}]的${field}字段无效,使用替代值`);
|
||||
// 对于价格字段,尝试使用其他价格字段,或者前一个K线的对应值
|
||||
if (field !== 'volume') {
|
||||
if (field === 'open' && kline.close !== null && !isNaN(kline.close)) {
|
||||
kline[field] = kline.close;
|
||||
} else if (index > 0) {
|
||||
kline[field] = subsetData.kline_data[index-1][field] || 0;
|
||||
} else {
|
||||
kline[field] = 0;
|
||||
}
|
||||
} else {
|
||||
kline[field] = 0; // 对于成交量,设为0
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 处理其他数据结构(笔、线段、中枢等)
|
||||
// 我们需要根据当前K线的时间来过滤其他数据结构
|
||||
try {
|
||||
const currentEndTime = new Date(subsetData.kline_data[endIndex - 1].date).getTime();
|
||||
|
||||
// 过滤笔列表
|
||||
if (subsetData.bi_list && subsetData.bi_list.length) {
|
||||
subsetData.bi_list = filterDataByTime(replayData.bi_list, currentEndTime);
|
||||
}
|
||||
|
||||
// 过滤线段列表
|
||||
if (subsetData.seg_list && subsetData.seg_list.length) {
|
||||
subsetData.seg_list = filterDataByTime(replayData.seg_list, currentEndTime);
|
||||
}
|
||||
|
||||
// 过滤中枢列表
|
||||
if (subsetData.zs_list && subsetData.zs_list.length) {
|
||||
subsetData.zs_list = filterDataByTime(replayData.zs_list, currentEndTime);
|
||||
}
|
||||
|
||||
// 过滤未完成中枢列表
|
||||
if (subsetData.uncompleted_zs_list && subsetData.uncompleted_zs_list.length) {
|
||||
subsetData.uncompleted_zs_list = filterDataByTime(replayData.uncompleted_zs_list, currentEndTime);
|
||||
}
|
||||
|
||||
// 过滤小周期数据
|
||||
if (subsetData.element_bi_list && subsetData.element_bi_list.length) {
|
||||
subsetData.element_bi_list = filterDataByTime(replayData.element_bi_list, currentEndTime);
|
||||
}
|
||||
|
||||
if (subsetData.element_seg_list && subsetData.element_seg_list.length) {
|
||||
subsetData.element_seg_list = filterDataByTime(replayData.element_seg_list, currentEndTime);
|
||||
}
|
||||
|
||||
if (subsetData.element_zs_list && subsetData.element_zs_list.length) {
|
||||
subsetData.element_zs_list = filterDataByTime(replayData.element_zs_list, currentEndTime);
|
||||
}
|
||||
|
||||
if (subsetData.element_uncompleted_zs_list && subsetData.element_uncompleted_zs_list.length) {
|
||||
subsetData.element_uncompleted_zs_list = filterDataByTime(replayData.element_uncompleted_zs_list, currentEndTime);
|
||||
}
|
||||
|
||||
// 过滤MACD
|
||||
if (subsetData.macd && subsetData.macd.length) {
|
||||
subsetData.macd = replayData.macd.slice(0, endIndex);
|
||||
|
||||
// 验证MACD数据
|
||||
subsetData.macd.forEach((item, index) => {
|
||||
['dif', 'dea', 'macd'].forEach(field => {
|
||||
if (item[field] === null || item[field] === undefined || isNaN(item[field])) {
|
||||
console.warn(`MACD数据[${index}]的${field}字段无效,设置为0`);
|
||||
item[field] = 0;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 过滤MACD背离
|
||||
if (subsetData.macd_divergence && subsetData.macd_divergence.length) {
|
||||
subsetData.macd_divergence = filterDataByTime(replayData.macd_divergence, currentEndTime);
|
||||
}
|
||||
|
||||
// 过滤分型类型
|
||||
if (subsetData.klc_fx_type && subsetData.klc_fx_type.length) {
|
||||
subsetData.klc_fx_type = filterDataByTime(replayData.klc_fx_type, currentEndTime, 'time');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('过滤数据时出错:', e);
|
||||
}
|
||||
|
||||
return subsetData;
|
||||
}
|
||||
|
||||
// 根据时间过滤数据
|
||||
function filterDataByTime(dataList, endTime, timeField = 'end_time') {
|
||||
if (!dataList || !Array.isArray(dataList)) return [];
|
||||
|
||||
return dataList.filter(item => {
|
||||
const itemTime = new Date(item[timeField]).getTime();
|
||||
return itemTime <= endTime;
|
||||
});
|
||||
}
|
||||
|
||||
// 更新回放状态显示
|
||||
function updateReplayStatus() {
|
||||
const percent = Math.floor((currentReplayIndex / totalReplaySteps) * 100);
|
||||
$('#replayStatus').text(`回放中 (${currentReplayIndex}/${totalReplaySteps})`);
|
||||
$('.progress-bar').css('width', `${percent}%`);
|
||||
}
|
||||
|
||||
// 暂停回放
|
||||
function pauseDataReplay() {
|
||||
if (replayTimer) {
|
||||
clearInterval(replayTimer);
|
||||
replayTimer = null;
|
||||
}
|
||||
|
||||
// 切换按钮状态
|
||||
$('#pauseReplay').hide();
|
||||
$('#startReplay').show();
|
||||
|
||||
if (currentReplayIndex >= totalReplaySteps) {
|
||||
$('#replayStatus').text('回放完成');
|
||||
} else {
|
||||
$('#replayStatus').text(`已暂停 (${currentReplayIndex}/${totalReplaySteps})`);
|
||||
}
|
||||
}
|
||||
|
||||
// 停止回放
|
||||
function stopDataReplay() {
|
||||
// 清除定时器
|
||||
if (replayTimer) {
|
||||
clearInterval(replayTimer);
|
||||
replayTimer = null;
|
||||
}
|
||||
|
||||
// 重置回放状态
|
||||
isReplaying = false;
|
||||
currentReplayIndex = 0;
|
||||
replayData = null;
|
||||
|
||||
// 更新UI
|
||||
$('#replayStatus').text('');
|
||||
$('#replayProgress').hide();
|
||||
$('#pauseReplay').hide();
|
||||
$('#stopReplay').hide();
|
||||
$('#startReplay').show();
|
||||
|
||||
// 恢复图表自动缩放
|
||||
if (tvWidget.mainChart) {
|
||||
tvWidget.mainChart.priceScale().applyOptions({
|
||||
autoScale: true
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user