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

1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
  1. <!DOCTYPE html>
  2. <html lang = "zh-CN">
  3. <head>
  4. <meta charset = "UTF-8">
  5. <meta name = "viewport" content = "width = device-width, initial-scale = 1.0">
  6. <title>Kronos Financial Prediction Web UI</title>
  7. <!--? <script src = "https://cdn.plot.ly/plotly-latest.min.js"></script>-->
  8. <script src = "https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
  9. <script src = "https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  10. <style>
  11. * {
  12. margin: 0;
  13. padding: 0;
  14. box-sizing: border-box;
  15. }
  16. body {
  17. font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  18. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  19. min-height: 100vh;
  20. color: #333;
  21. }
  22. .container {
  23. max-width: 1400px;
  24. margin: 0 auto;
  25. padding: 20px;
  26. }
  27. .header {
  28. text-align: center;
  29. margin-bottom: 30px;
  30. color: white;
  31. }
  32. .header h1 {
  33. font-size: 2.5rem;
  34. margin-bottom: 10px;
  35. text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
  36. }
  37. .header p {
  38. font-size: 1.1rem;
  39. opacity: 0.9;
  40. }
  41. .main-content {
  42. display: grid;
  43. grid-template-columns: 1fr 2fr;
  44. gap: 30px;
  45. margin-bottom: 30px;
  46. }
  47. .control-panel {
  48. background: white;
  49. border-radius: 15px;
  50. padding: 25px;
  51. box-shadow: 0 10px 30px rgba(0,0,0,0.2);
  52. height: fit-content;
  53. }
  54. .control-panel h2 {
  55. color: #4a5568;
  56. margin-bottom: 20px;
  57. font-size: 1.5rem;
  58. border-bottom: 2px solid #e2e8f0;
  59. padding-bottom: 10px;
  60. }
  61. .form-group {
  62. margin-bottom: 20px;
  63. }
  64. .form-group label {
  65. display: block;
  66. margin-bottom: 8px;
  67. font-weight: 600;
  68. color: #4a5568;
  69. }
  70. .form-group select,
  71. .form-group input {
  72. width: 100%;
  73. padding: 12px;
  74. border: 2px solid #e2e8f0;
  75. border-radius: 8px;
  76. font-size: 14px;
  77. transition: border-color 0.3s ease;
  78. }
  79. .form-group select:focus,
  80. .form-group input:focus {
  81. outline: none;
  82. border-color: #667eea;
  83. }
  84. /* Prediction quality parameter styles */
  85. .form-group input[type="range"] {
  86. width: 70%;
  87. margin-right: 10px;
  88. }
  89. .form-group input[type="number"] {
  90. width: 100%;
  91. }
  92. .form-group span {
  93. display: inline-block;
  94. min-width: 40px;
  95. font-weight: 600;
  96. color: #667eea;
  97. }
  98. .form-text {
  99. font-size: 12px;
  100. color: #718096;
  101. margin-top: 5px;
  102. }
  103. .btn {
  104. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  105. color: white;
  106. border: none;
  107. padding: 12px 24px;
  108. border-radius: 8px;
  109. font-size: 16px;
  110. font-weight: 600;
  111. cursor: pointer;
  112. transition: transform 0.2s ease, box-shadow 0.2s ease;
  113. width: 100%;
  114. margin-bottom: 10px;
  115. }
  116. .btn:hover {
  117. transform: translateY(-2px);
  118. box-shadow: 0 5px 15px rgba(0,0,0,0.2);
  119. }
  120. .btn:disabled {
  121. opacity: 0.6;
  122. cursor: not-allowed;
  123. transform: none;
  124. }
  125. .btn-secondary {
  126. background: linear-gradient(135deg, #718096 0%, #4a5568 100%);
  127. }
  128. .btn-success {
  129. background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
  130. }
  131. .btn-warning {
  132. background: linear-gradient(135deg, #ffc19d 0%, #ffc19d 100%);
  133. }
  134. /*表格弹窗样式*/
  135. .search {
  136. display: flex;
  137. justify-content: space-between;
  138. align-items: center;
  139. margin-bottom: 8px;
  140. }
  141. .search-container{
  142. white-space: nowrap;
  143. padding: 6px 12px;
  144. font-size: 14px;
  145. background-color: #8a94e0;
  146. color: white;
  147. border: none;
  148. border-radius: 6px;
  149. cursor: pointer;
  150. }
  151. .search-modal {
  152. display: none;
  153. position: fixed;
  154. top: 0;
  155. left: 0;
  156. width: 100%;
  157. height: 100%;
  158. background: rgba(0, 0, 0, 0.5);
  159. z-index: 1000;
  160. justify-content: center;
  161. align-items: center;
  162. }
  163. .search-modal-content {
  164. background: white;
  165. padding: 20px;
  166. border-radius: 10px;
  167. width: 90%;
  168. max-width: 1000px;
  169. max-height: 80vh;
  170. display: flex;
  171. flex-direction: column;
  172. text-align: center;
  173. }
  174. .search-modal-header {
  175. display: flex;
  176. justify-content: space-between;
  177. align-items: center;
  178. margin-bottom: 15px;
  179. }
  180. .search-modal-title {
  181. margin: 0;
  182. font-size: 1.5rem;
  183. }
  184. .close-modal-btn {
  185. background: #764ba2;
  186. color: white;
  187. border: none;
  188. padding: 5px 10px;
  189. border-radius: 5px;
  190. cursor: pointer;
  191. font-size: 1rem;
  192. }
  193. .search-results-container {
  194. overflow-y: auto;
  195. flex-grow: 1;
  196. }
  197. .rep-code {
  198. cursor: pointer;
  199. padding: 2px 8px;
  200. background: #f5f7fa;
  201. border-radius: 4px;
  202. font-size: 12px;
  203. color: #1989fa;
  204. transition: background 0.2s;
  205. display: inline-block;
  206. text-align: center;
  207. }
  208. .rep-code:hover {
  209. background: #e8f3ff;
  210. }
  211. .representative-codes {
  212. display: flex;
  213. flex-wrap: wrap;
  214. gap: 6px;
  215. justify-content: center;
  216. }
  217. .chart-grid {
  218. display: grid;
  219. grid-template-columns: 1fr auto;
  220. gap: 10px;
  221. align-items: center;
  222. }
  223. .status {
  224. padding: 15px;
  225. border-radius: 8px;
  226. margin-bottom: 20px;
  227. font-weight: 500;
  228. }
  229. .indicator-status {
  230. padding: 15px;
  231. border-radius: 8px;
  232. margin-bottom: 20px;
  233. font-weight: 500;
  234. }
  235. .status.success {
  236. background: #c6f6d5;
  237. color: #22543d;
  238. border: 1px solid #9ae6b4;
  239. }
  240. .status.error {
  241. background: #fed7d7;
  242. color: #742a2a;
  243. border: 1px solid #feb2b2;
  244. }
  245. .status.info {
  246. background: #bee3f8;
  247. color: #2a4365;
  248. border: 1px solid #90cdf4;
  249. }
  250. .status.warning {
  251. background: #fef5e7;
  252. color: #744210;
  253. border: 1px solid #fbd38d;
  254. }
  255. .chart-container {
  256. background: white;
  257. border-radius: 15px;
  258. padding: 25px;
  259. box-shadow: 0 10px 30px rgba(0,0,0,0.2);
  260. }
  261. .chart-container h2 {
  262. color: #4a5568;
  263. margin-bottom: 20px;
  264. font-size: 1.5rem;
  265. border-bottom: 2px solid #e2e8f0;
  266. padding-bottom: 10px;
  267. }
  268. #chart {
  269. width: 100%;
  270. height: 600px;
  271. }
  272. .data-info {
  273. background: #f7fafc;
  274. border: 1px solid #e2e8f0;
  275. border-radius: 8px;
  276. padding: 15px;
  277. margin-bottom: 20px;
  278. }
  279. .data-info h3 {
  280. color: #4a5568;
  281. margin-bottom: 10px;
  282. font-size: 1.1rem;
  283. }
  284. .data-info p {
  285. margin-bottom: 5px;
  286. color: #4a5568;
  287. }
  288. .data-info strong {
  289. color: #2d3748;
  290. }
  291. /* Time window selector styles */
  292. .time-window-container {
  293. background: #f7fafc;
  294. border: 1px solid #e2e8f0;
  295. border-radius: 8px;
  296. padding: 20px;
  297. margin-bottom: 20px;
  298. }
  299. .time-window-container h3 {
  300. color: #4a5568;
  301. margin-bottom: 15px;
  302. font-size: 1.1rem;
  303. }
  304. .time-window-info {
  305. display: flex;
  306. justify-content: space-between;
  307. margin-bottom: 15px;
  308. font-size: 12px;
  309. color: #666;
  310. }
  311. .time-window-slider {
  312. position: relative;
  313. margin-bottom: 10px;
  314. }
  315. .slider-track {
  316. position: relative;
  317. height: 6px;
  318. background: #e2e8f0;
  319. border-radius: 3px;
  320. cursor: pointer;
  321. }
  322. .slider-handle {
  323. position: absolute;
  324. top: -7px;
  325. width: 20px;
  326. height: 20px;
  327. background: #667eea;
  328. border-radius: 50%;
  329. cursor: grab;
  330. border: 2px solid white;
  331. box-shadow: 0 2px 4px rgba(0,0,0,0.2);
  332. z-index: 10;
  333. }
  334. .slider-handle:hover {
  335. background: #5a67d8;
  336. transform: scale(1.1);
  337. }
  338. .slider-handle:active {
  339. cursor: grabbing;
  340. }
  341. .slider-selection {
  342. position: absolute;
  343. height: 6px;
  344. background: #48bb78;
  345. border-radius: 3px;
  346. top: 0;
  347. }
  348. .slider-labels {
  349. display: flex;
  350. justify-content: space-between;
  351. font-size: 11px;
  352. color: #999;
  353. margin-top: 5px;
  354. }
  355. /* Comparison analysis styles */
  356. .comparison-section {
  357. background: #f7fafc;
  358. border: 1px solid #e2e8f0;
  359. border-radius: 8px;
  360. padding: 20px;
  361. margin-top: 20px;
  362. }
  363. .comparison-section h3 {
  364. color: #4a5568;
  365. margin-bottom: 15px;
  366. font-size: 1.1rem;
  367. }
  368. .comparison-info {
  369. background: white;
  370. border: 1px solid #e2e8f0;
  371. border-radius: 6px;
  372. padding: 15px;
  373. margin-bottom: 15px;
  374. }
  375. .comparison-table {
  376. width: 100%;
  377. border-collapse: collapse;
  378. margin-top: 15px;
  379. }
  380. .comparison-table th,
  381. .comparison-table td {
  382. border: 1px solid #e2e8f0;
  383. padding: 8px;
  384. text-align: center;
  385. font-size: 12px;
  386. }
  387. .comparison-table th {
  388. background: #f7fafc;
  389. font-weight: 600;
  390. }
  391. .error-stats {
  392. display: grid;
  393. grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  394. gap: 15px;
  395. margin-top: 15px;
  396. }
  397. .error-stat {
  398. background: white;
  399. border: 1px solid #e2e8f0;
  400. border-radius: 6px;
  401. padding: 15px;
  402. text-align: center;
  403. }
  404. .error-stat h4 {
  405. color: #4a5568;
  406. margin-bottom: 5px;
  407. font-size: 0.9rem;
  408. }
  409. .error-stat .value {
  410. font-size: 1.5rem;
  411. font-weight: 600;
  412. color: #667eea;
  413. }
  414. .error-stat .unit {
  415. font-size: 0.8rem;
  416. color: #718096;
  417. }
  418. .loading {
  419. display: none;
  420. text-align: center;
  421. padding: 20px;
  422. }
  423. .loading.show {
  424. display: block;
  425. }
  426. .spinner {
  427. border: 4px solid #f3f3f3;
  428. border-top: 4px solid #667eea;
  429. border-radius: 50%;
  430. width: 40px;
  431. height: 40px;
  432. animation: spin 1s linear infinite;
  433. margin: 0 auto 10px;
  434. }
  435. @keyframes spin {
  436. 0% { transform: rotate(0deg); }
  437. 100% { transform: rotate(360deg); }
  438. }
  439. .indicator-loading {
  440. display: none;
  441. text-align: center;
  442. padding: 20px;
  443. }
  444. .indicator-loading.show {
  445. display: block;
  446. }
  447. .indicator-loading .spinner {
  448. border: 4px solid #f3f3f3;
  449. border-top: 4px solid #6dea66;
  450. border-radius: 50%;
  451. width: 40px;
  452. height: 40px;
  453. animation: spin 1s linear infinite;
  454. margin: 0 auto 10px;
  455. }
  456. .model-info {
  457. background: #e6fffa;
  458. border: 1px solid #81e6d9;
  459. border-radius: 8px;
  460. padding: 15px;
  461. margin-bottom: 20px;
  462. }
  463. .model-info h3 {
  464. color: #234e52;
  465. margin-bottom: 10px;
  466. font-size: 1.1rem;
  467. }
  468. .model-info p {
  469. margin-bottom: 5px;
  470. color: #234e52;
  471. }
  472. .model-info strong {
  473. color: #0f2027;
  474. }
  475. @media (max-width: 768px) {
  476. .main-content {
  477. grid-template-columns: 1fr;
  478. }
  479. .container {
  480. padding: 10px;
  481. }
  482. .header h1 {
  483. font-size: 2rem;
  484. }
  485. }
  486. </style>
  487. </head>
  488. <body>
  489. <div class = "container">
  490. <div class = "header">
  491. <h1>🚀 Kronos Financial Prediction Web UI</h1>
  492. <p>AI-based financial K-line data prediction analysis platform</p>
  493. </div>
  494. <div class = "main-content">
  495. <div class = "control-panel">
  496. <h2>🎯 Control Panel</h2>
  497. <!-- Model Selection -->
  498. <div class = "form-group">
  499. <label for = "model-select">Select Model:</label>
  500. <select id = "model-select">
  501. <option value = "">Please load available models first</option>
  502. </select>
  503. <small class = "form-text">Select the Kronos model to use</small>
  504. </div>
  505. <!-- Device Selection -->
  506. <div class = "form-group">
  507. <label for = "device-select">Select Device:</label>
  508. <select id = "device-select">
  509. <option value = "cpu">CPU</option>
  510. <option value = "cuda">CUDA (NVIDIA GPU)</option>
  511. <option value = "mps">MPS (Apple Silicon)</option>
  512. </select>
  513. <small class = "form-text">Select the device to run the model on</small>
  514. </div>
  515. <!-- Model Status -->
  516. <div id = "model-status" class = "status info" style = "display: none;">
  517. Model status information
  518. </div>
  519. <!-- Load Model Button -->
  520. <button id = "load-model-btn" class = "btn btn-secondary">
  521. 🔄 Load Model
  522. </button>
  523. <hr style = "margin: 20px 0; border: 1px solid #e2e8f0;">
  524. <!-- 股票数据采集 -->
  525. <div class = "form-group">
  526. <div class="search">
  527. <label class="sr-only" for="stock_code">Ticker Symbol Input:</label>
  528. <button id="search-btn" class="search-container">
  529. 🔍 Search
  530. </button>
  531. </div>
  532. <input type = "text" class = "form-control" id = "stock_code" name = 'stock_code'
  533. value = "{{ stock_code }}" placeholder = "例如:sh.600000" >
  534. <small class="form-text">Enter the ticker symbol you want to analyze</small>
  535. </div>
  536. <button id="stock-data-btn" class="btn btn-secondary">
  537. 📄 Stock Data
  538. </button>
  539. <!--股票数据提示弹窗-->
  540. <div id="search-modal" class="search-modal">
  541. <div class="search-modal-content">
  542. <div class="search-modal-header">
  543. <h3 class="search-modal-title">📃 Stock Code Prompt:</h3>
  544. <button id="close-modal" class="close-modal-btn">×</button>
  545. </div>
  546. <div class="search-results-container">
  547. <table class="comparison-table">
  548. <thead>
  549. <tr>
  550. <th>Exchange</th>
  551. <th>Prefix</th>
  552. <th>Code_Range</th>
  553. <th>Representative_Code</th>
  554. </tr>
  555. </thead>
  556. <tbody id="search-results-body">
  557. <!-- 上海证券交易所 (SSE)-->
  558. <tr>
  559. <td rowspan="5">上海证券交易所 (SSE)</td>
  560. <td rowspan="5">sh.</td>
  561. <td>600</td>
  562. <td class="representative-codes">
  563. <span class="rep-code">sh.600000(浦发银行)</span>
  564. <span class="rep-code">sh.600036(招商银行)</span>
  565. <span class="rep-code">sh.600519(贵州茅台)</span>
  566. <span class="rep-code">sh.600887(伊利股份)</span>
  567. <span class="rep-code">sh.600900(长江电力)</span>
  568. <span class="rep-code">sh.600030(中信证券)</span>
  569. <span class="rep-code">sh.600050(中国联通)</span>
  570. <span class="rep-code">sh.600690(海尔智家)</span>
  571. <span class="rep-code">sh.600570(恒生电子)</span>
  572. <span class="rep-code">sh.600588(用友网络)</span>
  573. </td>
  574. </tr>
  575. <tr>
  576. <td>601</td>
  577. <td class="representative-codes">
  578. <span class="rep-code">sh.601318(中国平安)</span>
  579. <span class="rep-code">sh.601398(工商银行)</span>
  580. <span class="rep-code">sh.601888(中国中免)</span>
  581. <span class="rep-code">sh.601857(中国石油)</span>
  582. <span class="rep-code">sh.601012(隆基绿能)</span>
  583. <span class="rep-code">sh.601988(中国国航)</span>
  584. <span class="rep-code">sh.601088(中国神华)</span>
  585. <span class="rep-code">sh.601766(中国中车)</span>
  586. <span class="rep-code">sh.601390(中国中铁)</span>
  587. <span class="rep-code">sh.601186(中国铁建)</span>
  588. </td>
  589. </tr>
  590. <tr>
  591. <td>603</td>
  592. <td class="representative-codes">
  593. <span class="rep-code">sh.603288(海天味业)</span>
  594. <span class="rep-code">sh.603259(药明康德)</span>
  595. <span class="rep-code">sh.603986(兆易创新)</span>
  596. <span class="rep-code">sh.603501(韦尔股份)</span>
  597. <span class="rep-code">sh.603993(中科曙光)</span>
  598. <span class="rep-code">sh.603899(晨光股份)</span>
  599. <span class="rep-code">sh.603605(珀莱雅)</span>
  600. <span class="rep-code">sh.603707(健友股份)</span>
  601. <span class="rep-code">sh.603833(欧派家居)</span>
  602. <span class="rep-code">sh.603806(福斯特)</span>
  603. </td>
  604. </tr>
  605. <tr>
  606. <td>605</td>
  607. <td class="representative-codes">
  608. <span class="rep-code">sh.605499(东鹏饮料)</span>
  609. <span class="rep-code">sh.605338(巴比食品)</span>
  610. <span class="rep-code">sh.605111(新洁能)</span>
  611. <span class="rep-code">sh.605589(圣泉集团)</span>
  612. <span class="rep-code">sh.605168(三人行)</span>
  613. <span class="rep-code">sh.605066(天正电气)</span>
  614. <span class="rep-code">sh.605155(西大门)</span>
  615. <span class="rep-code">sh.605136(丽人丽妆)</span>
  616. <span class="rep-code">sh.605333(沪光股份)</span>
  617. <span class="rep-code">sh.605358(立昂微)</span>
  618. </td>
  619. </tr>
  620. <tr>
  621. <td>688</td>
  622. <td class="representative-codes">
  623. <span class="rep-code">sh.688012(中微公司)</span>
  624. <span class="rep-code">sh.688981(中芯国际)</span>
  625. <span class="rep-code">sh.688111(金山办公)</span>
  626. <span class="rep-code">sh.688036(传音控股)</span>
  627. <span class="rep-code">sh.688008(澜起科技)</span>
  628. <span class="rep-code">sh.688235(百济神州)</span>
  629. <span class="rep-code">sh.688185(康希诺)</span>
  630. <span class="rep-code">sh.688036(传音控股)</span>
  631. <span class="rep-code">sh.688187(时代电气)</span>
  632. <span class="rep-code">sh.688390(固德威)</span>
  633. </td>
  634. </tr>
  635. <!-- 深圳证券交易所 (SZSE)-->
  636. <tr>
  637. <td rowspan="5">深圳证券交易所</td>
  638. <td rowspan="5">sz.</td>
  639. <td>000</td>
  640. <td class="representative-codes">
  641. <span class="rep-code">sz.000001(平安银行)</span>
  642. <span class="rep-code">sz.000002(万科A)</span>
  643. <span class="rep-code">sz.000858(五粮液)</span>
  644. <span class="rep-code">sz.000333(美的集团)</span>
  645. <span class="rep-code">sz.000651(格力电器)</span>
  646. <span class="rep-code">sz.000538(云南白药)</span>
  647. <span class="rep-code">sz.000063(中兴通讯)</span>
  648. <span class="rep-code">sz.000100(TCL科技)</span>
  649. <span class="rep-code">sz.000157(中联重科)</span>
  650. <span class="rep-code">sz.000895(双汇发展)</span>
  651. </td>
  652. </tr>
  653. <tr>
  654. <td>001</td>
  655. <td class="representative-codes">
  656. <span class="rep-code">sz.001203(大中矿业)</span>
  657. <span class="rep-code">sz.001208(华菱线缆)</span>
  658. <span class="rep-code">sz.001212(中旗新材)</span>
  659. <span class="rep-code">sz.001213(中铁特货)</span>
  660. <span class="rep-code">sz.001215(千味央厨)</span>
  661. <span class="rep-code">sz.001217(华尔泰)</span>
  662. <span class="rep-code">sz.001218(丽臣实业)</span>
  663. <span class="rep-code">sz.001222(源飞宠物)</span>
  664. <span class="rep-code">sz.001225(和泰机电)</span>
  665. <span class="rep-code">sz.001309(德明利)</span>
  666. </td>
  667. </tr>
  668. <tr>
  669. <td>002</td>
  670. <td class="representative-codes">
  671. <span class="rep-code">sz.002024(苏宁易购)</span>
  672. <span class="rep-code">sz.002027(分众传媒)</span>
  673. <span class="rep-code">sz.002555(三七互娱)</span>
  674. <span class="rep-code">sz.002624(完美世界)</span>
  675. <span class="rep-code">sz.002049(紫光国微)</span>
  676. <span class="rep-code">sz.002179(中航光电)</span>
  677. <span class="rep-code">sz.002415(海康威视)</span>
  678. <span class="rep-code">sz.002475(立讯精密)</span>
  679. <span class="rep-code">sz.002594(比亚迪)</span>
  680. <span class="rep-code">sz.002142(宁波银行)</span>
  681. </td>
  682. </tr>
  683. <tr>
  684. <td>003</td>
  685. <td class="representative-codes">
  686. <span class="rep-code">sz.003816(中国广核)</span>
  687. <span class="rep-code">sz.003022(联泓新科)</span>
  688. <span class="rep-code">sz.003031(中瓷电子)</span>
  689. <span class="rep-code">sz.003032(传智教育)</span>
  690. <span class="rep-code">sz.003035(南网能源)</span>
  691. <span class="rep-code">sz.003036(泰坦股份)</span>
  692. <span class="rep-code">sz.003000(劲仔食品)</span>
  693. <span class="rep-code">sz.003001(中晶科技)</span>
  694. <span class="rep-code">sz.003002(传智教育)</span>
  695. <span class="rep-code">sz.003011(海象新材)</span>
  696. </td>
  697. </tr>
  698. <tr>
  699. <td>300</td>
  700. <td class="representative-codes">
  701. <span class="rep-code">sz.300750(宁德时代)</span>
  702. <span class="rep-code">sz.300059(东方财富)</span>
  703. <span class="rep-code">sz.300124(汇川技术)</span>
  704. <span class="rep-code">sz.300760(迈瑞医疗)</span>
  705. <span class="rep-code">sz.300014(亿纬锂能)</span>
  706. <span class="rep-code">sz.300122(智飞生物)</span>
  707. <span class="rep-code">sz.300015(爱尔眼科)</span>
  708. <span class="rep-code">sz.300274(阳光电源)</span>
  709. <span class="rep-code">sz.300496(中科创达)</span>
  710. <span class="rep-code">sz.300782(卓胜微)</span>
  711. </td>
  712. </tr>
  713. <!--? 北京证券交易所 (BSE) -->
  714. <!--? <tr>-->
  715. <!--? <td rowspan="5">北京证券交易所 (BSE)</td>-->
  716. <!--? <td rowspan="5">bj.</td>-->
  717. <!--? <td>43</td>-->
  718. <!--? <td class="representative-codes">-->
  719. <!--? <span class="rep-code">bj.430047(诺思兰德)</span>-->
  720. <!--? <span class="rep-code">bj.430090(同辉信息)</span>-->
  721. <!--? <span class="rep-code">bj.430198(微创光电)</span>-->
  722. <!--? <span class="rep-code">bj.430300(辰光医疗)</span>-->
  723. <!--? <span class="rep-code">bj.430425(乐创技术)</span>-->
  724. <!--? <span class="rep-code">bj.430476(海能技术)</span>-->
  725. <!--? <span class="rep-code">bj.430510(丰光精密)</span>-->
  726. <!--? <span class="rep-code">bj.430556(雅葆轩)</span>-->
  727. <!--? <span class="rep-code">bj.430564(天润科技)</span>-->
  728. <!--? <span class="rep-code">bj.430685(新芝生物)</span>-->
  729. <!--? </td>-->
  730. <!--? </tr>-->
  731. <!--? <tr>-->
  732. <!--? <td>83</td>-->
  733. <!--? <td class="representative-codes">-->
  734. <!--? <span class="rep-code">bj.830799(艾融软件)</span>-->
  735. <!--? <span class="rep-code">bj.830809(安达科技)</span>-->
  736. <!--? <span class="rep-code">bj.830839(万通液压)</span>-->
  737. <!--? <span class="rep-code">bj.830866(齐鲁华信)</span>-->
  738. <!--? <span class="rep-code">bj.830879(基康仪器)</span>-->
  739. <!--? <span class="rep-code">bj.830896(旺成科技)</span>-->
  740. <!--? <span class="rep-code">bj.830946(森萱医药)</span>-->
  741. <!--? <span class="rep-code">bj.830964(润农节水)</span>-->
  742. <!--? <span class="rep-code">bj.831010(凯添燃气)</span>-->
  743. <!--? <span class="rep-code">bj.831039(国义招标)</span>-->
  744. <!--? </td>-->
  745. <!--? </tr>-->
  746. <!--? <tr>-->
  747. <!--? <td>87</td>-->
  748. <!--? <td class="representative-codes">-->
  749. <!--? <span class="rep-code">bj.870204(沪江材料)</span>-->
  750. <!--? <span class="rep-code">bj.870357(雅葆轩)</span>-->
  751. <!--? <span class="rep-code">bj.870436(大地电气)</span>-->
  752. <!--? <span class="rep-code">bj.870640(曙光数创)</span>-->
  753. <!--? <span class="rep-code">bj.870726(鸿智科技)</span>-->
  754. <!--? <span class="rep-code">bj.870866(视声智能)</span>-->
  755. <!--? <span class="rep-code">bj.870976(视声智能)</span>-->
  756. <!--? <span class="rep-code">bj.871263(一致魔芋)</span>-->
  757. <!--? <span class="rep-code">bj.871396(佳合科技)</span>-->
  758. <!--? <span class="rep-code">bj.871478(海泰新能)</span>-->
  759. <!--? </td>-->
  760. <!--? </tr>-->
  761. <!--? <tr>-->
  762. <!--? <td>89</td>-->
  763. <!--? <td class="representative-codes">-->
  764. <!--? <span class="rep-code">bj.892089(科强股份)</span>-->
  765. <!--? <span class="rep-code">bj.892282(美心翼申)</span>-->
  766. <!--? <span class="rep-code">bj.892358(派诺科技)</span>-->
  767. <!--? <span class="rep-code">bj.892519(捷众科技)</span>-->
  768. <!--? <span class="rep-code">bj.892541(康农种业)</span>-->
  769. <!--? <span class="rep-code">bj.892622(许昌智能)</span>-->
  770. <!--? <span class="rep-code">bj.892679(云星宇)</span>-->
  771. <!--? <span class="rep-code">bj.892748(欣捷高新)</span>-->
  772. <!--? <span class="rep-code">bj.892810(捷安高科)</span>-->
  773. <!--? <span class="rep-code">bj.892925(海昇药业)</span>-->
  774. <!--? </td>-->
  775. <!--? </tr>-->
  776. <!--? <tr>-->
  777. <!--? <td>920</td>-->
  778. <!--? <td class="representative-codes">-->
  779. <!--? <span class="rep-code">bj.920099(万达轴承)</span>-->
  780. <!--? <span class="rep-code">bj.920175(欧福蛋业)</span>-->
  781. <!--? <span class="rep-code">bj.920177(瑞华技术)</span>-->
  782. <!--? <span class="rep-code">bj.920179(和特能源)</span>-->
  783. <!--? <span class="rep-code">bj.920180(豪钢重工)</span>-->
  784. <!--? <span class="rep-code">bj.920179(和特能源)</span>-->
  785. <!--? </td>-->
  786. <!--? </tr>-->
  787. </tbody>
  788. </table>
  789. </div>
  790. </div>
  791. </div>
  792. <hr style="margin: 20px 0; border: 1px solid #e2e8f0;">
  793. <!-- Data File Selection -->
  794. <div class = "form-group">
  795. <label for = "data-file-select">Select Data File:</label>
  796. <select id = "data-file-select">
  797. <option value = "">Please load data file list first</option>
  798. </select>
  799. <small class = "form-text">Select K-line data file from data directory</small>
  800. </div>
  801. <button id = "load-data-btn" class = "btn btn-secondary">
  802. 📁 Load Data
  803. </button>
  804. <!-- Data Information Display -->
  805. <div id = "data-info" class = "data-info" style = "display: none;">
  806. <h3>📊 Data Information</h3>
  807. <p><strong>Rows:</strong> <span id = "data-rows">-</span></p>
  808. <p><strong>Columns:</strong> <span id = "data-cols">-</span></p>
  809. <p><strong>Time Range:</strong> <span id = "data-time-range">-</span></p>
  810. <p><strong>Price Range:</strong> <span id = "data-price-range">-</span></p>
  811. <p><strong>Time Frequency:</strong> <span id = "data-timeframe">-</span></p>
  812. <p><strong>Prediction Columns:</strong> <span id = "data-prediction-cols">-</span></p>
  813. </div>
  814. <hr style = "margin: 20px 0; border: 1px solid #e2e8f0;">
  815. <!-- Time Window Selector -->
  816. <div class = "time-window-container">
  817. <h3>⏰ Time Window Selection</h3>
  818. <div class = "time-window-info">
  819. <span id = "window-start">Start: --</span>
  820. <span id = "window-end">End: --</span>
  821. <span id = "window-size">Window Size: 400 + 120 = 520 data points</span>
  822. </div>
  823. <div class = "time-window-slider">
  824. <div class = "slider-track">
  825. <div class = "slider-handle start-handle" id = "start-handle"></div>
  826. <div class = "slider-selection" id = "slider-selection"></div>
  827. <div class = "slider-handle end-handle" id = "end-handle"></div>
  828. </div>
  829. <div class = "slider-labels">
  830. <span id = "min-label">Earliest</span>
  831. <span id = "max-label">Latest</span>
  832. </div>
  833. </div>
  834. <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>
  835. </div>
  836. <!-- Prediction Parameters -->
  837. <div class="form-group">
  838. <label for="lookback">Lookback Window Size:</label>
  839. <input type="number" id="lookback" value="400" readonly>
  840. <small class="form-text">Fixed at 400 data points</small>
  841. </div>
  842. <div class="form-group">
  843. <label for="pred-len">Prediction Length:</label>
  844. <input type="number" id="pred-len" value="120" readonly>
  845. <small class="form-text">Fixed at 120 data points</small>
  846. </div>
  847. <!-- Prediction Quality Parameters -->
  848. <div class="form-group">
  849. <label for="temperature">Prediction Temperature (T):</label>
  850. <input type="range" id="temperature" value="1.0" min="0.1" max="2.0" step="0.1">
  851. <span id="temperature-value">1.0</span>
  852. <small class="form-text">Controls prediction randomness, higher values make predictions more
  853. diverse, lower values make predictions more conservative</small>
  854. </div>
  855. <div class="form-group">
  856. <label for="top-p">Nucleus Sampling Parameter (top_p):</label>
  857. <input type="range" id="top-p" value="0.9" min="0.1" max="1.0" step="0.1">
  858. <span id="top-p-value">0.9</span>
  859. <small class="form-text">Controls prediction diversity, higher values consider broader probability
  860. distributions</small>
  861. </div>
  862. <div class="form-group">
  863. <label for="sample-count">Sample Count:</label>
  864. <input type="number" id="sample-count" value="1" min="1" max="5" step="1">
  865. <small class="form-text">Generate multiple prediction samples to improve quality (recommended
  866. 1-3)</small>
  867. </div>
  868. <button id="predict-btn" class="btn btn-success" disabled>
  869. 🔮 Start Prediction
  870. </button>
  871. <!-- Loading Status -->
  872. <div id="loading" class="loading">
  873. <div class="spinner"></div>
  874. <p>Processing, please wait...</p>
  875. </div>
  876. </div>
  877. <div class="chart-container">
  878. <h2>📈 Prediction Results Chart</h2>
  879. <div id="chart"></div>
  880. <!-- Comparison Analysis -->
  881. <div id="comparison-section" class="comparison-section" style="display: none;">
  882. <h3>📊 Prediction vs Actual Data Comparison</h3>
  883. <div id="comparison-info" class="comparison-info">
  884. <p><strong>Prediction Type:</strong> <span id="prediction-type">-</span></p>
  885. <p><strong>Comparison Data:</strong> <span id="comparison-data">-</span></p>
  886. </div>
  887. <div class="error-stats">
  888. <div class="error-stat">
  889. <h4>Mean Absolute Error</h4>
  890. <div class="value" id="mae">-</div>
  891. <div class="unit">Price Units</div>
  892. </div>
  893. <div class="error-stat">
  894. <h4>Root Mean Square Error</h4>
  895. <div class="value" id="rmse">-</div>
  896. <div class="unit">Price Units</div>
  897. </div>
  898. <div class="error-stat">
  899. <h4>Mean Absolute Percentage Error</h4>
  900. <div class="value" id="mape">-</div>
  901. <div class="unit">%</div>
  902. </div>
  903. </div>
  904. <div class="error-details">
  905. <h4>Detailed Comparison Data:</h4>
  906. <div style="max-height: 300px; overflow-y: auto;">
  907. <table class="comparison-table">
  908. <thead>
  909. <tr>
  910. <th>Time</th>
  911. <th>Actual Open</th>
  912. <th>Predicted Open</th>
  913. <th>Actual High</th>
  914. <th>Predicted High</th>
  915. <th>Actual Low</th>
  916. <th>Predicted Low</th>
  917. <th>Actual Close</th>
  918. <th>Predicted Close</th>
  919. </tr>
  920. </thead>
  921. <tbody id="comparison-tbody">
  922. </tbody>
  923. </table>
  924. </div>
  925. </div>
  926. </div>
  927. <br>
  928. <!--技术指标图表-->
  929. <h2>📶 Technical Indicator Chart</h2>
  930. <div id="indicator-status" class="indicator-status" style="display: none;"></div>
  931. <div class="form-group">
  932. <label for="diagram_type">Select Diagram Type:</label>
  933. <div class="chart-grid">
  934. <select id="diagram_type" class="form-control">
  935. <option value="Volume Chart (VOL)">Volume Chart (VOL)</option>
  936. <option value="Moving Average (MA)">Moving Average (MA)</option>
  937. <option value="MACD Indicator (MACD)">MACD Indicator (MACD)</option>
  938. <option value="RSI Indicator (RSI)">RSI Indicator (RSI)</option>
  939. <option value="Bollinger Bands (BB)">Bollinger Bands (BB)</option>
  940. <option value="Stochastic Oscillator (STOCH)">Stochastic Oscillator (STOCH)</option>
  941. <option value="Rolling Window Mean Strategy">Rolling Window Mean Strategy</option>
  942. <option value="TRIX Indicator (TRIX)">TRIX Indicator (TRIX)</option>
  943. </select>
  944. <button id="generate-chart-btn" class="btn btn-warning">
  945. ✏️ Generate chart
  946. </button>
  947. </div>
  948. <small class="form-text">Select the type to draw the relevant chart</small>
  949. </div>
  950. <div id="indicator-loading" class="indicator-loading">
  951. <div class="spinner"></div>
  952. <p>Generating chart, please wait...</p>
  953. </div>
  954. <div id="indicator-chart"></div>
  955. <!-- 数据表格 -->
  956. <div id="data-presentation" class="comparison-section" style="display: none;">
  957. <h3>💹 Financial Data Visualization</h3>
  958. <div class="error-details">
  959. <h4>Detailed Financial Data:</h4>
  960. <div style="max-height: 300px; overflow-y: auto;">
  961. <table class="comparison-table">
  962. <thead>
  963. <tr>
  964. <th>Timestamps</th>
  965. <th>Open</th>
  966. <th>High</th>
  967. <th>Low</th>
  968. <th>Close</th>
  969. <th>Volume</th>
  970. <th>Amount</th>
  971. </tr>
  972. </thead>
  973. <tbody id="data-tbody">
  974. </tbody>
  975. </table>
  976. </div>
  977. </div>
  978. </div>
  979. </div>
  980. </div>
  981. </div>
  982. <script>
  983. // Global variables
  984. let currentDataFile = null;
  985. let currentDataInfo = null;
  986. let availableModels = [];
  987. let modelLoaded = false;
  988. // Initialize after page loads
  989. document.addEventListener('DOMContentLoaded', function() {
  990. initializeApp();
  991. });
  992. // Initialize application
  993. async function initializeApp() {
  994. console.log('🚀 Initializing Kronos Web UI...');
  995. // Load available models
  996. await loadAvailableModels();
  997. // Load data file list
  998. await loadDataFiles();
  999. // Set up event listeners
  1000. setupEventListeners();
  1001. // Initialize time slider
  1002. initializeTimeSlider();
  1003. console.log('✅ Application initialization completed');
  1004. }
  1005. // Load available models
  1006. async function loadAvailableModels() {
  1007. try {
  1008. console.log('开始加载模型列表...');
  1009. const response = await fetch('/api/available-models');
  1010. const data = await response.json();
  1011. console.log('API返回数据:', data);
  1012. const modelSelect = document.getElementById('model-select');
  1013. console.log('找到的下拉菜单:', modelSelect);
  1014. if (!modelSelect) {
  1015. console.error('错误: 找不到 modelSelect 元素');
  1016. return;
  1017. }
  1018. // 清空现有选项
  1019. modelSelect.innerHTML = '';
  1020. // 添加模型选项
  1021. for (const [key, model] of Object.entries(data.models)) {
  1022. const option = document.createElement('option');
  1023. option.value = key;
  1024. option.textContent = `${model.name} (${model.params}) - ${model.description}`;
  1025. modelSelect.appendChild(option);
  1026. console.log('添加选项:', option.textContent);
  1027. }
  1028. console.log('最终选项数量:', modelSelect.options.length);
  1029. } catch (error) {
  1030. console.error('加载模型列表失败:', error);
  1031. }
  1032. }
  1033. // Populate model selection dropdown
  1034. function populateModelSelect() {
  1035. const modelSelect = document.getElementById('model-select');
  1036. modelSelect.innerHTML = '<option value="">Please select model</option>';
  1037. Object.entries(availableModels).forEach(([key, model]) => {
  1038. const option = document.createElement('option');
  1039. option.value = key;
  1040. option.textContent = `${model.name} (${model.params}) - ${model.description}`;
  1041. modelSelect.appendChild(option);
  1042. });
  1043. }
  1044. // Load model
  1045. async function loadModel() {
  1046. const modelKey = document.getElementById('model-select').value;
  1047. const device = document.getElementById('device-select').value;
  1048. if (!modelKey) {
  1049. showStatus('error', 'Please select a model to load');
  1050. return;
  1051. }
  1052. try {
  1053. showLoading(true);
  1054. document.getElementById('load-model-btn').disabled = true;
  1055. const response = await axios.post('/api/load-model', {
  1056. model_key: modelKey,
  1057. device: device
  1058. });
  1059. if (response.data.success) {
  1060. modelLoaded = true;
  1061. showStatus('success', response.data.message);
  1062. updateModelStatus();
  1063. document.getElementById('predict-btn').disabled = false;
  1064. console.log('✅ Model loaded successfully:', response.data.model_info);
  1065. } else {
  1066. showStatus('error', response.data.error);
  1067. }
  1068. } catch (error) {
  1069. console.error('❌ Model loading failed:', error);
  1070. showStatus('error', `Model loading failed: ${error.response?.data?.error || error.message}`);
  1071. } finally {
  1072. showLoading(false);
  1073. document.getElementById('load-model-btn').disabled = false;
  1074. }
  1075. }
  1076. // Update model status
  1077. async function updateModelStatus() {
  1078. try {
  1079. const response = await axios.get('/api/model-status');
  1080. const status = response.data;
  1081. if (status.loaded) {
  1082. showStatus('success', `Model loaded: ${status.current_model.name} on ${status.current_model.device}`);
  1083. } else if (status.available) {
  1084. showStatus('info', 'Model available but not loaded');
  1085. } else {
  1086. showStatus('warning', 'Model library not available');
  1087. }
  1088. } catch (error) {
  1089. console.error('❌ Failed to get model status:', error);
  1090. }
  1091. }
  1092. //Stock Data按钮
  1093. document.addEventListener('DOMContentLoaded', function() {
  1094. const generateChartBtn = document.getElementById('stock-data-btn');
  1095. if (generateChartBtn) {
  1096. generateChartBtn.addEventListener('click', StockData);
  1097. console.log('Stock Data button event listener bound');
  1098. } else {
  1099. console.error('stock-data-btn element not found');
  1100. }
  1101. });
  1102. async function StockData() {
  1103. console.log('Get stock data...');
  1104. const stockCodeInput = document.getElementById('stock_code');
  1105. const generateBtn = document.getElementById('stock-data-btn');
  1106. const stockCode = stockCodeInput.value.trim();
  1107. generateBtn.disabled = true;
  1108. try {
  1109. if (!stockCode) {
  1110. showStatus('error', 'Stock code cannot be empty');
  1111. return;
  1112. }
  1113. const stockCodeRegex = /^[a-z]+\.\d+$/;
  1114. if (!stockCodeRegex.test(stockCode)) {
  1115. showStatus('error', 'The ticker symbol is in the wrong format');
  1116. return;
  1117. }
  1118. showLoading(true);
  1119. const response = await axios.post('/api/stock-data', {stock_code: stockCode});
  1120. if (response.data.success) {
  1121. showStatus('success', `Successfully fetched data for ${stockCode}`);
  1122. loadDataFiles();
  1123. stockCodeInput.value = '';
  1124. } else {
  1125. showStatus('error', response.data.error || 'Failed to fetch stock data');
  1126. }
  1127. } catch (error) {
  1128. console.error('❌ Failed to fetch stock data:', error);
  1129. showStatus('error', `Failed to fetch data`);
  1130. } finally {
  1131. showLoading(false);
  1132. if (generateBtn) generateBtn.disabled = false;
  1133. }
  1134. }
  1135. // Search按钮表格弹窗
  1136. document.addEventListener('DOMContentLoaded', function() {
  1137. const searchBtn = document.getElementById('search-btn');
  1138. const searchModal = document.getElementById('search-modal');
  1139. const closeModal = document.getElementById('close-modal');
  1140. const stockCodeInput = document.getElementById('stock_code');
  1141. searchBtn.addEventListener('click', () => {
  1142. searchModal.style.display = 'flex';
  1143. });
  1144. closeModal.addEventListener('click', () => {
  1145. searchModal.style.display = 'none';
  1146. });
  1147. document.addEventListener('click', (e) => {
  1148. if (e.target.classList.contains('rep-code')) {
  1149. const code = e.target.textContent.match(/[a-z]+\.\d+/)[0];
  1150. stockCodeInput.value = code;
  1151. closeModalFunc();
  1152. }
  1153. });
  1154. });
  1155. // Load data file list
  1156. async function loadDataFiles() {
  1157. try {
  1158. const response = await axios.get('/api/data-files');
  1159. const dataFiles = response.data;
  1160. const dataFileSelect = document.getElementById('data-file-select');
  1161. dataFileSelect.innerHTML = '<option value="">Please select data file</option>';
  1162. dataFiles.forEach(file => {
  1163. const option = document.createElement('option');
  1164. option.value = file.path;
  1165. option.textContent = `${file.name} (${file.size})`;
  1166. dataFileSelect.appendChild(option);
  1167. });
  1168. console.log('✅ Data file list loaded successfully:', dataFiles);
  1169. } catch (error) {
  1170. console.error('❌ Failed to load data file list:', error);
  1171. showStatus('error', 'Failed to load data file list');
  1172. }
  1173. }
  1174. // Load data file
  1175. async function loadData() {
  1176. const filePath = document.getElementById('data-file-select').value;
  1177. if (!filePath) {
  1178. showStatus('error', 'Please select a data file to load');
  1179. return;
  1180. }
  1181. try {
  1182. showLoading(true);
  1183. document.getElementById('load-data-btn').disabled = true;
  1184. const response = await axios.post('/api/load-data', {
  1185. file_path: filePath
  1186. });
  1187. if (response.data.success) {
  1188. currentDataFile = filePath;
  1189. currentDataInfo = response.data.data_info;
  1190. showDataInfo(response.data.data_info);
  1191. showStatus('success', response.data.message);
  1192. // Update prediction button status
  1193. if (modelLoaded) {
  1194. document.getElementById('predict-btn').disabled = false;
  1195. }
  1196. console.log('✅ Data loaded successfully:', response.data.data_info);
  1197. } else {
  1198. showStatus('error', response.data.error);
  1199. }
  1200. } catch (error) {
  1201. console.error('❌ Data loading failed:', error);
  1202. showStatus('error', `Data loading failed: ${error.response?.data?.error || error.message}`);
  1203. } finally {
  1204. showLoading(false);
  1205. document.getElementById('load-data-btn').disabled = false;
  1206. }
  1207. }
  1208. // Display data information
  1209. function showDataInfo(dataInfo) {
  1210. document.getElementById('data-info').style.display = 'block';
  1211. document.getElementById('data-rows').textContent = dataInfo.rows;
  1212. document.getElementById('data-cols').textContent = dataInfo.columns.length;
  1213. document.getElementById('data-time-range').textContent = `${dataInfo.start_date} to ${dataInfo.end_date}`;
  1214. document.getElementById('data-price-range').textContent = `${dataInfo.price_range.min.toFixed(4)} - ${dataInfo.price_range.max.toFixed(4)}`;
  1215. document.getElementById('data-timeframe').textContent = dataInfo.timeframe;
  1216. document.getElementById('data-prediction-cols').textContent = dataInfo.prediction_columns.join(', ');
  1217. // Initialize time window slider
  1218. initializeTimeWindowSlider(dataInfo);
  1219. }
  1220. // Time window slider related variables
  1221. let sliderData = null;
  1222. let isDragging = false;
  1223. let currentHandle = null;
  1224. // Initialize time window slider
  1225. function initializeTimeSlider() {
  1226. // Set up slider event listeners
  1227. setupSliderEventListeners();
  1228. }
  1229. // Set up slider event listeners
  1230. function setupSliderEventListeners() {
  1231. const startHandle = document.getElementById('start-handle');
  1232. const endHandle = document.getElementById('end-handle');
  1233. const track = document.querySelector('.slider-track');
  1234. // Start dragging
  1235. startHandle.addEventListener('mousedown', (e) => {
  1236. isDragging = true;
  1237. currentHandle = 'start';
  1238. e.preventDefault();
  1239. });
  1240. endHandle.addEventListener('mousedown', (e) => {
  1241. isDragging = true;
  1242. currentHandle = 'end';
  1243. e.preventDefault();
  1244. });
  1245. // Dragging
  1246. document.addEventListener('mousemove', (e) => {
  1247. if (!isDragging) return;
  1248. const rect = track.getBoundingClientRect();
  1249. const x = e.clientX - rect.left;
  1250. const percentage = Math.max(0, Math.min(1, x / rect.width));
  1251. if (currentHandle === 'start') {
  1252. updateStartHandle(percentage);
  1253. } else if (currentHandle === 'end') {
  1254. updateEndHandle(percentage);
  1255. }
  1256. updateSliderFromHandles();
  1257. });
  1258. // End dragging
  1259. document.addEventListener('mouseup', () => {
  1260. isDragging = false;
  1261. currentHandle = null;
  1262. });
  1263. // Click track to set position directly
  1264. track.addEventListener('click', (e) => {
  1265. const rect = track.getBoundingClientRect();
  1266. const x = e.clientX - rect.left;
  1267. const percentage = Math.max(0, Math.min(1, x / rect.width));
  1268. // Determine which handle is closer to the click position
  1269. const startHandle = document.getElementById('start-handle');
  1270. const endHandle = document.getElementById('end-handle');
  1271. const startRect = startHandle.getBoundingClientRect();
  1272. const endRect = endHandle.getBoundingClientRect();
  1273. if (Math.abs(x - (startRect.left - rect.left)) < Math.abs(x - (endRect.left - rect.left))) {
  1274. updateStartHandle(percentage);
  1275. } else {
  1276. updateEndHandle(percentage);
  1277. }
  1278. updateSliderFromHandles();
  1279. });
  1280. }
  1281. // Update start handle position
  1282. function updateStartHandle(percentage) {
  1283. const startHandle = document.getElementById('start-handle');
  1284. const selection = document.getElementById('slider-selection');
  1285. // Fixed window size of 520 data points
  1286. const windowSize = 520;
  1287. const totalRows = sliderData ? sliderData.totalRows : 1000;
  1288. const windowPercentage = windowSize / totalRows;
  1289. // Ensure start handle doesn't cause window to exceed data range
  1290. if (percentage + windowPercentage > 1) {
  1291. percentage = 1 - windowPercentage;
  1292. }
  1293. startHandle.style.left = (percentage * 100) + '%';
  1294. selection.style.left = (percentage * 100) + '%';
  1295. selection.style.width = (windowPercentage * 100) + '%';
  1296. // Automatically adjust end handle position to maintain fixed window size
  1297. const endHandle = document.getElementById('end-handle');
  1298. endHandle.style.left = ((percentage + windowPercentage) * 100) + '%';
  1299. }
  1300. // Update end handle position
  1301. function updateEndHandle(percentage) {
  1302. const endHandle = document.getElementById('end-handle');
  1303. const selection = document.getElementById('slider-selection');
  1304. // Fixed window size of 520 data points
  1305. const windowSize = 520;
  1306. const totalRows = sliderData ? sliderData.totalRows : 1000;
  1307. const windowPercentage = windowSize / totalRows;
  1308. // Ensure end handle doesn't cause window to exceed data range
  1309. if (percentage - windowPercentage < 0) {
  1310. percentage = windowPercentage;
  1311. }
  1312. endHandle.style.left = (percentage * 100) + '%';
  1313. selection.style.left = ((percentage - windowPercentage) * 100) + '%';
  1314. selection.style.width = (windowPercentage * 100) + '%';
  1315. // Automatically adjust start handle position to maintain fixed window size
  1316. const startHandle = document.getElementById('start-handle');
  1317. startHandle.style.left = ((percentage - windowPercentage) * 100) + '%';
  1318. }
  1319. // Update slider display based on handle positions
  1320. function updateSliderFromHandles() {
  1321. const startHandle = document.getElementById('start-handle');
  1322. const endHandle = document.getElementById('end-handle');
  1323. const startPercentage = parseFloat(startHandle.style.left) / 100;
  1324. const endPercentage = parseFloat(endHandle.style.left) / 100;
  1325. if (!sliderData) return;
  1326. // Calculate selected time range
  1327. const totalTime = sliderData.endDate.getTime() - sliderData.startDate.getTime();
  1328. const startTime = sliderData.startDate.getTime() + (totalTime * startPercentage);
  1329. const endTime = sliderData.startDate.getTime() + (totalTime * endPercentage);
  1330. const startDate = new Date(startTime);
  1331. const endDate = new Date(endTime);
  1332. // Update display information
  1333. document.getElementById('window-start').textContent = `Start: ${startDate.toLocaleDateString()}`;
  1334. document.getElementById('window-end').textContent = `End: ${endDate.toLocaleDateString()}`;
  1335. // Display fixed window size
  1336. document.getElementById('window-size').textContent = `Window Size: 400 + 120 = 520 data points (fixed)`;
  1337. // Input field values remain fixed
  1338. document.getElementById('lookback').value = 400;
  1339. document.getElementById('pred-len').value = 120;
  1340. }
  1341. // Update slider based on input fields
  1342. function updateSliderFromInputs() {
  1343. if (!sliderData) return;
  1344. // Fixed window size: 400 + 120 = 520 data points
  1345. const lookback = 400;
  1346. const predLen = 120;
  1347. const windowSize = lookback + predLen; // Fixed at 520
  1348. // Calculate slider position
  1349. const totalRows = sliderData.totalRows;
  1350. if (windowSize > totalRows) {
  1351. // If window size exceeds total data amount, show error
  1352. showStatus('error', `Insufficient data, need at least ${windowSize} data points, currently only ${totalRows} available`);
  1353. return;
  1354. }
  1355. // Calculate slider position (default select first half of data)
  1356. const startPercentage = 0.1; // Start from 10%
  1357. const endPercentage = startPercentage + (windowSize / totalRows);
  1358. // Update handle positions
  1359. updateStartHandle(startPercentage);
  1360. updateEndHandle(endPercentage);
  1361. // Update display information
  1362. updateSliderFromHandles();
  1363. }
  1364. // Initialize time window slider
  1365. function initializeTimeWindowSlider(dataInfo) {
  1366. sliderData = {
  1367. startDate: new Date(dataInfo.start_date),
  1368. endDate: new Date(dataInfo.end_date),
  1369. totalRows: dataInfo.rows,
  1370. timeframe: dataInfo.timeframe
  1371. };
  1372. // Set slider labels
  1373. document.getElementById('min-label').textContent = dataInfo.start_date.split('T')[0];
  1374. document.getElementById('max-label').textContent = dataInfo.end_date.split('T')[0];
  1375. // Initialize slider position
  1376. updateSliderFromInputs();
  1377. }
  1378. // Binding Generate chart
  1379. document.addEventListener('DOMContentLoaded', function() {
  1380. const generateChartBtn = document.getElementById('generate-chart-btn');
  1381. if (generateChartBtn) {
  1382. generateChartBtn.addEventListener('click', generateTechnicalChart);
  1383. console.log('Generate chart button event listener bound');
  1384. } else {
  1385. console.error('generate-chart-btn element not found');
  1386. }
  1387. });
  1388. // 技术指标图表
  1389. async function generateTechnicalChart() {
  1390. console.log('Generating technical indicator chart...');
  1391. const indicatorLoading = document.getElementById('indicator-loading');
  1392. indicatorLoading.classList.add('show');
  1393. const generateBtn = document.getElementById('generate-chart-btn');
  1394. generateBtn.disabled = true;
  1395. try {
  1396. await new Promise(resolve => setTimeout(resolve, 2000));
  1397. const startHandle = document.getElementById('start-handle');
  1398. const endHandle = document.getElementById('end-handle');
  1399. const startPercentage = parseFloat(startHandle.style.left) / 100;
  1400. const endPercentage = parseFloat(endHandle.style.left) / 100;
  1401. const totalRows = sliderData ? sliderData.totalRows : 0;
  1402. const historicalStartIdx = Math.floor(startPercentage * totalRows);
  1403. const lookback = Math.floor((endPercentage - startPercentage) * totalRows);
  1404. const predLen = parseInt(document.getElementById('pred-len').value);
  1405. const filePath = document.getElementById('data-file-select').value;
  1406. const diagramType = document.getElementById('diagram_type').value;
  1407. if (!filePath) throw new Error('Please select a data file first');
  1408. if (isNaN(lookback) || isNaN(predLen)) throw new Error('Invalid parameters');
  1409. // 获取接口
  1410. const response = await fetch('/api/generate-chart', {
  1411. method: 'POST',
  1412. headers: { 'Content-Type': 'application/json' },
  1413. body: JSON.stringify({
  1414. file_path: filePath,
  1415. lookback: lookback,
  1416. pred_len: predLen,
  1417. diagram_type: diagramType,
  1418. historical_start_idx: historicalStartIdx
  1419. })
  1420. });
  1421. const result = await response.json();
  1422. if (!result.success) throw new Error(result.error || 'Failed to generate chart');
  1423. // 渲染图标
  1424. const chartContainer = document.getElementById('indicator-chart');
  1425. if (chartContainer) {
  1426. if (chartContainer.data) Plotly.purge(chartContainer);
  1427. Plotly.newPlot(
  1428. chartContainer,
  1429. result.chart.data,
  1430. result.chart.layout,
  1431. { responsive: true }
  1432. );
  1433. } else {
  1434. throw new Error('Indicator chart container not found');
  1435. }
  1436. document.getElementById('data-presentation').style.display = 'block';
  1437. if (result.table_data) {
  1438. fillDataTable(result.table_data);
  1439. } else {
  1440. console.warn('No table data returned from server');
  1441. fillDataTable([]);
  1442. }
  1443. showIndicatorStatus('success', `chart (${diagramType}) generated successfully`);
  1444. } catch (error) {
  1445. console.error('Chart generation error:', error);
  1446. showIndicatorStatus('error', `Chart generation failed: ${error.message}`);
  1447. } finally {
  1448. indicatorLoading.classList.remove('show');
  1449. generateBtn.disabled = false;
  1450. }
  1451. }
  1452. // 数据表格
  1453. function fillDataTable(data) {
  1454. const tbody = document.getElementById('data-tbody');
  1455. tbody.innerHTML = '';
  1456. if (!data || data.length === 0) {
  1457. const emptyRow = document.createElement('tr');
  1458. emptyRow.innerHTML = '<td colspan = "7" style = "text-align:center">暂无数据</td>';
  1459. tbody.appendChild(emptyRow);
  1460. return;
  1461. }
  1462. data.forEach(item => {
  1463. const row = document.createElement('tr');
  1464. row.innerHTML = `
  1465. <td>${new Date(item.timestamps).toLocaleString()}</td>
  1466. <td>${item.open.toFixed(4)}</td>
  1467. <td>${item.high.toFixed(4)}</td>
  1468. <td>${item.low.toFixed(4)}</td>
  1469. <td>${item.close.toFixed(4)}</td>
  1470. <td>${item.volume ? item.volume.toLocaleString() : '-'}</td>
  1471. <td>${item.amount ? item.amount.toFixed(2) : '-'}</td>
  1472. `;
  1473. tbody.appendChild(row);
  1474. });
  1475. }
  1476. // Start prediction
  1477. async function startPrediction() {
  1478. if (!currentDataFile) {
  1479. showStatus('error', 'Please load data file first');
  1480. return;
  1481. }
  1482. if (!modelLoaded) {
  1483. showStatus('error', 'Please load model first');
  1484. return;
  1485. }
  1486. try {
  1487. showLoading(true);
  1488. document.getElementById('predict-btn').disabled = true;
  1489. const lookback = parseInt(document.getElementById('lookback').value);
  1490. const predLen = parseInt(document.getElementById('pred-len').value);
  1491. // Get selected time range from time window slider
  1492. const startHandle = document.getElementById('start-handle');
  1493. const startPercentage = parseFloat(startHandle.style.left) / 100;
  1494. if (!sliderData) {
  1495. showStatus('error', 'Time window slider not initialized');
  1496. return;
  1497. }
  1498. // Calculate selected time range
  1499. const totalTime = sliderData.endDate.getTime() - sliderData.startDate.getTime();
  1500. const startTime = sliderData.startDate.getTime() + (totalTime * startPercentage);
  1501. const startDate = new Date(startTime);
  1502. // Get prediction quality parameters
  1503. const temperature = parseFloat(document.getElementById('temperature').value);
  1504. const topP = parseFloat(document.getElementById('top-p').value);
  1505. const sampleCount = parseInt(document.getElementById('sample-count').value);
  1506. let predictionParams = {
  1507. file_path: currentDataFile,
  1508. lookback: lookback,
  1509. pred_len: predLen,
  1510. start_date: startDate.toISOString().slice(0, 16), // Format as YYYY-MM-DDTHH:MM
  1511. temperature: temperature,
  1512. top_p: topP,
  1513. sample_count: sampleCount
  1514. };
  1515. console.log('🚀 Starting prediction, parameters:', predictionParams);
  1516. const response = await axios.post('/api/predict', predictionParams);
  1517. console.log('📊 Prediction response received:', response.data);
  1518. // 添加更详细的响应数据检查
  1519. console.log('🔍 Response data check:');
  1520. console.log('- success:', response.data.success);
  1521. console.log('- has chart:', !!response.data.chart);
  1522. console.log('- chart length:', response.data.chart ? response.data.chart.length : 0);
  1523. console.log('- prediction results count:', response.data.prediction_results ? response.data.prediction_results.length : 0);
  1524. console.log('- actual data count:', response.data.actual_data ? response.data.actual_data.length : 0);
  1525. if (response.data.success) {
  1526. // Display prediction results
  1527. displayPredictionResult(response.data);
  1528. showStatus('success', response.data.message);
  1529. } else {
  1530. showStatus('error', response.data.error);
  1531. }
  1532. } catch (error) {
  1533. console.error('❌ Prediction failed:', error);
  1534. showStatus('error', `Prediction failed: ${error.response?.data?.error || error.message}`);
  1535. } finally {
  1536. showLoading(false);
  1537. document.getElementById('predict-btn').disabled = false;
  1538. }
  1539. }
  1540. // // Display prediction results
  1541. // {#function displayPredictionResult(result) {#}
  1542. // {# // Display chart#}
  1543. // {# const chartData = JSON.parse(result.chart);#}
  1544. // {# Plotly.newPlot('chart', chartData.data, chartData.layout);#}
  1545. // {##}
  1546. // {# // Display comparison analysis (if actual data exists)#}
  1547. // {# if (result.has_comparison) {#}
  1548. // {# displayComparisonAnalysis(result);#}
  1549. // {# } else {#}
  1550. // {# document.getElementById('comparison-section').style.display = 'none';#}
  1551. // {# }#}
  1552. // {#}#}
  1553. function displayPredictionResult(result) {
  1554. console.log('🔍 DEBUG - displayPredictionResult called');
  1555. console.log('Result keys:', Object.keys(result));
  1556. console.log('Has chart:', !!result.chart);
  1557. console.log('Chart type:', typeof result.chart);
  1558. console.log('Chart length:', result.chart ? result.chart.length : 0);
  1559. console.log('Has comparison:', result.has_comparison);
  1560. console.log('Actual data length:', result.actual_data ? result.actual_data.length : 0);
  1561. try {
  1562. // Parse and display chart
  1563. if (result.chart) {
  1564. console.log('📊 Parsing chart data...');
  1565. const chartData = JSON.parse(result.chart);
  1566. console.log('📈 Chart data parsed successfully:', chartData);
  1567. // Clear previous chart
  1568. const chartDiv = document.getElementById('chart');
  1569. chartDiv.innerHTML = '';
  1570. // Create new chart with error handling
  1571. Plotly.newPlot('chart', chartData.data, chartData.layout, {
  1572. responsive: true
  1573. }).then(function () {
  1574. console.log('✅ Chart rendered successfully');
  1575. // 确保图表容器可见
  1576. chartDiv.style.display = 'block';
  1577. }).catch(function (error) {
  1578. console.error('❌ Chart rendering failed:', error);
  1579. showStatus('error', `图表渲染失败: ${error.message}`);
  1580. // 显示错误信息
  1581. chartDiv.innerHTML = `
  1582. <div style="text-align: center; padding: 50px; color: #666;">
  1583. <h3>图表加载失败</h3>
  1584. <p>错误信息: ${error.message}</p>
  1585. <p>请检查控制台获取详细信息</p>
  1586. </div>
  1587. `;
  1588. });
  1589. } else {
  1590. console.error('❌ No chart data in response');
  1591. showStatus('error', '服务器返回的图表数据为空');
  1592. }
  1593. // Display comparison analysis (if actual data exists)
  1594. if (result.has_comparison && result.actual_data && result.actual_data.length > 0) {
  1595. console.log('📊 Displaying comparison analysis');
  1596. displayComparisonAnalysis(result);
  1597. } else {
  1598. console.log('ℹ️ No comparison data available');
  1599. document.getElementById('comparison-section').style.display = 'none';
  1600. }
  1601. } catch (error) {
  1602. console.error('❌ Error displaying prediction result:', error);
  1603. showStatus('error', `结果显示失败: ${error.message}`);
  1604. // 显示错误信息在图表区域
  1605. const chartDiv = document.getElementById('chart');
  1606. chartDiv.innerHTML = `
  1607. <div style="text-align: center; padding: 50px; color: #666;">
  1608. <h3>数据处理失败</h3>
  1609. <p>错误信息: ${error.message}</p>
  1610. <p>请检查数据格式是否正确</p>
  1611. </div>
  1612. `;
  1613. }
  1614. }
  1615. // Display comparison analysis
  1616. function displayComparisonAnalysis(result) {
  1617. document.getElementById('comparison-section').style.display = 'block';
  1618. // Update comparison information
  1619. document.getElementById('prediction-type').textContent = result.prediction_type;
  1620. document.getElementById('comparison-data').textContent = `${result.actual_data.length} actual data points`;
  1621. // Calculate error statistics
  1622. const errorStats = getPredictionQuality(result.prediction_results, result.actual_data);
  1623. // Display error statistics
  1624. document.getElementById('mae').textContent = errorStats.mae.toFixed(4);
  1625. document.getElementById('rmse').textContent = errorStats.rmse.toFixed(4);
  1626. document.getElementById('mape').textContent = errorStats.mape.toFixed(2);
  1627. // Fill comparison table
  1628. fillComparisonTable(result.prediction_results, result.actual_data);
  1629. }
  1630. // Calculate prediction quality metrics
  1631. function getPredictionQuality(predictions, actuals) {
  1632. if (!predictions || !actuals || predictions.length === 0 || actuals.length === 0) {
  1633. return { mae: 0, rmse: 0, mape: 0 };
  1634. }
  1635. const minLen = Math.min(predictions.length, actuals.length);
  1636. let mae = 0, rmse = 0, mape = 0;
  1637. for (let i = 0; i < minLen; i++) {
  1638. const pred = predictions[i];
  1639. const act = actuals[i];
  1640. // Use closing price to calculate errors
  1641. const error = Math.abs(pred.close - act.close);
  1642. const percentError = (error / act.close) * 100;
  1643. mae += error;
  1644. rmse += error * error;
  1645. mape += percentError;
  1646. }
  1647. mae /= minLen;
  1648. rmse = Math.sqrt(rmse / minLen);
  1649. mape /= minLen;
  1650. return { mae, rmse, mape };
  1651. }
  1652. // Fill comparison table
  1653. function fillComparisonTable(predictions, actuals) {
  1654. const tbody = document.getElementById('comparison-tbody');
  1655. tbody.innerHTML = '';
  1656. const minLen = Math.min(predictions.length, actuals.length);
  1657. for (let i = 0; i < minLen; i++) {
  1658. const pred = predictions[i];
  1659. const act = actuals[i];
  1660. const row = document.createElement('tr');
  1661. row.innerHTML = `
  1662. <td>${new Date(pred.timestamp).toLocaleString()}</td>
  1663. <td>${act.open.toFixed(4)}</td>
  1664. <td>${pred.open.toFixed(4)}</td>
  1665. <td>${act.high.toFixed(4)}</td>
  1666. <td>${pred.high.toFixed(4)}</td>
  1667. <td>${act.low.toFixed(4)}</td>
  1668. <td>${pred.low.toFixed(4)}</td>
  1669. <td>${act.close.toFixed(4)}</td>
  1670. <td>${pred.close.toFixed(4)}</td>
  1671. `;
  1672. tbody.appendChild(row);
  1673. }
  1674. }
  1675. // Set up event listeners
  1676. function setupEventListeners() {
  1677. // Load model button
  1678. document.getElementById('load-model-btn').addEventListener('click', loadModel);
  1679. // Load data button
  1680. document.getElementById('load-data-btn').addEventListener('click', loadData);
  1681. // Prediction button
  1682. document.getElementById('predict-btn').addEventListener('click', startPrediction);
  1683. // Generate chart button
  1684. document.addEventListener('DOMContentLoaded', function(){
  1685. // Prediction quality parameter sliders
  1686. document.getElementById('temperature').addEventListener('input', function() {
  1687. document.getElementById('temperature-value').textContent = this.value;
  1688. });
  1689. document.getElementById('top-p').addEventListener('input', function() {
  1690. document.getElementById('top-p-value').textContent = this.value;
  1691. });
  1692. // Update slider when lookback window size changes
  1693. document.getElementById('lookback').addEventListener('input', updateSliderFromInputs);
  1694. document.getElementById('pred-len').addEventListener('input', updateSliderFromInputs);
  1695. const chartButton = document.getElementById('load-chart-btn');
  1696. if (chartButton) {
  1697. chartButton.addEventListener('click', generatechart);
  1698. console.log('Chart button event listener bound');
  1699. } else {
  1700. console.error('load-chart-btn element not found');
  1701. }
  1702. });
  1703. }
  1704. // Display status information
  1705. function showStatus(type, message) {
  1706. const statusDiv = document.getElementById('model-status');
  1707. statusDiv.className = `status ${type}`;
  1708. statusDiv.textContent = message;
  1709. statusDiv.style.display = 'block';
  1710. // Auto-hide
  1711. setTimeout(() => {
  1712. statusDiv.style.display = 'none';
  1713. }, 5000);
  1714. }
  1715. function showIndicatorStatus(type, message) {
  1716. const statusDiv = document.getElementById('indicator-status');
  1717. statusDiv.className = `indicator-status status ${type}`;
  1718. statusDiv.textContent = message;
  1719. statusDiv.style.display = 'block';
  1720. // Auto-hide after 5 seconds
  1721. setTimeout(() => {
  1722. statusDiv.style.display = 'none';
  1723. }, 5000);
  1724. }
  1725. // Show/hide loading status
  1726. function showLoading(show) {
  1727. const loadingDiv = document.getElementById('loading');
  1728. if (show) {
  1729. loadingDiv.classList.add('show');
  1730. } else {
  1731. loadingDiv.classList.remove('show');
  1732. }
  1733. }
  1734. </script>
  1735. </body>
  1736. </html>