Charles 1f394cace3 feat: 添加Kronos Web UI完整功能
- 集成Kronos模型(mini/small/base)
- 支持CPU/CUDA/MPS设备选择
- 时间窗口滑条选择器(400+120固定窗口)
- 预测质量参数控制(Temperature, Top-P, Sample Count)
- 预测vs实际数据对比分析
- 完整的Flask后端和现代化前端界面
- 支持CSV和Feather格式数据文件
- 完整的启动脚本和文档
2025-08-26 16:19:21 +08:00

1239 lines
44 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kronos 金融预测 Web UI</title>
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #333;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 30px;
color: white;
}
.header h1 {
font-size: 2.5rem;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.header p {
font-size: 1.1rem;
opacity: 0.9;
}
.main-content {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 30px;
margin-bottom: 30px;
}
.control-panel {
background: white;
border-radius: 15px;
padding: 25px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
height: fit-content;
}
.control-panel h2 {
color: #4a5568;
margin-bottom: 20px;
font-size: 1.5rem;
border-bottom: 2px solid #e2e8f0;
padding-bottom: 10px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #4a5568;
}
.form-group select,
.form-group input {
width: 100%;
padding: 12px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s ease;
}
.form-group select:focus,
.form-group input:focus {
outline: none;
border-color: #667eea;
}
/* 预测质量参数样式 */
.form-group input[type="range"] {
width: 70%;
margin-right: 10px;
}
.form-group input[type="number"] {
width: 100%;
}
.form-group span {
display: inline-block;
min-width: 40px;
font-weight: 600;
color: #667eea;
}
.form-text {
font-size: 12px;
color: #718096;
margin-top: 5px;
}
.btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
width: 100%;
margin-bottom: 10px;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-secondary {
background: linear-gradient(135deg, #718096 0%, #4a5568 100%);
}
.btn-success {
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
}
.btn-warning {
background: linear-gradient(135deg, #ed8936 0%, #dd6b20 100%);
}
.status {
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
font-weight: 500;
}
.status.success {
background: #c6f6d5;
color: #22543d;
border: 1px solid #9ae6b4;
}
.status.error {
background: #fed7d7;
color: #742a2a;
border: 1px solid #feb2b2;
}
.status.info {
background: #bee3f8;
color: #2a4365;
border: 1px solid #90cdf4;
}
.status.warning {
background: #fef5e7;
color: #744210;
border: 1px solid #fbd38d;
}
.chart-container {
background: white;
border-radius: 15px;
padding: 25px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
.chart-container h2 {
color: #4a5568;
margin-bottom: 20px;
font-size: 1.5rem;
border-bottom: 2px solid #e2e8f0;
padding-bottom: 10px;
}
#chart {
width: 100%;
height: 600px;
}
.data-info {
background: #f7fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
}
.data-info h3 {
color: #4a5568;
margin-bottom: 10px;
font-size: 1.1rem;
}
.data-info p {
margin-bottom: 5px;
color: #4a5568;
}
.data-info strong {
color: #2d3748;
}
/* 时间窗口选择器样式 */
.time-window-container {
background: #f7fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.time-window-container h3 {
color: #4a5568;
margin-bottom: 15px;
font-size: 1.1rem;
}
.time-window-info {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
font-size: 12px;
color: #666;
}
.time-window-slider {
position: relative;
margin-bottom: 10px;
}
.slider-track {
position: relative;
height: 6px;
background: #e2e8f0;
border-radius: 3px;
cursor: pointer;
}
.slider-handle {
position: absolute;
top: -7px;
width: 20px;
height: 20px;
background: #667eea;
border-radius: 50%;
cursor: grab;
border: 2px solid white;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
z-index: 10;
}
.slider-handle:hover {
background: #5a67d8;
transform: scale(1.1);
}
.slider-handle:active {
cursor: grabbing;
}
.slider-selection {
position: absolute;
height: 6px;
background: #48bb78;
border-radius: 3px;
top: 0;
}
.slider-labels {
display: flex;
justify-content: space-between;
font-size: 11px;
color: #999;
margin-top: 5px;
}
/* 对比分析样式 */
.comparison-section {
background: #f7fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 20px;
margin-top: 20px;
}
.comparison-section h3 {
color: #4a5568;
margin-bottom: 15px;
font-size: 1.1rem;
}
.comparison-info {
background: white;
border: 1px solid #e2e8f0;
border-radius: 6px;
padding: 15px;
margin-bottom: 15px;
}
.comparison-table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
.comparison-table th,
.comparison-table td {
border: 1px solid #e2e8f0;
padding: 8px;
text-align: center;
font-size: 12px;
}
.comparison-table th {
background: #f7fafc;
font-weight: 600;
}
.error-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-top: 15px;
}
.error-stat {
background: white;
border: 1px solid #e2e8f0;
border-radius: 6px;
padding: 15px;
text-align: center;
}
.error-stat h4 {
color: #4a5568;
margin-bottom: 5px;
font-size: 0.9rem;
}
.error-stat .value {
font-size: 1.5rem;
font-weight: 600;
color: #667eea;
}
.error-stat .unit {
font-size: 0.8rem;
color: #718096;
}
.loading {
display: none;
text-align: center;
padding: 20px;
}
.loading.show {
display: block;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.model-info {
background: #e6fffa;
border: 1px solid #81e6d9;
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
}
.model-info h3 {
color: #234e52;
margin-bottom: 10px;
font-size: 1.1rem;
}
.model-info p {
margin-bottom: 5px;
color: #234e52;
}
.model-info strong {
color: #0f2027;
}
@media (max-width: 768px) {
.main-content {
grid-template-columns: 1fr;
}
.container {
padding: 10px;
}
.header h1 {
font-size: 2rem;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🚀 Kronos 金融预测 Web UI</h1>
<p>基于AI的金融K线数据预测分析平台</p>
</div>
<div class="main-content">
<div class="control-panel">
<h2>🎯 控制面板</h2>
<!-- 模型选择 -->
<div class="form-group">
<label for="model-select">选择模型:</label>
<select id="model-select">
<option value="">请先加载可用模型</option>
</select>
<small class="form-text">选择要使用的Kronos模型</small>
</div>
<!-- 设备选择 -->
<div class="form-group">
<label for="device-select">选择设备:</label>
<select id="device-select">
<option value="cpu">CPU</option>
<option value="cuda">CUDA (NVIDIA GPU)</option>
<option value="mps">MPS (Apple Silicon)</option>
</select>
<small class="form-text">选择模型运行的设备</small>
</div>
<!-- 模型状态 -->
<div id="model-status" class="status info" style="display: none;">
模型状态信息
</div>
<!-- 加载模型按钮 -->
<button id="load-model-btn" class="btn btn-secondary">
🔄 加载模型
</button>
<hr style="margin: 20px 0; border: 1px solid #e2e8f0;">
<!-- 数据文件选择 -->
<div class="form-group">
<label for="data-file-select">选择数据文件:</label>
<select id="data-file-select">
<option value="">请先加载数据文件列表</option>
</select>
<small class="form-text">从data目录选择K线数据文件</small>
</div>
<button id="load-data-btn" class="btn btn-secondary">
📁 加载数据
</button>
<!-- 数据信息显示 -->
<div id="data-info" class="data-info" style="display: none;">
<h3>📊 数据信息</h3>
<p><strong>行数:</strong> <span id="data-rows">-</span></p>
<p><strong>列数:</strong> <span id="data-cols">-</span></p>
<p><strong>时间范围:</strong> <span id="data-time-range">-</span></p>
<p><strong>价格范围:</strong> <span id="data-price-range">-</span></p>
<p><strong>时间频率:</strong> <span id="data-timeframe">-</span></p>
<p><strong>预测列:</strong> <span id="data-prediction-cols">-</span></p>
</div>
<hr style="margin: 20px 0; border: 1px solid #e2e8f0;">
<!-- 时间窗口选择器 -->
<div class="time-window-container">
<h3>⏰ 时间窗口选择</h3>
<div class="time-window-info">
<span id="window-start">开始: --</span>
<span id="window-end">结束: --</span>
<span id="window-size">窗口大小: 400+120=520个数据点</span>
</div>
<div class="time-window-slider">
<div class="slider-track">
<div class="slider-handle start-handle" id="start-handle"></div>
<div class="slider-selection" id="slider-selection"></div>
<div class="slider-handle end-handle" id="end-handle"></div>
</div>
<div class="slider-labels">
<span id="min-label">最早</span>
<span id="max-label">最新</span>
</div>
</div>
<small class="form-text">拖动滑条选择520个数据点的时间窗口位置绿色区域表示固定的400+120数据点范围</small>
</div>
<!-- 预测参数 -->
<div class="form-group">
<label for="lookback">回看窗口大小:</label>
<input type="number" id="lookback" value="400" readonly>
<small class="form-text">固定为400个数据点</small>
</div>
<div class="form-group">
<label for="pred-len">预测长度:</label>
<input type="number" id="pred-len" value="120" readonly>
<small class="form-text">固定为120个数据点</small>
</div>
<!-- 预测质量参数 -->
<div class="form-group">
<label for="temperature">预测温度 (T):</label>
<input type="range" id="temperature" value="1.0" min="0.1" max="2.0" step="0.1">
<span id="temperature-value">1.0</span>
<small class="form-text">控制预测的随机性,值越高预测越多样化,值越低预测越保守</small>
</div>
<div class="form-group">
<label for="top-p">核采样参数 (top_p):</label>
<input type="range" id="top-p" value="0.9" min="0.1" max="1.0" step="0.1">
<span id="top-p-value">0.9</span>
<small class="form-text">控制预测的多样性,值越高考虑的概率分布越广</small>
</div>
<div class="form-group">
<label for="sample-count">样本数量:</label>
<input type="number" id="sample-count" value="1" min="1" max="5" step="1">
<small class="form-text">生成多个预测样本以提高质量建议1-3个</small>
</div>
<button id="predict-btn" class="btn btn-success" disabled>
🔮 开始预测
</button>
<!-- 加载状态 -->
<div id="loading" class="loading">
<div class="spinner"></div>
<p>正在处理,请稍候...</p>
</div>
</div>
<div class="chart-container">
<h2>📈 预测结果图表</h2>
<div id="chart"></div>
<!-- 对比分析 -->
<div id="comparison-section" class="comparison-section" style="display: none;">
<h3>📊 预测 vs 实际数据对比</h3>
<div id="comparison-info" class="comparison-info">
<p><strong>预测类型:</strong> <span id="prediction-type">-</span></p>
<p><strong>对比数据:</strong> <span id="comparison-data">-</span></p>
</div>
<div class="error-stats">
<div class="error-stat">
<h4>平均绝对误差</h4>
<div class="value" id="mae">-</div>
<div class="unit">价格单位</div>
</div>
<div class="error-stat">
<h4>均方根误差</h4>
<div class="value" id="rmse">-</div>
<div class="unit">价格单位</div>
</div>
<div class="error-stat">
<h4>平均绝对百分比误差</h4>
<div class="value" id="mape">-</div>
<div class="unit">%</div>
</div>
</div>
<div class="error-details">
<h4>详细对比数据:</h4>
<div style="max-height: 300px; overflow-y: auto;">
<table class="comparison-table">
<thead>
<tr>
<th>时间</th>
<th>实际开盘</th>
<th>预测开盘</th>
<th>实际最高</th>
<th>预测最高</th>
<th>实际最低</th>
<th>预测最低</th>
<th>实际收盘</th>
<th>预测收盘</th>
</tr>
</thead>
<tbody id="comparison-tbody">
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// 全局变量
let currentDataFile = null;
let currentDataInfo = null;
let availableModels = [];
let modelLoaded = false;
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
initializeApp();
});
// 初始化应用
async function initializeApp() {
console.log('🚀 初始化 Kronos Web UI...');
// 加载可用模型
await loadAvailableModels();
// 加载数据文件列表
await loadDataFiles();
// 设置事件监听器
setupEventListeners();
// 初始化时间滑块
initializeTimeSlider();
console.log('✅ 应用初始化完成');
}
// 加载可用模型
async function loadAvailableModels() {
try {
const response = await axios.get('/api/available-models');
if (response.data.model_available) {
availableModels = response.data.models;
populateModelSelect();
console.log('✅ 可用模型加载成功:', availableModels);
} else {
console.warn('⚠️ Kronos模型库不可用');
showStatus('warning', 'Kronos模型库不可用将使用模拟预测');
}
} catch (error) {
console.error('❌ 加载可用模型失败:', error);
showStatus('error', '加载可用模型失败');
}
}
// 填充模型选择下拉框
function populateModelSelect() {
const modelSelect = document.getElementById('model-select');
modelSelect.innerHTML = '<option value="">请选择模型</option>';
Object.entries(availableModels).forEach(([key, model]) => {
const option = document.createElement('option');
option.value = key;
option.textContent = `${model.name} (${model.params}) - ${model.description}`;
modelSelect.appendChild(option);
});
}
// 加载模型
async function loadModel() {
const modelKey = document.getElementById('model-select').value;
const device = document.getElementById('device-select').value;
if (!modelKey) {
showStatus('error', '请选择要加载的模型');
return;
}
try {
showLoading(true);
document.getElementById('load-model-btn').disabled = true;
const response = await axios.post('/api/load-model', {
model_key: modelKey,
device: device
});
if (response.data.success) {
modelLoaded = true;
showStatus('success', response.data.message);
updateModelStatus();
document.getElementById('predict-btn').disabled = false;
console.log('✅ 模型加载成功:', response.data.model_info);
} else {
showStatus('error', response.data.error);
}
} catch (error) {
console.error('❌ 模型加载失败:', error);
showStatus('error', `模型加载失败: ${error.response?.data?.error || error.message}`);
} finally {
showLoading(false);
document.getElementById('load-model-btn').disabled = false;
}
}
// 更新模型状态
async function updateModelStatus() {
try {
const response = await axios.get('/api/model-status');
const status = response.data;
if (status.loaded) {
showStatus('success', `模型已加载: ${status.current_model.name} on ${status.current_model.device}`);
} else if (status.available) {
showStatus('info', '模型可用但未加载');
} else {
showStatus('warning', '模型库不可用');
}
} catch (error) {
console.error('❌ 获取模型状态失败:', error);
}
}
// 加载数据文件列表
async function loadDataFiles() {
try {
const response = await axios.get('/api/data-files');
const dataFiles = response.data;
const dataFileSelect = document.getElementById('data-file-select');
dataFileSelect.innerHTML = '<option value="">请选择数据文件</option>';
dataFiles.forEach(file => {
const option = document.createElement('option');
option.value = file.path;
option.textContent = `${file.name} (${file.size})`;
dataFileSelect.appendChild(option);
});
console.log('✅ 数据文件列表加载成功:', dataFiles);
} catch (error) {
console.error('❌ 加载数据文件列表失败:', error);
showStatus('error', '加载数据文件列表失败');
}
}
// 加载数据文件
async function loadData() {
const filePath = document.getElementById('data-file-select').value;
if (!filePath) {
showStatus('error', '请选择要加载的数据文件');
return;
}
try {
showLoading(true);
document.getElementById('load-data-btn').disabled = true;
const response = await axios.post('/api/load-data', {
file_path: filePath
});
if (response.data.success) {
currentDataFile = filePath;
currentDataInfo = response.data.data_info;
showDataInfo(response.data.data_info);
showStatus('success', response.data.message);
// 更新预测按钮状态
if (modelLoaded) {
document.getElementById('predict-btn').disabled = false;
}
console.log('✅ 数据加载成功:', response.data.data_info);
} else {
showStatus('error', response.data.error);
}
} catch (error) {
console.error('❌ 数据加载失败:', error);
showStatus('error', `数据加载失败: ${error.response?.data?.error || error.message}`);
} finally {
showLoading(false);
document.getElementById('load-data-btn').disabled = false;
}
}
// 显示数据信息
function showDataInfo(dataInfo) {
document.getElementById('data-info').style.display = 'block';
document.getElementById('data-rows').textContent = dataInfo.rows;
document.getElementById('data-cols').textContent = dataInfo.columns.length;
document.getElementById('data-time-range').textContent = `${dataInfo.start_date}${dataInfo.end_date}`;
document.getElementById('data-price-range').textContent = `${dataInfo.price_range.min.toFixed(4)} - ${dataInfo.price_range.max.toFixed(4)}`;
document.getElementById('data-timeframe').textContent = dataInfo.timeframe;
document.getElementById('data-prediction-cols').textContent = dataInfo.prediction_columns.join(', ');
// 初始化时间窗口滑条
initializeTimeWindowSlider(dataInfo);
}
// 时间窗口滑条相关变量
let sliderData = null;
let isDragging = false;
let currentHandle = null;
// 初始化时间窗口滑条
function initializeTimeSlider() {
// 设置滑条事件监听器
setupSliderEventListeners();
}
// 设置滑条事件监听器
function setupSliderEventListeners() {
const startHandle = document.getElementById('start-handle');
const endHandle = document.getElementById('end-handle');
const track = document.querySelector('.slider-track');
// 开始拖拽
startHandle.addEventListener('mousedown', (e) => {
isDragging = true;
currentHandle = 'start';
e.preventDefault();
});
endHandle.addEventListener('mousedown', (e) => {
isDragging = true;
currentHandle = 'end';
e.preventDefault();
});
// 拖拽中
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const rect = track.getBoundingClientRect();
const x = e.clientX - rect.left;
const percentage = Math.max(0, Math.min(1, x / rect.width));
if (currentHandle === 'start') {
updateStartHandle(percentage);
} else if (currentHandle === 'end') {
updateEndHandle(percentage);
}
updateSliderFromHandles();
});
// 结束拖拽
document.addEventListener('mouseup', () => {
isDragging = false;
currentHandle = null;
});
// 点击轨道直接设置位置
track.addEventListener('click', (e) => {
const rect = track.getBoundingClientRect();
const x = e.clientX - rect.left;
const percentage = Math.max(0, Math.min(1, x / rect.width));
// 判断点击位置更接近哪个手柄
const startHandle = document.getElementById('start-handle');
const endHandle = document.getElementById('end-handle');
const startRect = startHandle.getBoundingClientRect();
const endRect = endHandle.getBoundingClientRect();
if (Math.abs(x - (startRect.left - rect.left)) < Math.abs(x - (endRect.left - rect.left))) {
updateStartHandle(percentage);
} else {
updateEndHandle(percentage);
}
updateSliderFromHandles();
});
}
// 更新开始手柄位置
function updateStartHandle(percentage) {
const startHandle = document.getElementById('start-handle');
const selection = document.getElementById('slider-selection');
// 固定窗口大小为520个数据点
const windowSize = 520;
const totalRows = sliderData ? sliderData.totalRows : 1000;
const windowPercentage = windowSize / totalRows;
// 确保开始手柄不会导致窗口超出数据范围
if (percentage + windowPercentage > 1) {
percentage = 1 - windowPercentage;
}
startHandle.style.left = (percentage * 100) + '%';
selection.style.left = (percentage * 100) + '%';
selection.style.width = (windowPercentage * 100) + '%';
// 自动调整结束手柄位置,保持固定窗口大小
const endHandle = document.getElementById('end-handle');
endHandle.style.left = ((percentage + windowPercentage) * 100) + '%';
}
// 更新结束手柄位置
function updateEndHandle(percentage) {
const endHandle = document.getElementById('end-handle');
const selection = document.getElementById('slider-selection');
// 固定窗口大小为520个数据点
const windowSize = 520;
const totalRows = sliderData ? sliderData.totalRows : 1000;
const windowPercentage = windowSize / totalRows;
// 确保结束手柄不会导致窗口超出数据范围
if (percentage - windowPercentage < 0) {
percentage = windowPercentage;
}
endHandle.style.left = (percentage * 100) + '%';
selection.style.left = ((percentage - windowPercentage) * 100) + '%';
selection.style.width = (windowPercentage * 100) + '%';
// 自动调整开始手柄位置,保持固定窗口大小
const startHandle = document.getElementById('start-handle');
startHandle.style.left = ((percentage - windowPercentage) * 100) + '%';
}
// 根据手柄位置更新滑条显示
function updateSliderFromHandles() {
const startHandle = document.getElementById('start-handle');
const endHandle = document.getElementById('end-handle');
const startPercentage = parseFloat(startHandle.style.left) / 100;
const endPercentage = parseFloat(endHandle.style.left) / 100;
if (!sliderData) return;
// 计算选中的时间范围
const totalTime = sliderData.endDate.getTime() - sliderData.startDate.getTime();
const startTime = sliderData.startDate.getTime() + (totalTime * startPercentage);
const endTime = sliderData.startDate.getTime() + (totalTime * endPercentage);
const startDate = new Date(startTime);
const endDate = new Date(endTime);
// 更新显示信息
document.getElementById('window-start').textContent = `开始: ${startDate.toLocaleDateString()}`;
document.getElementById('window-end').textContent = `结束: ${endDate.toLocaleDateString()}`;
// 显示固定的窗口大小
document.getElementById('window-size').textContent = `窗口大小: 400 + 120 = 520 个数据点 (固定)`;
// 输入框值保持固定
document.getElementById('lookback').value = 400;
document.getElementById('pred-len').value = 120;
}
// 根据输入框更新滑条
function updateSliderFromInputs() {
if (!sliderData) return;
// 固定窗口大小400 + 120 = 520个数据点
const lookback = 400;
const predLen = 120;
const windowSize = lookback + predLen; // 固定为520
// 计算滑条位置
const totalRows = sliderData.totalRows;
if (windowSize > totalRows) {
// 如果窗口大小超过总数据量,显示错误
showStatus('error', `数据量不足,需要至少${windowSize}个数据点,当前只有${totalRows}`);
return;
}
// 计算滑条位置(默认选择数据的前半部分)
const startPercentage = 0.1; // 从10%开始
const endPercentage = startPercentage + (windowSize / totalRows);
// 更新手柄位置
updateStartHandle(startPercentage);
updateEndHandle(endPercentage);
// 更新显示信息
updateSliderFromHandles();
}
// 初始化时间窗口滑条
function initializeTimeWindowSlider(dataInfo) {
sliderData = {
startDate: new Date(dataInfo.start_date),
endDate: new Date(dataInfo.end_date),
totalRows: dataInfo.rows,
timeframe: dataInfo.timeframe
};
// 设置滑条标签
document.getElementById('min-label').textContent = dataInfo.start_date.split('T')[0];
document.getElementById('max-label').textContent = dataInfo.end_date.split('T')[0];
// 初始化滑条位置
updateSliderFromInputs();
}
// 开始预测
async function startPrediction() {
if (!currentDataFile) {
showStatus('error', '请先加载数据文件');
return;
}
if (!modelLoaded) {
showStatus('error', '请先加载模型');
return;
}
try {
showLoading(true);
document.getElementById('predict-btn').disabled = true;
const lookback = parseInt(document.getElementById('lookback').value);
const predLen = parseInt(document.getElementById('pred-len').value);
// 从时间窗口滑条获取选择的时间范围
const startHandle = document.getElementById('start-handle');
const startPercentage = parseFloat(startHandle.style.left) / 100;
if (!sliderData) {
showStatus('error', '时间窗口滑条未初始化');
return;
}
// 计算选择的时间范围
const totalTime = sliderData.endDate.getTime() - sliderData.startDate.getTime();
const startTime = sliderData.startDate.getTime() + (totalTime * startPercentage);
const startDate = new Date(startTime);
// 获取预测质量参数
const temperature = parseFloat(document.getElementById('temperature').value);
const topP = parseFloat(document.getElementById('top-p').value);
const sampleCount = parseInt(document.getElementById('sample-count').value);
let predictionParams = {
file_path: currentDataFile,
lookback: lookback,
pred_len: predLen,
start_date: startDate.toISOString().slice(0, 16), // 格式化为 YYYY-MM-DDTHH:MM
temperature: temperature,
top_p: topP,
sample_count: sampleCount
};
console.log('🚀 开始预测,参数:', predictionParams);
const response = await axios.post('/api/predict', predictionParams);
if (response.data.success) {
// 显示预测结果
displayPredictionResult(response.data);
showStatus('success', response.data.message);
} else {
showStatus('error', response.data.error);
}
} catch (error) {
console.error('❌ 预测失败:', error);
showStatus('error', `预测失败: ${error.response?.data?.error || error.message}`);
} finally {
showLoading(false);
document.getElementById('predict-btn').disabled = false;
}
}
// 显示预测结果
function displayPredictionResult(result) {
// 显示图表
const chartData = JSON.parse(result.chart);
Plotly.newPlot('chart', chartData.data, chartData.layout);
// 显示对比分析(如果有实际数据)
if (result.has_comparison) {
displayComparisonAnalysis(result);
} else {
document.getElementById('comparison-section').style.display = 'none';
}
}
// 显示对比分析
function displayComparisonAnalysis(result) {
document.getElementById('comparison-section').style.display = 'block';
// 更新对比信息
document.getElementById('prediction-type').textContent = result.prediction_type;
document.getElementById('comparison-data').textContent = `${result.actual_data.length} 个实际数据点`;
// 计算误差统计
const errorStats = getPredictionQuality(result.prediction_results, result.actual_data);
// 显示误差统计
document.getElementById('mae').textContent = errorStats.mae.toFixed(4);
document.getElementById('rmse').textContent = errorStats.rmse.toFixed(4);
document.getElementById('mape').textContent = errorStats.mape.toFixed(2);
// 填充对比表格
fillComparisonTable(result.prediction_results, result.actual_data);
}
// 计算预测质量指标
function getPredictionQuality(predictions, actuals) {
if (!predictions || !actuals || predictions.length === 0 || actuals.length === 0) {
return { mae: 0, rmse: 0, mape: 0 };
}
const minLen = Math.min(predictions.length, actuals.length);
let mae = 0, rmse = 0, mape = 0;
for (let i = 0; i < minLen; i++) {
const pred = predictions[i];
const act = actuals[i];
// 使用收盘价计算误差
const error = Math.abs(pred.close - act.close);
const percentError = (error / act.close) * 100;
mae += error;
rmse += error * error;
mape += percentError;
}
mae /= minLen;
rmse = Math.sqrt(rmse / minLen);
mape /= minLen;
return { mae, rmse, mape };
}
// 填充对比表格
function fillComparisonTable(predictions, actuals) {
const tbody = document.getElementById('comparison-tbody');
tbody.innerHTML = '';
const minLen = Math.min(predictions.length, actuals.length);
for (let i = 0; i < minLen; i++) {
const pred = predictions[i];
const act = actuals[i];
const row = document.createElement('tr');
row.innerHTML = `
<td>${new Date(pred.timestamp).toLocaleString()}</td>
<td>${act.open.toFixed(4)}</td>
<td>${pred.open.toFixed(4)}</td>
<td>${act.high.toFixed(4)}</td>
<td>${pred.high.toFixed(4)}</td>
<td>${act.low.toFixed(4)}</td>
<td>${pred.low.toFixed(4)}</td>
<td>${act.close.toFixed(4)}</td>
<td>${pred.close.toFixed(4)}</td>
`;
tbody.appendChild(row);
}
}
// 设置事件监听器
function setupEventListeners() {
// 加载模型按钮
document.getElementById('load-model-btn').addEventListener('click', loadModel);
// 加载数据按钮
document.getElementById('load-data-btn').addEventListener('click', loadData);
// 预测按钮
document.getElementById('predict-btn').addEventListener('click', startPrediction);
// 预测质量参数滑块
document.getElementById('temperature').addEventListener('input', function() {
document.getElementById('temperature-value').textContent = this.value;
});
document.getElementById('top-p').addEventListener('input', function() {
document.getElementById('top-p-value').textContent = this.value;
});
// 回看窗口大小变化时更新滑条
document.getElementById('lookback').addEventListener('input', updateSliderFromInputs);
document.getElementById('pred-len').addEventListener('input', updateSliderFromInputs);
}
// 显示状态信息
function showStatus(type, message) {
const statusDiv = document.getElementById('model-status');
statusDiv.className = `status ${type}`;
statusDiv.textContent = message;
statusDiv.style.display = 'block';
// 自动隐藏
setTimeout(() => {
statusDiv.style.display = 'none';
}, 5000);
}
// 显示/隐藏加载状态
function showLoading(show) {
const loadingDiv = document.getElementById('loading');
if (show) {
loadingDiv.classList.add('show');
} else {
loadingDiv.classList.remove('show');
}
}
</script>
</body>
</html>