@ -1,12 +1,15 @@
<!DOCTYPE html>
< html lang = "zh-CN" >
< html lang = "zh-CN" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< 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.plot.ly/plotly-2.27.0.min.js" > < / script >
< script src = "https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js" > < / script >
{# < script src = "https://cdn.plot.ly/plotly-latest.min.js" > < / script > #}
< script src = "https://cdn.plot.ly/plotly-2.27.0.min.js" > < / script >
< script src = "https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js" > < / script >
< style >
* {
margin: 0;
@ -151,7 +154,14 @@
}
.btn-warning {
background: linear-gradient(135deg, #ed8936 0%, #dd6b20 100%);
background: linear-gradient(135deg, #ffc19d 0%, #ffc19d 100%);
}
.chart-grid {
display: grid;
grid-template-columns: 1fr auto;
gap: 10px;
align-items: center;
}
.status {
@ -161,6 +171,13 @@
font-weight: 500;
}
.indicator-status {
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
font-weight: 500;
}
.status.success {
background: #c6f6d5;
color: #22543d;
@ -401,6 +418,26 @@
100% { transform: rotate(360deg); }
}
.indicator-loading {
display: none;
text-align: center;
padding: 20px;
}
.indicator-loading.show {
display: block;
}
.indicator-loading .spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #6dea66;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
}
.model-info {
background: #e6fffa;
border: 1px solid #81e6d9;
@ -438,100 +475,126 @@
}
}
< / style >
< / head >
< body >
< div class = "container" >
< div class = "header" >
< 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" >
< div class = "main-content" >
< div class = "control-panel" >
< h2 > 🎯 Control Panel< / h2 >
<!-- Model Selection -->
< div class = "form-group" >
< label for = "model-select" > Select Model:< / label >
< select id = "model-select" >
< option value = "" > Please load available models first< / option >
< 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 >
< small class = "form-text" > Select the Kronos model to use< / small >
< / div >
<!-- Device Selection -->
< 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 >
< 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 >
< small class = "form-text" > Select the device to run the model on< / small >
< / div >
<!-- Model Status -->
< div id = "model-status" class = "status info" style = "display: none;" >
< div id = "model-status" class = "status info" style = "display: none;" >
Model status information
< / div >
<!-- Load Model Button -->
< button id = "load-model-btn" class = "btn btn-secondary" >
< button id = "load-model-btn" class = "btn btn-secondary" >
🔄 Load Model
< / button >
< hr style = "margin: 20px 0; border: 1px solid #e2e8f0;" >
<!-- Stock data acquisition -->
< div class = "form-group" >
< label class = "sr-only" for = "stock_code" > Ticker Symbol Input:< / label >
< input type = "text" class = "form-control" id = "stock_code" name = 'stock_code'
value = "{{ stock_code }}" placeholder = "例如:sh.600000" >
< small class = "form-text" > Enter the ticker symbol you want to analyze< / small >
< / div >
< button id = "stock-data-btn" class = "btn btn-secondary" >
📄 Stock Data
< / button >
< hr style = "margin: 20px 0; border: 1px solid #e2e8f0;" >
<!-- Data File Selection -->
< div class = "form-group" >
< label for = "data-file-select" > Select Data File:< / label >
< select id = "data-file-select" >
< option value = "" > Please load data file list first< / option >
< div class = "form-group" >
< label for = "data-file-select" > Select Data File:< / label >
< select id = "data-file-select" >
< option value = "" > Please load data file list first< / option >
< / select >
< small class = "form-text" > Select K-line data file from data directory< / small >
< small class = "form-text" > Select K-line data file from data directory< / small >
< / div >
< button id = "load-data-btn" class = "btn btn-secondary" >
< button id = "load-data-btn" class = "btn btn-secondary" >
📁 Load Data
< / button >
<!-- Data Information Display -->
< div id = "data-info" class = "data-info" style = "display: none;" >
< 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 >
< 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;" >
< hr style = "margin: 20px 0; border: 1px solid #e2e8f0;" >
<!-- Time Window Selector -->
< div class = "time-window-container" >
< div class = "time-window-container" >
< h3 > ⏰ Time Window Selection< / h3 >
< div class = "time-window-info" >
< span id = "window-start" > Start: --< / span >
< span id = "window-end" > End: --< / span >
< span id = "window-size" > Window Size: 400+120=520 data points< / span >
< div class = "time-window-info" >
< span id = "window-start" > Start: --< / span >
< span id = "window-end" > End: --< / span >
< span id = "window-size" > Window Size: 400 + 120 = 520 data points< / 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 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" > Earliest< / span >
< span id = "max-label" > Latest< / span >
< div class = "slider-labels" >
< span id = "min-label" > Earliest< / span >
< span id = "max-label" > Latest< / span >
< / div >
< / div >
< small class = "form-text" > Drag slider to select time window position for 520 data points, green area represents fixed 400+120 data point range< / small >
< small class = "form-text" > Drag slider to select time window position for 520 data points, green area represents fixed 400+120 data point range< / small >
< / div >
<!-- Prediction Parameters -->
< div class = "form-group" >
< label for = "lookback" > Lookback Window Size:< / label >
@ -545,31 +608,36 @@
< small class = "form-text" > Fixed at 120 data points< / small >
< / div >
<!-- Prediction Quality Parameters -->
< 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 >
< 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 >
< 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 >
< small class = "form-text" > Generate multiple prediction samples to improve quality (recommended
1-3)< / small >
< / div >
< button id = "predict-btn" class = "btn btn-success" disabled >
🔮 Start Prediction
< / button >
<!-- Loading Status -->
< div id = "loading" class = "loading" >
< div class = "spinner" > < / div >
@ -577,6 +645,7 @@
< / div >
< / div >
< div class = "chart-container" >
< h2 > 📈 Prediction Results Chart< / h2 >
< div id = "chart" > < / div >
@ -630,10 +699,72 @@
< / div >
< / div >
< / div >
< br >
< h2 > 📶 Technical Indicator Chart< / h2 >
< div id = "indicator-status" class = "indicator-status" style = "display: none;" > < / div >
< div class = "form-group" >
< label for = "diagram_type" > Select Diagram Type:< / label >
< div class = "chart-grid" >
< select id = "diagram_type" class = "form-control" >
< option value = "Volume Chart (VOL)" > Volume Chart (VOL)< / option >
< option value = "Moving Average (MA)" > Moving Average (MA)< / option >
< option value = "MACD Indicator (MACD)" > MACD Indicator (MACD)< / option >
< option value = "RSI Indicator (RSI)" > RSI Indicator (RSI)< / option >
< option value = "Bollinger Bands (BB)" > Bollinger Bands (BB)< / option >
< option value = "Stochastic Oscillator (STOCH)" > Stochastic Oscillator (STOCH)< / option >
< option value = "Rolling Window Mean Strategy" > Rolling Window Mean Strategy< / option >
< option value = "TRIX Indicator (TRIX)" > Triple Exponential Average (TRIX) Strategy< / option >
< / select >
< button id = "generate-chart-btn" class = "btn btn-warning" >
✏️ Generate chart
< / button >
< / div >
< small class = "form-text" > Select the type to draw the relevant chart< / small >
< / div >
< div id = "indicator-loading" class = "indicator-loading" >
< div class = "spinner" > < / div >
< p > Generating chart, please wait...< / p >
< / div >
< div id = "indicator-chart" > < / div >
<!-- Data Presentation -->
< div id = "data-presentation" class = "comparison-section" style = "display: none;" >
< h3 > 💹 Financial Data Visualization< / h3 >
< div class = "error-details" >
< h4 > Detailed Financial Data:< / h4 >
< div style = "max-height: 300px; overflow-y: auto;" >
< table class = "comparison-table" >
< thead >
< tr >
< th > Timestamps< / th >
< th > Open< / th >
< th > High< / th >
< th > Low< / th >
< th > Close< / th >
< th > Volume< / th >
< th > Amount< / th >
< / tr >
< / thead >
< tbody id = "data-tbody" >
< / tbody >
< / table >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< script >
// Global variables
let currentDataFile = null;
@ -641,11 +772,13 @@
let availableModels = [];
let modelLoaded = false;
// Initialize after page loads
document.addEventListener('DOMContentLoaded', function() {
initializeApp();
});
// Initialize application
async function initializeApp() {
console.log('🚀 Initializing Kronos Web UI...');
@ -665,6 +798,7 @@
console.log('✅ Application initialization completed');
}
// Load available models
async function loadAvailableModels() {
try {
@ -700,6 +834,7 @@
}
}
// Populate model selection dropdown
function populateModelSelect() {
const modelSelect = document.getElementById('model-select');
@ -713,6 +848,7 @@
});
}
// Load model
async function loadModel() {
const modelKey = document.getElementById('model-select').value;
@ -750,6 +886,7 @@
}
}
// Update model status
async function updateModelStatus() {
try {
@ -768,6 +905,59 @@
}
}
//Stock Data按钮
document.addEventListener('DOMContentLoaded', function() {
const generateChartBtn = document.getElementById('stock-data-btn');
if (generateChartBtn) {
generateChartBtn.addEventListener('click', StockData);
console.log('Stock Data button event listener bound');
} else {
console.error('stock-data-btn element not found');
}
});
async function StockData() {
console.log('Get stock data...');
const stockCodeInput = document.getElementById('stock_code');
const generateBtn = document.getElementById('stock-data-btn');
const stockCode = stockCodeInput.value.trim();
generateBtn.disabled = true;
try {
if (!stockCode) {
showStatus('error', 'Stock code cannot be empty');
return;
}
const stockCodeRegex = /^[a-z]+\.\d+$/;
if (!stockCodeRegex.test(stockCode)) {
showStatus('error', 'The ticker symbol is in the wrong format');
return;
}
showLoading(true);
const response = await axios.post('/api/stock-data', {stock_code: stockCode});
if (response.data.success) {
showStatus('success', `Successfully fetched data for ${stockCode}`);
loadDataFiles();
stockCodeInput.value = '';
} else {
showStatus('error', response.data.error || 'Failed to fetch stock data');
}
} catch (error) {
console.error('❌ Failed to fetch stock data:', error);
showStatus('error', `Failed to fetch data`);
} finally {
showLoading(false);
if (generateBtn) generateBtn.disabled = false;
}
}
// Load data file list
async function loadDataFiles() {
try {
@ -791,6 +981,7 @@
}
}
// Load data file
async function loadData() {
const filePath = document.getElementById('data-file-select').value;
@ -832,6 +1023,7 @@
}
}
// Display data information
function showDataInfo(dataInfo) {
document.getElementById('data-info').style.display = 'block';
@ -846,17 +1038,20 @@
initializeTimeWindowSlider(dataInfo);
}
// Time window slider related variables
let sliderData = null;
let isDragging = false;
let currentHandle = null;
// Initialize time window slider
function initializeTimeSlider() {
// Set up slider event listeners
setupSliderEventListeners();
}
// Set up slider event listeners
function setupSliderEventListeners() {
const startHandle = document.getElementById('start-handle');
@ -893,12 +1088,14 @@
updateSliderFromHandles();
});
// End dragging
document.addEventListener('mouseup', () => {
isDragging = false;
currentHandle = null;
});
// Click track to set position directly
track.addEventListener('click', (e) => {
const rect = track.getBoundingClientRect();
@ -921,6 +1118,7 @@
});
}
// Update start handle position
function updateStartHandle(percentage) {
const startHandle = document.getElementById('start-handle');
@ -945,6 +1143,7 @@
endHandle.style.left = ((percentage + windowPercentage) * 100) + '%';
}
// Update end handle position
function updateEndHandle(percentage) {
const endHandle = document.getElementById('end-handle');
@ -969,6 +1168,7 @@
startHandle.style.left = ((percentage - windowPercentage) * 100) + '%';
}
// Update slider display based on handle positions
function updateSliderFromHandles() {
const startHandle = document.getElementById('start-handle');
@ -999,6 +1199,7 @@
document.getElementById('pred-len').value = 120;
}
// Update slider based on input fields
function updateSliderFromInputs() {
if (!sliderData) return;
@ -1029,6 +1230,7 @@
updateSliderFromHandles();
}
// Initialize time window slider
function initializeTimeWindowSlider(dataInfo) {
sliderData = {
@ -1046,6 +1248,134 @@
updateSliderFromInputs();
}
// Binding Generate chart
document.addEventListener('DOMContentLoaded', function() {
const generateChartBtn = document.getElementById('generate-chart-btn');
if (generateChartBtn) {
generateChartBtn.addEventListener('click', generateTechnicalChart);
console.log('Generate chart button event listener bound');
} else {
console.error('generate-chart-btn element not found');
}
});
// Generate technical chart
async function generateTechnicalChart() {
console.log('Generating technical indicator chart...');
const indicatorLoading = document.getElementById('indicator-loading');
indicatorLoading.classList.add('show');
const generateBtn = document.getElementById('generate-chart-btn');
generateBtn.disabled = true;
try {
await new Promise(resolve => setTimeout(resolve, 2000));
// get arguments
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;
const totalRows = sliderData ? sliderData.totalRows : 0;
const historicalStartIdx = Math.floor(startPercentage * totalRows);
const lookback = Math.floor((endPercentage - startPercentage) * totalRows);
const predLen = parseInt(document.getElementById('pred-len').value);
const filePath = document.getElementById('data-file-select').value;
const diagramType = document.getElementById('diagram_type').value;
// validate arguments
if (!filePath) throw new Error('Please select a data file first');
if (isNaN(lookback) || isNaN(predLen)) throw new Error('Invalid parameters');
// fetch chart data
const response = await fetch('/api/generate-chart', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
file_path: filePath,
lookback: lookback,
pred_len: predLen,
diagram_type: diagramType,
historical_start_idx: historicalStartIdx
})
});
const result = await response.json();
if (!result.success) throw new Error(result.error || 'Failed to generate chart');
// render the chart
const chartContainer = document.getElementById('indicator-chart');
if (chartContainer) {
if (chartContainer.data) Plotly.purge(chartContainer);
Plotly.newPlot(
chartContainer,
result.chart.data,
result.chart.layout,
{ responsive: true }
);
} else {
throw new Error('Indicator chart container not found');
}
document.getElementById('data-presentation').style.display = 'block';
if (result.table_data) {
fillDataTable(result.table_data);
} else {
console.warn('No table data returned from server');
fillDataTable([]);
}
showIndicatorStatus('success', `chart (${diagramType}) generated successfully`);
} catch (error) {
console.error('Chart generation error:', error);
showIndicatorStatus('error', `Chart generation failed: ${error.message}`);
} finally {
indicatorLoading.classList.remove('show');
generateBtn.disabled = false;
}
}
// Fill data table
function fillDataTable(data) {
const tbody = document.getElementById('data-tbody');
tbody.innerHTML = '';
if (!data || data.length === 0) {
const emptyRow = document.createElement('tr');
emptyRow.innerHTML = '< td colspan = "7" style = "text-align:center" > 暂无数据< / td > ';
tbody.appendChild(emptyRow);
return;
}
data.forEach(item => {
const row = document.createElement('tr');
row.innerHTML = `
< td > ${new Date(item.timestamps).toLocaleString()}< / td >
< td > ${item.open.toFixed(4)}< / td >
< td > ${item.high.toFixed(4)}< / td >
< td > ${item.low.toFixed(4)}< / td >
< td > ${item.close.toFixed(4)}< / td >
< td > ${item.volume ? item.volume.toLocaleString() : '-'}< / td >
< td > ${item.amount ? item.amount.toFixed(2) : '-'}< / td >
`;
tbody.appendChild(row);
});
}
// Start prediction
async function startPrediction() {
if (!currentDataFile) {
@ -1125,19 +1455,20 @@
}
}
// Display prediction results
{#function displayPredictionResult(result) {#}
{# // Display chart#}
{# const chartData = JSON.parse(result.chart);#}
{# Plotly.newPlot('chart', chartData.data, chartData.layout);#}
{##}
{# // Display comparison analysis (if actual data exists)#}
{# if (result.has_comparison) {#}
{# displayComparisonAnalysis(result);#}
{# } else {#}
{# document.getElementById('comparison-section').style.display = 'none';#}
{# }#}
{#}#}
// // Display prediction results
// {#function displayPredictionResult(result) {#}
// {# // Display chart#}
// {# const chartData = JSON.parse(result.chart);#}
// {# Plotly.newPlot('chart', chartData.data, chartData.layout);#}
// {##}
// {# // Display comparison analysis (if actual data exists)#}
// {# if (result.has_comparison) {#}
// {# displayComparisonAnalysis(result);#}
// {# } else {#}
// {# document.getElementById('comparison-section').style.display = 'none';#}
// {# }#}
// {#}#}
function displayPredictionResult(result) {
console.log('🔍 DEBUG - displayPredictionResult called');
@ -1230,6 +1561,7 @@
fillComparisonTable(result.prediction_results, result.actual_data);
}
// Calculate prediction quality metrics
function getPredictionQuality(predictions, actuals) {
if (!predictions || !actuals || predictions.length === 0 || actuals.length === 0) {
@ -1259,6 +1591,7 @@
return { mae, rmse, mape };
}
// Fill comparison table
function fillComparisonTable(predictions, actuals) {
const tbody = document.getElementById('comparison-tbody');
@ -1286,6 +1619,7 @@
}
}
// Set up event listeners
function setupEventListeners() {
// Load model button
@ -1297,6 +1631,9 @@
// Prediction button
document.getElementById('predict-btn').addEventListener('click', startPrediction);
// Generate chart button
document.addEventListener('DOMContentLoaded', function(){
// Prediction quality parameter sliders
document.getElementById('temperature').addEventListener('input', function() {
document.getElementById('temperature-value').textContent = this.value;
@ -1309,8 +1646,18 @@
// Update slider when lookback window size changes
document.getElementById('lookback').addEventListener('input', updateSliderFromInputs);
document.getElementById('pred-len').addEventListener('input', updateSliderFromInputs);
const chartButton = document.getElementById('load-chart-btn');
if (chartButton) {
chartButton.addEventListener('click', generatechart);
console.log('Chart button event listener bound');
} else {
console.error('load-chart-btn element not found');
}
});
}
// Display status information
function showStatus(type, message) {
const statusDiv = document.getElementById('model-status');
@ -1324,6 +1671,20 @@
}, 5000);
}
function showIndicatorStatus(type, message) {
const statusDiv = document.getElementById('indicator-status');
statusDiv.className = `indicator-status status ${type}`;
statusDiv.textContent = message;
statusDiv.style.display = 'block';
// Auto-hide after 5 seconds
setTimeout(() => {
statusDiv.style.display = 'none';
}, 5000);
}
// Show/hide loading status
function showLoading(show) {
const loadingDiv = document.getElementById('loading');
@ -1333,6 +1694,7 @@
loadingDiv.classList.remove('show');
}
}
< / script >
< / body >
< / html >