|
|
<!DOCTYPE html>
|
|
|
<html lang="zh-CN">
|
|
|
<head>
|
|
|
<meta charset="UTF-8">
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
<title>Kronos Financial Prediction 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 Financial Prediction Web UI</h1>
|
|
|
<p>AI-based financial K-line data prediction analysis platform</p>
|
|
|
</div>
|
|
|
|
|
|
<div class="main-content">
|
|
|
<div class="control-panel">
|
|
|
<h2>🎯 Control Panel</h2>
|
|
|
|
|
|
|
|
|
<div class="form-group">
|
|
|
<label for="model-select">Select Model:</label>
|
|
|
<select id="model-select">
|
|
|
<option value="">Please load available models first</option>
|
|
|
</select>
|
|
|
<small class="form-text">Select the Kronos model to use</small>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<div class="form-group">
|
|
|
<label for="device-select">Select Device:</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">Select the device to run the model on</small>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<div id="model-status" class="status info" style="display: none;">
|
|
|
Model status information
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<button id="load-model-btn" class="btn btn-secondary">
|
|
|
🔄 Load Model
|
|
|
</button>
|
|
|
|
|
|
<hr style="margin: 20px 0; border: 1px solid #e2e8f0;">
|
|
|
|
|
|
|
|
|
<div class="form-group">
|
|
|
<label for="symbol-select">选择交易对:</label>
|
|
|
<select id="symbol-select">
|
|
|
<option value="">请先加载交易对列表</option>
|
|
|
</select>
|
|
|
<small class="form-text">选择要分析的加密货币交易对</small>
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
<label for="timeframe-select">选择时间周期:</label>
|
|
|
<select id="timeframe-select">
|
|
|
<option value="">请先加载时间周期列表</option>
|
|
|
</select>
|
|
|
<small class="form-text">选择K线数据的时间间隔</small>
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
<label for="data-limit">数据数量:</label>
|
|
|
<select id="data-limit">
|
|
|
<option value="400">400条</option>
|
|
|
<option value="1000" selected>1000条</option>
|
|
|
<option value="1500">1500条</option>
|
|
|
</select>
|
|
|
<small class="form-text">获取最近的K线数据条数</small>
|
|
|
</div>
|
|
|
|
|
|
<button id="load-data-btn" class="btn btn-secondary">
|
|
|
📈 获取币安数据
|
|
|
</button>
|
|
|
|
|
|
|
|
|
<div id="data-info" class="data-info" style="display: none;">
|
|
|
<h3>📊 Data Information</h3>
|
|
|
<p><strong>Rows:</strong> <span id="data-rows">-</span></p>
|
|
|
<p><strong>Columns:</strong> <span id="data-cols">-</span></p>
|
|
|
<p><strong>Time Range:</strong> <span id="data-time-range">-</span></p>
|
|
|
<p><strong>Price Range:</strong> <span id="data-price-range">-</span></p>
|
|
|
<p><strong>Time Frequency:</strong> <span id="data-timeframe">-</span></p>
|
|
|
<p><strong>Prediction Columns:</strong> <span id="data-prediction-cols">-</span></p>
|
|
|
</div>
|
|
|
|
|
|
<hr style="margin: 20px 0; border: 1px solid #e2e8f0;">
|
|
|
|
|
|
|
|
|
<div class="form-group">
|
|
|
<label for="lookback-input">回看窗口大小:</label>
|
|
|
<input type="number" id="lookback-input" value="400" min="50" max="1000" step="10">
|
|
|
<small class="form-text">用于预测的历史数据点数量(默认400)</small>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<div class="form-group">
|
|
|
<label for="lookback">回看窗口大小:</label>
|
|
|
<input type="number" id="lookback" value="400" readonly>
|
|
|
<small class="form-text">自动从上方输入框同步</small>
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
<label for="pred-len">Prediction Length:</label>
|
|
|
<input type="number" id="pred-len" value="120" readonly>
|
|
|
<small class="form-text">Fixed at 120 data points</small>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<div class="form-group">
|
|
|
<label for="temperature">Prediction 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">Controls prediction randomness, higher values make predictions more diverse, lower values make predictions more conservative</small>
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
<label for="top-p">Nucleus Sampling Parameter (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">Controls prediction diversity, higher values consider broader probability distributions</small>
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
<label for="sample-count">Sample Count:</label>
|
|
|
<input type="number" id="sample-count" value="1" min="1" max="5" step="1">
|
|
|
<small class="form-text">Generate multiple prediction samples to improve quality (recommended 1-3)</small>
|
|
|
</div>
|
|
|
|
|
|
<button id="predict-btn" class="btn btn-success">
|
|
|
🔮 Start Prediction
|
|
|
</button>
|
|
|
|
|
|
|
|
|
<div id="loading" class="loading">
|
|
|
<div class="spinner"></div>
|
|
|
<p>Processing, please wait...</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="chart-container">
|
|
|
<h2>📈 Prediction Results Chart</h2>
|
|
|
<div id="chart"></div>
|
|
|
|
|
|
|
|
|
<div id="comparison-section" class="comparison-section" style="display: none;">
|
|
|
<h3>📊 Prediction vs Actual Data Comparison</h3>
|
|
|
<div id="comparison-info" class="comparison-info">
|
|
|
<p><strong>Prediction Type:</strong> <span id="prediction-type">-</span></p>
|
|
|
<p><strong>Comparison Data:</strong> <span id="comparison-data">-</span></p>
|
|
|
</div>
|
|
|
|
|
|
<div class="error-stats">
|
|
|
<div class="error-stat">
|
|
|
<h4>Mean Absolute Error</h4>
|
|
|
<div class="value" id="mae">-</div>
|
|
|
<div class="unit">Price Units</div>
|
|
|
</div>
|
|
|
<div class="error-stat">
|
|
|
<h4>Root Mean Square Error</h4>
|
|
|
<div class="value" id="rmse">-</div>
|
|
|
<div class="unit">Price Units</div>
|
|
|
</div>
|
|
|
<div class="error-stat">
|
|
|
<h4>Mean Absolute Percentage Error</h4>
|
|
|
<div class="value" id="mape">-</div>
|
|
|
<div class="unit">%</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="error-details">
|
|
|
<h4>Detailed Comparison Data:</h4>
|
|
|
<div style="max-height: 300px; overflow-y: auto;">
|
|
|
<table class="comparison-table">
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th>Time</th>
|
|
|
<th>Actual Open</th>
|
|
|
<th>Predicted Open</th>
|
|
|
<th>Actual High</th>
|
|
|
<th>Predicted High</th>
|
|
|
<th>Actual Low</th>
|
|
|
<th>Predicted Low</th>
|
|
|
<th>Actual Close</th>
|
|
|
<th>Predicted Close</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody id="comparison-tbody">
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<script>
|
|
|
|
|
|
let currentSymbol = null;
|
|
|
let currentInterval = null;
|
|
|
let currentLimit = null;
|
|
|
let currentDataInfo = null;
|
|
|
let availableModels = [];
|
|
|
let modelLoaded = false;
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
initializeApp();
|
|
|
});
|
|
|
|
|
|
|
|
|
async function initializeApp() {
|
|
|
console.log('🚀 Initializing Kronos Web UI...');
|
|
|
|
|
|
|
|
|
await loadAvailableModels();
|
|
|
|
|
|
|
|
|
await loadSymbols();
|
|
|
await loadTimeframes();
|
|
|
|
|
|
|
|
|
setupEventListeners();
|
|
|
|
|
|
|
|
|
initializeLookbackSync();
|
|
|
|
|
|
|
|
|
updatePredictButtonStatus();
|
|
|
|
|
|
console.log('✅ Application initialization completed');
|
|
|
}
|
|
|
|
|
|
|
|
|
async function loadAvailableModels() {
|
|
|
try {
|
|
|
const response = await axios.get('/api/available-models');
|
|
|
if (response.data.model_available) {
|
|
|
availableModels = response.data.models;
|
|
|
populateModelSelect();
|
|
|
console.log('✅ Available models loaded successfully:', availableModels);
|
|
|
} else {
|
|
|
console.warn('⚠️ Kronos model library not available');
|
|
|
showStatus('warning', 'Kronos model library not available, will use simulated prediction');
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('❌ Failed to load available models:', error);
|
|
|
showStatus('error', 'Failed to load available models');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
function populateModelSelect() {
|
|
|
const modelSelect = document.getElementById('model-select');
|
|
|
modelSelect.innerHTML = '<option value="">Please select model</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', 'Please select a model to load');
|
|
|
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();
|
|
|
updatePredictButtonStatus();
|
|
|
console.log('✅ Model loaded successfully:', response.data.model_info);
|
|
|
} else {
|
|
|
showStatus('error', response.data.error);
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('❌ Model loading failed:', error);
|
|
|
showStatus('error', `Model loading failed: ${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', `Model loaded: ${status.current_model.name} on ${status.current_model.device}`);
|
|
|
} else if (status.available) {
|
|
|
showStatus('info', 'Model available but not loaded');
|
|
|
} else {
|
|
|
showStatus('warning', 'Model library not available');
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('❌ Failed to get model status:', error);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
async function loadSymbols() {
|
|
|
try {
|
|
|
const response = await axios.get('/api/symbols');
|
|
|
const symbols = response.data;
|
|
|
|
|
|
const symbolSelect = document.getElementById('symbol-select');
|
|
|
symbolSelect.innerHTML = '<option value="">请选择交易对</option>';
|
|
|
|
|
|
symbols.forEach(symbol => {
|
|
|
const option = document.createElement('option');
|
|
|
option.value = symbol.symbol;
|
|
|
option.textContent = symbol.name;
|
|
|
symbolSelect.appendChild(option);
|
|
|
});
|
|
|
|
|
|
console.log('✅ 交易对列表加载成功:', symbols);
|
|
|
} catch (error) {
|
|
|
console.error('❌ 加载交易对列表失败:', error);
|
|
|
showStatus('error', '加载交易对列表失败');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
async function loadTimeframes() {
|
|
|
try {
|
|
|
const response = await axios.get('/api/timeframes');
|
|
|
const timeframes = response.data;
|
|
|
|
|
|
const timeframeSelect = document.getElementById('timeframe-select');
|
|
|
timeframeSelect.innerHTML = '<option value="">请选择时间周期</option>';
|
|
|
|
|
|
timeframes.forEach(timeframe => {
|
|
|
const option = document.createElement('option');
|
|
|
option.value = timeframe.value;
|
|
|
option.textContent = `${timeframe.label} - ${timeframe.description}`;
|
|
|
timeframeSelect.appendChild(option);
|
|
|
});
|
|
|
|
|
|
|
|
|
timeframeSelect.value = '1h';
|
|
|
|
|
|
console.log('✅ 时间周期列表加载成功:', timeframes);
|
|
|
} catch (error) {
|
|
|
console.error('❌ 加载时间周期列表失败:', error);
|
|
|
showStatus('error', '加载时间周期列表失败');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
async function loadData() {
|
|
|
const symbol = document.getElementById('symbol-select').value;
|
|
|
const interval = document.getElementById('timeframe-select').value;
|
|
|
const limit = parseInt(document.getElementById('data-limit').value);
|
|
|
|
|
|
if (!symbol) {
|
|
|
showStatus('error', '请选择交易对');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (!interval) {
|
|
|
showStatus('error', '请选择时间周期');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
showLoading(true);
|
|
|
document.getElementById('load-data-btn').disabled = true;
|
|
|
|
|
|
const response = await axios.post('/api/load-data', {
|
|
|
symbol: symbol,
|
|
|
interval: interval,
|
|
|
limit: limit
|
|
|
});
|
|
|
|
|
|
if (response.data.success) {
|
|
|
currentSymbol = symbol;
|
|
|
currentInterval = interval;
|
|
|
currentLimit = limit;
|
|
|
currentDataInfo = response.data.data_info;
|
|
|
|
|
|
|
|
|
console.log('🔍 Data loaded, setting variables:');
|
|
|
console.log(' symbol:', symbol, '-> currentSymbol:', currentSymbol);
|
|
|
console.log(' interval:', interval, '-> currentInterval:', currentInterval);
|
|
|
console.log(' limit:', limit, '-> currentLimit:', currentLimit);
|
|
|
console.log(' data_info exists:', !!response.data.data_info);
|
|
|
|
|
|
showDataInfo(response.data.data_info);
|
|
|
showStatus('success', response.data.message);
|
|
|
|
|
|
|
|
|
updatePredictButtonStatus();
|
|
|
|
|
|
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 updatePredictButtonStatus() {
|
|
|
const predictBtn = document.getElementById('predict-btn');
|
|
|
|
|
|
|
|
|
console.log('🔍 Updating predict button status:');
|
|
|
console.log(' modelLoaded:', modelLoaded);
|
|
|
console.log(' currentSymbol:', currentSymbol);
|
|
|
console.log(' currentInterval:', currentInterval);
|
|
|
console.log(' currentLimit:', currentLimit);
|
|
|
|
|
|
|
|
|
if (modelLoaded && currentSymbol && currentInterval && currentLimit) {
|
|
|
predictBtn.textContent = '🔮 Start Prediction';
|
|
|
console.log('✅ Predict button enabled');
|
|
|
} else {
|
|
|
if (!modelLoaded) {
|
|
|
predictBtn.textContent = '🔮 请先加载模型';
|
|
|
console.log('❌ Model not loaded');
|
|
|
} else {
|
|
|
predictBtn.textContent = '🔮 请先选择交易对/周期/数量';
|
|
|
console.log('❌ Missing symbol/interval/limit');
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
const timeRange = `${dataInfo.start_date} 至 ${dataInfo.end_date} (北京时间)`;
|
|
|
document.getElementById('data-time-range').textContent = timeRange;
|
|
|
|
|
|
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);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function initializeTimeWindowSlider(dataInfo) {
|
|
|
console.log('🔧 Initializing time window slider with data:', dataInfo);
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
function initializeLookbackSync() {
|
|
|
const lookbackInput = document.getElementById('lookback-input');
|
|
|
const lookbackDisplay = document.getElementById('lookback');
|
|
|
|
|
|
if (lookbackInput && lookbackDisplay) {
|
|
|
|
|
|
lookbackInput.addEventListener('input', function() {
|
|
|
lookbackDisplay.value = this.value;
|
|
|
});
|
|
|
|
|
|
|
|
|
lookbackDisplay.value = lookbackInput.value;
|
|
|
|
|
|
console.log('✅ Lookback input sync initialized');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function updateSliderFromInputs() {
|
|
|
if (!sliderData) return;
|
|
|
|
|
|
|
|
|
const lookback = 400;
|
|
|
const predLen = 120;
|
|
|
const windowSize = lookback + predLen;
|
|
|
|
|
|
|
|
|
const totalRows = sliderData.totalRows;
|
|
|
|
|
|
if (windowSize > totalRows) {
|
|
|
|
|
|
showStatus('error', `Insufficient data, need at least ${windowSize} data points, currently only ${totalRows} available`);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
|
|
|
const startPercentage = 0.1;
|
|
|
const endPercentage = startPercentage + (windowSize / totalRows);
|
|
|
|
|
|
|
|
|
updateStartHandle(startPercentage);
|
|
|
updateEndHandle(endPercentage);
|
|
|
|
|
|
|
|
|
updateSliderFromHandles();
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function startPrediction() {
|
|
|
console.log('🔮 Start Prediction button clicked!');
|
|
|
console.log(' modelLoaded:', modelLoaded);
|
|
|
|
|
|
|
|
|
const symbolSel = document.getElementById('symbol-select').value;
|
|
|
const intervalSel = document.getElementById('timeframe-select').value;
|
|
|
const limitSel = parseInt(document.getElementById('data-limit').value);
|
|
|
|
|
|
|
|
|
currentSymbol = symbolSel || currentSymbol;
|
|
|
currentInterval = intervalSel || currentInterval;
|
|
|
currentLimit = limitSel || currentLimit;
|
|
|
|
|
|
console.log(' selectedSymbol:', symbolSel, 'selectedInterval:', intervalSel, 'selectedLimit:', limitSel);
|
|
|
if (!symbolSel || !intervalSel || !limitSel) {
|
|
|
showStatus('error', '请先选择交易对、时间周期和数据数量');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (!modelLoaded) {
|
|
|
console.log('❌ Model not loaded');
|
|
|
showStatus('error', '请先加载模型');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
showLoading(true);
|
|
|
document.getElementById('predict-btn').disabled = true;
|
|
|
|
|
|
const lookback = parseInt(document.getElementById('lookback-input').value) || 400;
|
|
|
const predLen = parseInt(document.getElementById('pred-len').value);
|
|
|
|
|
|
console.log('🔮 Starting prediction with parameters:');
|
|
|
console.log(' lookback:', lookback);
|
|
|
console.log(' predLen:', predLen);
|
|
|
console.log(' symbol:', symbolSel);
|
|
|
console.log(' interval:', intervalSel);
|
|
|
|
|
|
|
|
|
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 = {
|
|
|
symbol: symbolSel,
|
|
|
interval: intervalSel,
|
|
|
limit: limitSel,
|
|
|
lookback: lookback,
|
|
|
pred_len: predLen,
|
|
|
temperature: temperature,
|
|
|
top_p: topP,
|
|
|
sample_count: sampleCount
|
|
|
};
|
|
|
|
|
|
console.log('🚀 Starting prediction, parameters:', 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('❌ Prediction failed:', error);
|
|
|
showStatus('error', `Prediction failed: ${error.response?.data?.error || error.message}`);
|
|
|
} finally {
|
|
|
showLoading(false);
|
|
|
document.getElementById('predict-btn').disabled = false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
function displayPredictionResult(result) {
|
|
|
|
|
|
const chartData = JSON.parse(result.chart);
|
|
|
|
|
|
|
|
|
const decodedChart = decodePlotlyBdata(chartData);
|
|
|
Plotly.newPlot('chart', decodedChart.data, decodedChart.layout);
|
|
|
|
|
|
|
|
|
if (result.has_comparison) {
|
|
|
displayComparisonAnalysis(result);
|
|
|
} else {
|
|
|
document.getElementById('comparison-section').style.display = 'none';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
function decodePlotlyBdata(node) {
|
|
|
if (Array.isArray(node)) {
|
|
|
return node.map(decodePlotlyBdata);
|
|
|
}
|
|
|
if (node && typeof node === 'object') {
|
|
|
if (Object.prototype.hasOwnProperty.call(node, 'bdata') && Object.prototype.hasOwnProperty.call(node, 'dtype')) {
|
|
|
try {
|
|
|
const bytes = atob(node.bdata);
|
|
|
const buffer = new ArrayBuffer(bytes.length);
|
|
|
const view = new Uint8Array(buffer);
|
|
|
for (let i = 0; i < bytes.length; i++) view[i] = bytes.charCodeAt(i);
|
|
|
|
|
|
let typed;
|
|
|
switch (node.dtype) {
|
|
|
case 'f8':
|
|
|
typed = new Float64Array(buffer);
|
|
|
break;
|
|
|
case 'f4':
|
|
|
typed = new Float32Array(buffer);
|
|
|
break;
|
|
|
case 'i4':
|
|
|
typed = new Int32Array(buffer);
|
|
|
break;
|
|
|
case 'u4':
|
|
|
typed = new Uint32Array(buffer);
|
|
|
break;
|
|
|
case 'i2':
|
|
|
typed = new Int16Array(buffer);
|
|
|
break;
|
|
|
case 'u2':
|
|
|
typed = new Uint16Array(buffer);
|
|
|
break;
|
|
|
case 'i1':
|
|
|
typed = new Int8Array(buffer);
|
|
|
break;
|
|
|
case 'u1':
|
|
|
typed = new Uint8Array(buffer);
|
|
|
break;
|
|
|
default:
|
|
|
typed = new Float32Array(buffer);
|
|
|
}
|
|
|
return Array.from(typed);
|
|
|
} catch (e) {
|
|
|
console.warn('Failed to decode bdata, fallback to original node', e);
|
|
|
return node;
|
|
|
}
|
|
|
}
|
|
|
const out = {};
|
|
|
for (const k in node) {
|
|
|
if (Object.prototype.hasOwnProperty.call(node, k)) {
|
|
|
out[k] = decodePlotlyBdata(node[k]);
|
|
|
}
|
|
|
}
|
|
|
return out;
|
|
|
}
|
|
|
return node;
|
|
|
}
|
|
|
|
|
|
|
|
|
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} actual data points`;
|
|
|
|
|
|
|
|
|
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('symbol-select').addEventListener('change', function() {
|
|
|
currentSymbol = this.value || null;
|
|
|
updatePredictButtonStatus();
|
|
|
});
|
|
|
document.getElementById('timeframe-select').addEventListener('change', function() {
|
|
|
currentInterval = this.value || null;
|
|
|
updatePredictButtonStatus();
|
|
|
});
|
|
|
document.getElementById('data-limit').addEventListener('change', function() {
|
|
|
const v = parseInt(this.value);
|
|
|
currentLimit = isNaN(v) ? null : v;
|
|
|
updatePredictButtonStatus();
|
|
|
});
|
|
|
|
|
|
|
|
|
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>
|
|
|
|