You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2085 lines
86 KiB
2085 lines
86 KiB
<!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.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;
|
|
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;
|
|
}
|
|
|
|
/* Prediction quality parameter styles */
|
|
.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, #ffc19d 0%, #ffc19d 100%);
|
|
}
|
|
|
|
/*表格弹窗样式*/
|
|
.search {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.search-container{
|
|
white-space: nowrap;
|
|
padding: 6px 12px;
|
|
font-size: 14px;
|
|
background-color: #8a94e0;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.search-modal {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
z-index: 1000;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
|
|
.search-modal-content {
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
width: 90%;
|
|
max-width: 1000px;
|
|
max-height: 80vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
text-align: center;
|
|
}
|
|
|
|
.search-modal-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.search-modal-title {
|
|
margin: 0;
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
.close-modal-btn {
|
|
background: #764ba2;
|
|
color: white;
|
|
border: none;
|
|
padding: 5px 10px;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.search-results-container {
|
|
overflow-y: auto;
|
|
flex-grow: 1;
|
|
}
|
|
|
|
.rep-code {
|
|
cursor: pointer;
|
|
padding: 2px 8px;
|
|
background: #f5f7fa;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
color: #1989fa;
|
|
transition: background 0.2s;
|
|
display: inline-block;
|
|
text-align: center;
|
|
}
|
|
|
|
.rep-code:hover {
|
|
background: #e8f3ff;
|
|
}
|
|
|
|
.representative-codes {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
justify-content: center;
|
|
}
|
|
|
|
.chart-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr auto;
|
|
gap: 10px;
|
|
align-items: center;
|
|
}
|
|
|
|
.status {
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
margin-bottom: 20px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.indicator-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 selector styles */
|
|
.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 analysis styles */
|
|
.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); }
|
|
}
|
|
|
|
.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;
|
|
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>
|
|
|
|
|
|
<!-- 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>
|
|
</select>
|
|
<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>
|
|
</select>
|
|
<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;">
|
|
Model status information
|
|
</div>
|
|
|
|
|
|
<!-- Load Model Button -->
|
|
<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">
|
|
<div class="search">
|
|
<label class="sr-only" for="stock_code">Ticker Symbol Input:</label>
|
|
|
|
<button id="search-btn" class="search-container">
|
|
🔍 Search
|
|
</button>
|
|
</div>
|
|
|
|
<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>
|
|
|
|
<!--股票数据提示弹窗-->
|
|
<div id="search-modal" class="search-modal">
|
|
<div class="search-modal-content">
|
|
<div class="search-modal-header">
|
|
<h3 class="search-modal-title">📃 Stock Code Prompt:</h3>
|
|
<button id="close-modal" class="close-modal-btn">×</button>
|
|
</div>
|
|
<div class="search-results-container">
|
|
<table class="comparison-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Exchange</th>
|
|
<th>Prefix</th>
|
|
<th>Code_Range</th>
|
|
<th>Representative_Code</th>
|
|
</tr>
|
|
</thead>
|
|
|
|
<tbody id="search-results-body">
|
|
|
|
<!-- 上海证券交易所 (SSE)-->
|
|
<tr>
|
|
<td rowspan="5">上海证券交易所 (SSE)</td>
|
|
<td rowspan="5">sh.</td>
|
|
<td>600</td>
|
|
<td class="representative-codes">
|
|
<span class="rep-code">sh.600000(浦发银行)</span>
|
|
<span class="rep-code">sh.600036(招商银行)</span>
|
|
<span class="rep-code">sh.600519(贵州茅台)</span>
|
|
<span class="rep-code">sh.600887(伊利股份)</span>
|
|
<span class="rep-code">sh.600900(长江电力)</span>
|
|
<span class="rep-code">sh.600030(中信证券)</span>
|
|
<span class="rep-code">sh.600050(中国联通)</span>
|
|
<span class="rep-code">sh.600690(海尔智家)</span>
|
|
<span class="rep-code">sh.600570(恒生电子)</span>
|
|
<span class="rep-code">sh.600588(用友网络)</span>
|
|
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td>601</td>
|
|
<td class="representative-codes">
|
|
<span class="rep-code">sh.601318(中国平安)</span>
|
|
<span class="rep-code">sh.601398(工商银行)</span>
|
|
<span class="rep-code">sh.601888(中国中免)</span>
|
|
<span class="rep-code">sh.601857(中国石油)</span>
|
|
<span class="rep-code">sh.601012(隆基绿能)</span>
|
|
<span class="rep-code">sh.601988(中国国航)</span>
|
|
<span class="rep-code">sh.601088(中国神华)</span>
|
|
<span class="rep-code">sh.601766(中国中车)</span>
|
|
<span class="rep-code">sh.601390(中国中铁)</span>
|
|
<span class="rep-code">sh.601186(中国铁建)</span>
|
|
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td>603</td>
|
|
<td class="representative-codes">
|
|
<span class="rep-code">sh.603288(海天味业)</span>
|
|
<span class="rep-code">sh.603259(药明康德)</span>
|
|
<span class="rep-code">sh.603986(兆易创新)</span>
|
|
<span class="rep-code">sh.603501(韦尔股份)</span>
|
|
<span class="rep-code">sh.603993(中科曙光)</span>
|
|
<span class="rep-code">sh.603899(晨光股份)</span>
|
|
<span class="rep-code">sh.603605(珀莱雅)</span>
|
|
<span class="rep-code">sh.603707(健友股份)</span>
|
|
<span class="rep-code">sh.603833(欧派家居)</span>
|
|
<span class="rep-code">sh.603806(福斯特)</span>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td>605</td>
|
|
<td class="representative-codes">
|
|
<span class="rep-code">sh.605499(东鹏饮料)</span>
|
|
<span class="rep-code">sh.605338(巴比食品)</span>
|
|
<span class="rep-code">sh.605111(新洁能)</span>
|
|
<span class="rep-code">sh.605589(圣泉集团)</span>
|
|
<span class="rep-code">sh.605168(三人行)</span>
|
|
<span class="rep-code">sh.605066(天正电气)</span>
|
|
<span class="rep-code">sh.605155(西大门)</span>
|
|
<span class="rep-code">sh.605136(丽人丽妆)</span>
|
|
<span class="rep-code">sh.605333(沪光股份)</span>
|
|
<span class="rep-code">sh.605358(立昂微)</span>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td>688</td>
|
|
<td class="representative-codes">
|
|
<span class="rep-code">sh.688012(中微公司)</span>
|
|
<span class="rep-code">sh.688981(中芯国际)</span>
|
|
<span class="rep-code">sh.688111(金山办公)</span>
|
|
<span class="rep-code">sh.688036(传音控股)</span>
|
|
<span class="rep-code">sh.688008(澜起科技)</span>
|
|
<span class="rep-code">sh.688235(百济神州)</span>
|
|
<span class="rep-code">sh.688185(康希诺)</span>
|
|
<span class="rep-code">sh.688036(传音控股)</span>
|
|
<span class="rep-code">sh.688187(时代电气)</span>
|
|
<span class="rep-code">sh.688390(固德威)</span>
|
|
</td>
|
|
</tr>
|
|
|
|
<!-- 深圳证券交易所 (SZSE)-->
|
|
<tr>
|
|
<td rowspan="5">深圳证券交易所</td>
|
|
<td rowspan="5">sz.</td>
|
|
<td>000</td>
|
|
<td class="representative-codes">
|
|
<span class="rep-code">sz.000001(平安银行)</span>
|
|
<span class="rep-code">sz.000002(万科A)</span>
|
|
<span class="rep-code">sz.000858(五粮液)</span>
|
|
<span class="rep-code">sz.000333(美的集团)</span>
|
|
<span class="rep-code">sz.000651(格力电器)</span>
|
|
<span class="rep-code">sz.000538(云南白药)</span>
|
|
<span class="rep-code">sz.000063(中兴通讯)</span>
|
|
<span class="rep-code">sz.000100(TCL科技)</span>
|
|
<span class="rep-code">sz.000157(中联重科)</span>
|
|
<span class="rep-code">sz.000895(双汇发展)</span>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td>001</td>
|
|
<td class="representative-codes">
|
|
<span class="rep-code">sz.001203(大中矿业)</span>
|
|
<span class="rep-code">sz.001208(华菱线缆)</span>
|
|
<span class="rep-code">sz.001212(中旗新材)</span>
|
|
<span class="rep-code">sz.001213(中铁特货)</span>
|
|
<span class="rep-code">sz.001215(千味央厨)</span>
|
|
<span class="rep-code">sz.001217(华尔泰)</span>
|
|
<span class="rep-code">sz.001218(丽臣实业)</span>
|
|
<span class="rep-code">sz.001222(源飞宠物)</span>
|
|
<span class="rep-code">sz.001225(和泰机电)</span>
|
|
<span class="rep-code">sz.001309(德明利)</span>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td>002</td>
|
|
<td class="representative-codes">
|
|
<span class="rep-code">sz.002024(苏宁易购)</span>
|
|
<span class="rep-code">sz.002027(分众传媒)</span>
|
|
<span class="rep-code">sz.002555(三七互娱)</span>
|
|
<span class="rep-code">sz.002624(完美世界)</span>
|
|
<span class="rep-code">sz.002049(紫光国微)</span>
|
|
<span class="rep-code">sz.002179(中航光电)</span>
|
|
<span class="rep-code">sz.002415(海康威视)</span>
|
|
<span class="rep-code">sz.002475(立讯精密)</span>
|
|
<span class="rep-code">sz.002594(比亚迪)</span>
|
|
<span class="rep-code">sz.002142(宁波银行)</span>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td>003</td>
|
|
<td class="representative-codes">
|
|
<span class="rep-code">sz.003816(中国广核)</span>
|
|
<span class="rep-code">sz.003022(联泓新科)</span>
|
|
<span class="rep-code">sz.003031(中瓷电子)</span>
|
|
<span class="rep-code">sz.003032(传智教育)</span>
|
|
<span class="rep-code">sz.003035(南网能源)</span>
|
|
<span class="rep-code">sz.003036(泰坦股份)</span>
|
|
<span class="rep-code">sz.003000(劲仔食品)</span>
|
|
<span class="rep-code">sz.003001(中晶科技)</span>
|
|
<span class="rep-code">sz.003002(传智教育)</span>
|
|
<span class="rep-code">sz.003011(海象新材)</span>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td>300</td>
|
|
<td class="representative-codes">
|
|
<span class="rep-code">sz.300750(宁德时代)</span>
|
|
<span class="rep-code">sz.300059(东方财富)</span>
|
|
<span class="rep-code">sz.300124(汇川技术)</span>
|
|
<span class="rep-code">sz.300760(迈瑞医疗)</span>
|
|
<span class="rep-code">sz.300014(亿纬锂能)</span>
|
|
<span class="rep-code">sz.300122(智飞生物)</span>
|
|
<span class="rep-code">sz.300015(爱尔眼科)</span>
|
|
<span class="rep-code">sz.300274(阳光电源)</span>
|
|
<span class="rep-code">sz.300496(中科创达)</span>
|
|
<span class="rep-code">sz.300782(卓胜微)</span>
|
|
</td>
|
|
</tr>
|
|
|
|
<!--? 北京证券交易所 (BSE) -->
|
|
<!--? <tr>-->
|
|
<!--? <td rowspan="5">北京证券交易所 (BSE)</td>-->
|
|
<!--? <td rowspan="5">bj.</td>-->
|
|
<!--? <td>43</td>-->
|
|
<!--? <td class="representative-codes">-->
|
|
<!--? <span class="rep-code">bj.430047(诺思兰德)</span>-->
|
|
<!--? <span class="rep-code">bj.430090(同辉信息)</span>-->
|
|
<!--? <span class="rep-code">bj.430198(微创光电)</span>-->
|
|
<!--? <span class="rep-code">bj.430300(辰光医疗)</span>-->
|
|
<!--? <span class="rep-code">bj.430425(乐创技术)</span>-->
|
|
<!--? <span class="rep-code">bj.430476(海能技术)</span>-->
|
|
<!--? <span class="rep-code">bj.430510(丰光精密)</span>-->
|
|
<!--? <span class="rep-code">bj.430556(雅葆轩)</span>-->
|
|
<!--? <span class="rep-code">bj.430564(天润科技)</span>-->
|
|
<!--? <span class="rep-code">bj.430685(新芝生物)</span>-->
|
|
<!--? </td>-->
|
|
<!--? </tr>-->
|
|
<!--? <tr>-->
|
|
<!--? <td>83</td>-->
|
|
<!--? <td class="representative-codes">-->
|
|
<!--? <span class="rep-code">bj.830799(艾融软件)</span>-->
|
|
<!--? <span class="rep-code">bj.830809(安达科技)</span>-->
|
|
<!--? <span class="rep-code">bj.830839(万通液压)</span>-->
|
|
<!--? <span class="rep-code">bj.830866(齐鲁华信)</span>-->
|
|
<!--? <span class="rep-code">bj.830879(基康仪器)</span>-->
|
|
<!--? <span class="rep-code">bj.830896(旺成科技)</span>-->
|
|
<!--? <span class="rep-code">bj.830946(森萱医药)</span>-->
|
|
<!--? <span class="rep-code">bj.830964(润农节水)</span>-->
|
|
<!--? <span class="rep-code">bj.831010(凯添燃气)</span>-->
|
|
<!--? <span class="rep-code">bj.831039(国义招标)</span>-->
|
|
<!--? </td>-->
|
|
<!--? </tr>-->
|
|
<!--? <tr>-->
|
|
<!--? <td>87</td>-->
|
|
<!--? <td class="representative-codes">-->
|
|
<!--? <span class="rep-code">bj.870204(沪江材料)</span>-->
|
|
<!--? <span class="rep-code">bj.870357(雅葆轩)</span>-->
|
|
<!--? <span class="rep-code">bj.870436(大地电气)</span>-->
|
|
<!--? <span class="rep-code">bj.870640(曙光数创)</span>-->
|
|
<!--? <span class="rep-code">bj.870726(鸿智科技)</span>-->
|
|
<!--? <span class="rep-code">bj.870866(视声智能)</span>-->
|
|
<!--? <span class="rep-code">bj.870976(视声智能)</span>-->
|
|
<!--? <span class="rep-code">bj.871263(一致魔芋)</span>-->
|
|
<!--? <span class="rep-code">bj.871396(佳合科技)</span>-->
|
|
<!--? <span class="rep-code">bj.871478(海泰新能)</span>-->
|
|
<!--? </td>-->
|
|
<!--? </tr>-->
|
|
<!--? <tr>-->
|
|
<!--? <td>89</td>-->
|
|
<!--? <td class="representative-codes">-->
|
|
<!--? <span class="rep-code">bj.892089(科强股份)</span>-->
|
|
<!--? <span class="rep-code">bj.892282(美心翼申)</span>-->
|
|
<!--? <span class="rep-code">bj.892358(派诺科技)</span>-->
|
|
<!--? <span class="rep-code">bj.892519(捷众科技)</span>-->
|
|
<!--? <span class="rep-code">bj.892541(康农种业)</span>-->
|
|
<!--? <span class="rep-code">bj.892622(许昌智能)</span>-->
|
|
<!--? <span class="rep-code">bj.892679(云星宇)</span>-->
|
|
<!--? <span class="rep-code">bj.892748(欣捷高新)</span>-->
|
|
<!--? <span class="rep-code">bj.892810(捷安高科)</span>-->
|
|
<!--? <span class="rep-code">bj.892925(海昇药业)</span>-->
|
|
<!--? </td>-->
|
|
<!--? </tr>-->
|
|
<!--? <tr>-->
|
|
<!--? <td>920</td>-->
|
|
<!--? <td class="representative-codes">-->
|
|
<!--? <span class="rep-code">bj.920099(万达轴承)</span>-->
|
|
<!--? <span class="rep-code">bj.920175(欧福蛋业)</span>-->
|
|
<!--? <span class="rep-code">bj.920177(瑞华技术)</span>-->
|
|
<!--? <span class="rep-code">bj.920179(和特能源)</span>-->
|
|
<!--? <span class="rep-code">bj.920180(豪钢重工)</span>-->
|
|
<!--? <span class="rep-code">bj.920179(和特能源)</span>-->
|
|
<!--? </td>-->
|
|
<!--? </tr>-->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<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>
|
|
</select>
|
|
<small class = "form-text">Select K-line data file from data directory</small>
|
|
</div>
|
|
|
|
<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;">
|
|
<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;">
|
|
|
|
|
|
<!-- Time Window Selector -->
|
|
<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>
|
|
|
|
<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>
|
|
</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>
|
|
</div>
|
|
|
|
|
|
<!-- Prediction Parameters -->
|
|
<div class="form-group">
|
|
<label for="lookback">Lookback Window Size:</label>
|
|
<input type="number" id="lookback" value="400" readonly>
|
|
<small class="form-text">Fixed at 400 data points</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>
|
|
|
|
|
|
<!-- 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>
|
|
</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" disabled>
|
|
🔮 Start Prediction
|
|
</button>
|
|
|
|
|
|
<!-- Loading Status -->
|
|
<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>
|
|
|
|
<!-- Comparison Analysis -->
|
|
<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>
|
|
|
|
<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)">TRIX Indicator (TRIX)</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>
|
|
|
|
<!-- 数据表格 -->
|
|
<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;
|
|
let currentDataInfo = null;
|
|
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...');
|
|
|
|
// Load available models
|
|
await loadAvailableModels();
|
|
|
|
// Load data file list
|
|
await loadDataFiles();
|
|
|
|
// Set up event listeners
|
|
setupEventListeners();
|
|
|
|
// Initialize time slider
|
|
initializeTimeSlider();
|
|
|
|
console.log('✅ Application initialization completed');
|
|
}
|
|
|
|
|
|
// Load available models
|
|
async function loadAvailableModels() {
|
|
try {
|
|
console.log('开始加载模型列表...');
|
|
const response = await fetch('/api/available-models');
|
|
const data = await response.json();
|
|
console.log('API返回数据:', data);
|
|
|
|
const modelSelect = document.getElementById('model-select');
|
|
console.log('找到的下拉菜单:', modelSelect);
|
|
|
|
if (!modelSelect) {
|
|
console.error('错误: 找不到 modelSelect 元素');
|
|
return;
|
|
}
|
|
|
|
// 清空现有选项
|
|
modelSelect.innerHTML = '';
|
|
|
|
// 添加模型选项
|
|
for (const [key, model] of Object.entries(data.models)) {
|
|
const option = document.createElement('option');
|
|
option.value = key;
|
|
option.textContent = `${model.name} (${model.params}) - ${model.description}`;
|
|
modelSelect.appendChild(option);
|
|
console.log('添加选项:', option.textContent);
|
|
}
|
|
|
|
console.log('最终选项数量:', modelSelect.options.length);
|
|
|
|
} catch (error) {
|
|
console.error('加载模型列表失败:', error);
|
|
}
|
|
}
|
|
|
|
|
|
// Populate model selection dropdown
|
|
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);
|
|
});
|
|
}
|
|
|
|
|
|
// Load model
|
|
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();
|
|
document.getElementById('predict-btn').disabled = false;
|
|
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;
|
|
}
|
|
}
|
|
|
|
|
|
// Update model status
|
|
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);
|
|
}
|
|
}
|
|
|
|
|
|
//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;
|
|
}
|
|
}
|
|
|
|
|
|
// Search按钮表格弹窗
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const searchBtn = document.getElementById('search-btn');
|
|
const searchModal = document.getElementById('search-modal');
|
|
const closeModal = document.getElementById('close-modal');
|
|
const stockCodeInput = document.getElementById('stock_code');
|
|
|
|
searchBtn.addEventListener('click', () => {
|
|
searchModal.style.display = 'flex';
|
|
});
|
|
|
|
closeModal.addEventListener('click', () => {
|
|
searchModal.style.display = 'none';
|
|
});
|
|
|
|
document.addEventListener('click', (e) => {
|
|
if (e.target.classList.contains('rep-code')) {
|
|
const code = e.target.textContent.match(/[a-z]+\.\d+/)[0];
|
|
stockCodeInput.value = code;
|
|
closeModalFunc();
|
|
}
|
|
});
|
|
});
|
|
|
|
|
|
// Load data file list
|
|
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="">Please select data file</option>';
|
|
|
|
dataFiles.forEach(file => {
|
|
const option = document.createElement('option');
|
|
option.value = file.path;
|
|
option.textContent = `${file.name} (${file.size})`;
|
|
dataFileSelect.appendChild(option);
|
|
});
|
|
|
|
console.log('✅ Data file list loaded successfully:', dataFiles);
|
|
} catch (error) {
|
|
console.error('❌ Failed to load data file list:', error);
|
|
showStatus('error', 'Failed to load data file list');
|
|
}
|
|
}
|
|
|
|
|
|
// Load data file
|
|
async function loadData() {
|
|
const filePath = document.getElementById('data-file-select').value;
|
|
|
|
if (!filePath) {
|
|
showStatus('error', 'Please select a data file to load');
|
|
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);
|
|
|
|
// Update prediction button status
|
|
if (modelLoaded) {
|
|
document.getElementById('predict-btn').disabled = false;
|
|
}
|
|
|
|
console.log('✅ Data loaded successfully:', response.data.data_info);
|
|
} else {
|
|
showStatus('error', response.data.error);
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ Data loading failed:', error);
|
|
showStatus('error', `Data loading failed: ${error.response?.data?.error || error.message}`);
|
|
} finally {
|
|
showLoading(false);
|
|
document.getElementById('load-data-btn').disabled = false;
|
|
}
|
|
}
|
|
|
|
|
|
// Display data information
|
|
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} to ${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(', ');
|
|
|
|
// Initialize time window slider
|
|
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');
|
|
const endHandle = document.getElementById('end-handle');
|
|
const track = document.querySelector('.slider-track');
|
|
|
|
// Start dragging
|
|
startHandle.addEventListener('mousedown', (e) => {
|
|
isDragging = true;
|
|
currentHandle = 'start';
|
|
e.preventDefault();
|
|
});
|
|
|
|
endHandle.addEventListener('mousedown', (e) => {
|
|
isDragging = true;
|
|
currentHandle = 'end';
|
|
e.preventDefault();
|
|
});
|
|
|
|
// Dragging
|
|
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();
|
|
});
|
|
|
|
|
|
// End dragging
|
|
document.addEventListener('mouseup', () => {
|
|
isDragging = false;
|
|
currentHandle = null;
|
|
});
|
|
|
|
|
|
// Click track to set position directly
|
|
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));
|
|
|
|
// Determine which handle is closer to the click position
|
|
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();
|
|
});
|
|
}
|
|
|
|
|
|
// Update start handle position
|
|
function updateStartHandle(percentage) {
|
|
const startHandle = document.getElementById('start-handle');
|
|
const selection = document.getElementById('slider-selection');
|
|
|
|
// Fixed window size of 520 data points
|
|
const windowSize = 520;
|
|
const totalRows = sliderData ? sliderData.totalRows : 1000;
|
|
const windowPercentage = windowSize / totalRows;
|
|
|
|
// Ensure start handle doesn't cause window to exceed data range
|
|
if (percentage + windowPercentage > 1) {
|
|
percentage = 1 - windowPercentage;
|
|
}
|
|
|
|
startHandle.style.left = (percentage * 100) + '%';
|
|
selection.style.left = (percentage * 100) + '%';
|
|
selection.style.width = (windowPercentage * 100) + '%';
|
|
|
|
// Automatically adjust end handle position to maintain fixed window size
|
|
const endHandle = document.getElementById('end-handle');
|
|
endHandle.style.left = ((percentage + windowPercentage) * 100) + '%';
|
|
}
|
|
|
|
|
|
// Update end handle position
|
|
function updateEndHandle(percentage) {
|
|
const endHandle = document.getElementById('end-handle');
|
|
const selection = document.getElementById('slider-selection');
|
|
|
|
// Fixed window size of 520 data points
|
|
const windowSize = 520;
|
|
const totalRows = sliderData ? sliderData.totalRows : 1000;
|
|
const windowPercentage = windowSize / totalRows;
|
|
|
|
// Ensure end handle doesn't cause window to exceed data range
|
|
if (percentage - windowPercentage < 0) {
|
|
percentage = windowPercentage;
|
|
}
|
|
|
|
endHandle.style.left = (percentage * 100) + '%';
|
|
selection.style.left = ((percentage - windowPercentage) * 100) + '%';
|
|
selection.style.width = (windowPercentage * 100) + '%';
|
|
|
|
// Automatically adjust start handle position to maintain fixed window size
|
|
const startHandle = document.getElementById('start-handle');
|
|
startHandle.style.left = ((percentage - windowPercentage) * 100) + '%';
|
|
}
|
|
|
|
|
|
// Update slider display based on handle positions
|
|
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;
|
|
|
|
// Calculate selected time range
|
|
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);
|
|
|
|
// Update display information
|
|
document.getElementById('window-start').textContent = `Start: ${startDate.toLocaleDateString()}`;
|
|
document.getElementById('window-end').textContent = `End: ${endDate.toLocaleDateString()}`;
|
|
|
|
// Display fixed window size
|
|
document.getElementById('window-size').textContent = `Window Size: 400 + 120 = 520 data points (fixed)`;
|
|
|
|
// Input field values remain fixed
|
|
document.getElementById('lookback').value = 400;
|
|
document.getElementById('pred-len').value = 120;
|
|
}
|
|
|
|
|
|
// Update slider based on input fields
|
|
function updateSliderFromInputs() {
|
|
if (!sliderData) return;
|
|
|
|
// Fixed window size: 400 + 120 = 520 data points
|
|
const lookback = 400;
|
|
const predLen = 120;
|
|
const windowSize = lookback + predLen; // Fixed at 520
|
|
|
|
// Calculate slider position
|
|
const totalRows = sliderData.totalRows;
|
|
|
|
if (windowSize > totalRows) {
|
|
// If window size exceeds total data amount, show error
|
|
showStatus('error', `Insufficient data, need at least ${windowSize} data points, currently only ${totalRows} available`);
|
|
return;
|
|
}
|
|
|
|
// Calculate slider position (default select first half of data)
|
|
const startPercentage = 0.1; // Start from 10%
|
|
const endPercentage = startPercentage + (windowSize / totalRows);
|
|
|
|
// Update handle positions
|
|
updateStartHandle(startPercentage);
|
|
updateEndHandle(endPercentage);
|
|
|
|
// Update display information
|
|
updateSliderFromHandles();
|
|
}
|
|
|
|
|
|
// Initialize time window slider
|
|
function initializeTimeWindowSlider(dataInfo) {
|
|
sliderData = {
|
|
startDate: new Date(dataInfo.start_date),
|
|
endDate: new Date(dataInfo.end_date),
|
|
totalRows: dataInfo.rows,
|
|
timeframe: dataInfo.timeframe
|
|
};
|
|
|
|
// Set slider labels
|
|
document.getElementById('min-label').textContent = dataInfo.start_date.split('T')[0];
|
|
document.getElementById('max-label').textContent = dataInfo.end_date.split('T')[0];
|
|
|
|
// Initialize slider position
|
|
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');
|
|
}
|
|
});
|
|
|
|
|
|
// 技术指标图表
|
|
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));
|
|
|
|
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;
|
|
|
|
if (!filePath) throw new Error('Please select a data file first');
|
|
if (isNaN(lookback) || isNaN(predLen)) throw new Error('Invalid parameters');
|
|
|
|
// 获取接口
|
|
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');
|
|
|
|
// 渲染图标
|
|
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;
|
|
}
|
|
}
|
|
|
|
|
|
// 数据表格
|
|
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) {
|
|
showStatus('error', 'Please load data file first');
|
|
return;
|
|
}
|
|
|
|
if (!modelLoaded) {
|
|
showStatus('error', 'Please load model first');
|
|
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);
|
|
|
|
// Get selected time range from time window slider
|
|
const startHandle = document.getElementById('start-handle');
|
|
const startPercentage = parseFloat(startHandle.style.left) / 100;
|
|
|
|
if (!sliderData) {
|
|
showStatus('error', 'Time window slider not initialized');
|
|
return;
|
|
}
|
|
|
|
// Calculate selected time range
|
|
const totalTime = sliderData.endDate.getTime() - sliderData.startDate.getTime();
|
|
const startTime = sliderData.startDate.getTime() + (totalTime * startPercentage);
|
|
const startDate = new Date(startTime);
|
|
|
|
// Get prediction quality parameters
|
|
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), // Format as YYYY-MM-DDTHH:MM
|
|
temperature: temperature,
|
|
top_p: topP,
|
|
sample_count: sampleCount
|
|
};
|
|
|
|
console.log('🚀 Starting prediction, parameters:', predictionParams);
|
|
|
|
const response = await axios.post('/api/predict', predictionParams);
|
|
|
|
console.log('📊 Prediction response received:', response.data);
|
|
|
|
// 添加更详细的响应数据检查
|
|
console.log('🔍 Response data check:');
|
|
console.log('- success:', response.data.success);
|
|
console.log('- has chart:', !!response.data.chart);
|
|
console.log('- chart length:', response.data.chart ? response.data.chart.length : 0);
|
|
console.log('- prediction results count:', response.data.prediction_results ? response.data.prediction_results.length : 0);
|
|
console.log('- actual data count:', response.data.actual_data ? response.data.actual_data.length : 0);
|
|
|
|
|
|
if (response.data.success) {
|
|
// Display prediction results
|
|
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;
|
|
}
|
|
}
|
|
|
|
// // 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');
|
|
console.log('Result keys:', Object.keys(result));
|
|
console.log('Has chart:', !!result.chart);
|
|
console.log('Chart type:', typeof result.chart);
|
|
console.log('Chart length:', result.chart ? result.chart.length : 0);
|
|
console.log('Has comparison:', result.has_comparison);
|
|
console.log('Actual data length:', result.actual_data ? result.actual_data.length : 0);
|
|
|
|
try {
|
|
// Parse and display chart
|
|
if (result.chart) {
|
|
console.log('📊 Parsing chart data...');
|
|
const chartData = JSON.parse(result.chart);
|
|
console.log('📈 Chart data parsed successfully:', chartData);
|
|
|
|
// Clear previous chart
|
|
const chartDiv = document.getElementById('chart');
|
|
chartDiv.innerHTML = '';
|
|
|
|
// Create new chart with error handling
|
|
Plotly.newPlot('chart', chartData.data, chartData.layout, {
|
|
responsive: true
|
|
}).then(function () {
|
|
console.log('✅ Chart rendered successfully');
|
|
// 确保图表容器可见
|
|
chartDiv.style.display = 'block';
|
|
}).catch(function (error) {
|
|
console.error('❌ Chart rendering failed:', error);
|
|
showStatus('error', `图表渲染失败: ${error.message}`);
|
|
|
|
// 显示错误信息
|
|
chartDiv.innerHTML = `
|
|
<div style="text-align: center; padding: 50px; color: #666;">
|
|
<h3>图表加载失败</h3>
|
|
<p>错误信息: ${error.message}</p>
|
|
<p>请检查控制台获取详细信息</p>
|
|
</div>
|
|
`;
|
|
});
|
|
} else {
|
|
console.error('❌ No chart data in response');
|
|
showStatus('error', '服务器返回的图表数据为空');
|
|
}
|
|
|
|
// Display comparison analysis (if actual data exists)
|
|
if (result.has_comparison && result.actual_data && result.actual_data.length > 0) {
|
|
console.log('📊 Displaying comparison analysis');
|
|
displayComparisonAnalysis(result);
|
|
} else {
|
|
console.log('ℹ️ No comparison data available');
|
|
document.getElementById('comparison-section').style.display = 'none';
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('❌ Error displaying prediction result:', error);
|
|
showStatus('error', `结果显示失败: ${error.message}`);
|
|
|
|
// 显示错误信息在图表区域
|
|
const chartDiv = document.getElementById('chart');
|
|
chartDiv.innerHTML = `
|
|
<div style="text-align: center; padding: 50px; color: #666;">
|
|
<h3>数据处理失败</h3>
|
|
<p>错误信息: ${error.message}</p>
|
|
<p>请检查数据格式是否正确</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
|
|
// Display comparison analysis
|
|
function displayComparisonAnalysis(result) {
|
|
document.getElementById('comparison-section').style.display = 'block';
|
|
|
|
// Update comparison information
|
|
document.getElementById('prediction-type').textContent = result.prediction_type;
|
|
document.getElementById('comparison-data').textContent = `${result.actual_data.length} actual data points`;
|
|
|
|
// Calculate error statistics
|
|
const errorStats = getPredictionQuality(result.prediction_results, result.actual_data);
|
|
|
|
// Display error statistics
|
|
document.getElementById('mae').textContent = errorStats.mae.toFixed(4);
|
|
document.getElementById('rmse').textContent = errorStats.rmse.toFixed(4);
|
|
document.getElementById('mape').textContent = errorStats.mape.toFixed(2);
|
|
|
|
// Fill comparison table
|
|
fillComparisonTable(result.prediction_results, result.actual_data);
|
|
}
|
|
|
|
|
|
// Calculate prediction quality metrics
|
|
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];
|
|
|
|
// Use closing price to calculate errors
|
|
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 };
|
|
}
|
|
|
|
|
|
// Fill comparison table
|
|
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);
|
|
}
|
|
}
|
|
|
|
|
|
// Set up event listeners
|
|
function setupEventListeners() {
|
|
// Load model button
|
|
document.getElementById('load-model-btn').addEventListener('click', loadModel);
|
|
|
|
// Load data button
|
|
document.getElementById('load-data-btn').addEventListener('click', loadData);
|
|
|
|
// 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;
|
|
});
|
|
|
|
document.getElementById('top-p').addEventListener('input', function() {
|
|
document.getElementById('top-p-value').textContent = this.value;
|
|
});
|
|
|
|
// 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');
|
|
statusDiv.className = `status ${type}`;
|
|
statusDiv.textContent = message;
|
|
statusDiv.style.display = 'block';
|
|
|
|
// Auto-hide
|
|
setTimeout(() => {
|
|
statusDiv.style.display = 'none';
|
|
}, 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');
|
|
if (show) {
|
|
loadingDiv.classList.add('show');
|
|
} else {
|
|
loadingDiv.classList.remove('show');
|
|
}
|
|
}
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|