index_plotly_sol_spot_only.html 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  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>{{ target_asset }}/SOL 价格监控 (Plotly - 现货Only)</title>
  7. <script src='https://cdn.plot.ly/plotly-latest.min.js'></script>
  8. <style>
  9. body { font-family: Arial, sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
  10. .container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); margin-bottom: 20px; }
  11. h1, h2 { text-align: center; color: #333; margin-top:10px; margin-bottom:15px; }
  12. table { width: 100%; border-collapse: collapse; margin-top: 10px; margin-bottom: 15px; }
  13. th, td { border: 1px solid #ddd; padding: 8px; text-align: left; font-size:0.85em; word-break: break-all;}
  14. th { background-color: #e9e9e9; white-space: nowrap; }
  15. .price-up { color: green; }
  16. .price-down { color: red; }
  17. .error-message { color: #c00; font-style: italic;}
  18. .status-cell { min-width: 100px; }
  19. .timestamp { font-size: 0.9em; color: #666; text-align: right; margin-top: 10px; }
  20. .chart-container { width: 95%; height: 400px; margin: 20px auto; } /* 主图默认高度 */
  21. .diff-chart-container { height: 280px; } /* 副图特定高度 */
  22. .controls-container { text-align: center; margin-bottom: 15px; margin-top: 5px; }
  23. .control-button { background-color: #007bff; color: white; border: none; padding: 8px 15px; font-size: 14px; border-radius: 5px; cursor: pointer; margin:0 5px; }
  24. .control-button:hover { background-color: #0056b3; }
  25. .pause-button-active { background-color: #ffc107; color: #333; }
  26. .platform-name {font-weight: bold;}
  27. #main-title { font-size: 1.8em; }
  28. h2 { font-size: 1.3em; }
  29. .status-line { text-align: center; margin-top: 10px; font-size:0.8em; font-style: italic; color: grey; }
  30. </style>
  31. </head>
  32. <body>
  33. <div class="container">
  34. <h1 id="main-title">{{ target_asset }}/SOL 价格监控 (现货 vs OO)</h1>
  35. <div class="controls-container">
  36. <button id="pause-resume-button" class="control-button">暂停刷新</button>
  37. </div>
  38. <table>
  39. <thead> <tr><th>平台</th><th>价格 (SOL)</th><th class="status-cell">状态/错误</th></tr> </thead>
  40. <tbody>
  41. <tr><td class="platform-name">OpenOcean ({{ target_asset }}/SOL)</td><td id="oo-rfc-sol-price">加载中...</td><td id="oo-status" class="status-cell"></td></tr>
  42. <tr><td class="platform-name" id="gate-spot-label">Gate.io 现货 ({{ spot_pair_usdt }} → {{ target_asset }}/SOL)</td><td id="gate-spot-rfc-sol-price">加载中...</td><td id="gate-spot-status" class="status-cell"></td></tr>
  43. <tr><td class="platform-name">参考汇率 (Binance)</td><td id="sol-usdt-price-binance" title="{{ sol_usdt_pair_binance }}">加载中... (USDT)</td><td id="binance-sol-status" class="status-cell"></td></tr>
  44. </tbody>
  45. </table>
  46. <h2>价差百分比 (基于 {{ target_asset }}/SOL 价格)</h2>
  47. <table>
  48. <thead><tr><th>对比</th><th>价差 (%)</th></tr></thead>
  49. <tbody><tr><td id="diff-label-oo-spot">OO vs Gate.io 现货 (转换后)</td><td id="diff-oo-vs-spot-rfc-sol">计算中...</td></tr></tbody>
  50. </table>
  51. <div class="timestamp">最后更新: <span id="last-updated">N/A</span></div>
  52. </div>
  53. <div class="container">
  54. <h2 id="price-chart-title">{{ target_asset }}/SOL 价格历史曲线</h2>
  55. <div id='priceHistoryChartPlotly' class="chart-container"></div>
  56. <div id="price-chart-status" class="status-line">加载价格图表...</div>
  57. </div>
  58. <div class="container">
  59. <h2 id="diff-chart-title">{{ target_asset }}/SOL 价差百分比历史曲线 (OO vs Spot)</h2>
  60. <div id='diffPercentageChartPlotly' class="chart-container diff-chart-container"></div> <!-- Applied diff-chart-container class -->
  61. <div id="diff-chart-status" class="status-line">加载价差图表...</div>
  62. </div>
  63. <script type='text/javascript'>
  64. const priceChartDiv = document.getElementById('priceHistoryChartPlotly');
  65. const diffChartDiv = document.getElementById('diffPercentageChartPlotly');
  66. const priceChartStatusDiv = document.getElementById('price-chart-status');
  67. const diffChartStatusDiv = document.getElementById('diff-chart-status');
  68. const pauseResumeButton = document.getElementById('pause-resume-button');
  69. const refreshIntervalMs = {{ refresh_interval_ms }};
  70. let dataUpdateIntervalID = null;
  71. let isPaused = false;
  72. let pricePlotInitialized = false;
  73. let diffPlotInitialized = false;
  74. let isSyncingLayout = false;
  75. function formatPriceForTable(priceStr, precision = 8) {
  76. if (priceStr === null || priceStr === undefined || String(priceStr).toLowerCase() === "n/a") return "N/A";
  77. const price = parseFloat(priceStr); if (isNaN(price)) return "N/A";
  78. if (price === 0) return (0).toFixed(precision);
  79. if (Math.abs(price) < 1e-9 && price !== 0) return price.toExponential(3);
  80. return price.toFixed(precision);
  81. }
  82. function formatPercentageForTable(percStr) {
  83. if (percStr === null || percStr === undefined || String(percStr).toLowerCase() === "n/a") return "N/A";
  84. if (String(percStr).includes('%')) return percStr;
  85. const perc = parseFloat(percStr); if (isNaN(perc)) return "N/A";
  86. return `${perc > 0 ? '+' : ''}${perc.toFixed(4)}%`;
  87. }
  88. function syncPlotlyXAxes(sourceDiv, targetDiv, eventData) {
  89. if (isSyncingLayout) return; isSyncingLayout = true;
  90. const update = {}; let newXRange = null;
  91. if (eventData && eventData['xaxis.autorange'] === true) { update['xaxis.autorange'] = true; }
  92. else if (eventData && eventData['xaxis.range[0]'] !== undefined && eventData['xaxis.range[1]'] !== undefined) {
  93. newXRange = [eventData['xaxis.range[0]'], eventData['xaxis.range[1]']];
  94. update['xaxis.range'] = newXRange; update['xaxis.autorange'] = false;
  95. } else {
  96. if (sourceDiv.layout && sourceDiv.layout.xaxis) {
  97. if (sourceDiv.layout.xaxis.autorange) { update['xaxis.autorange'] = true; }
  98. else if (sourceDiv.layout.xaxis.range) { newXRange = sourceDiv.layout.xaxis.range; update['xaxis.range'] = newXRange; update['xaxis.autorange'] = false; }
  99. else { isSyncingLayout = false; return; }
  100. } else { isSyncingLayout = false; return; }
  101. }
  102. Plotly.relayout(targetDiv, update).then(() => { isSyncingLayout = false; }).catch(e => { console.error("Error syncing layout:", e); isSyncingLayout = false; });
  103. }
  104. async function updateTableData() {
  105. if (isPaused) return;
  106. try {
  107. const response = await fetch('/table-data'); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
  108. const data = await response.json();
  109. document.getElementById('oo-rfc-sol-price').textContent = formatPriceForTable(data.oo_rfc_sol_price);
  110. document.getElementById('gate-spot-rfc-sol-price').textContent = formatPriceForTable(data.gate_spot_rfc_sol_price);
  111. document.getElementById('sol-usdt-price-binance').textContent = formatPriceForTable(data.sol_usdt_price_binance, 4);
  112. document.getElementById('oo-status').textContent = data.oo_error || '正常';
  113. document.getElementById('gate-spot-status').textContent = data.gate_spot_error || '正常';
  114. document.getElementById('binance-sol-status').textContent = data.binance_sol_error || '正常';
  115. document.getElementById('oo-status').className = data.oo_error ? 'status-cell error-message' : 'status-cell';
  116. document.getElementById('gate-spot-status').className = data.gate_spot_error ? 'status-cell error-message' : 'status-cell';
  117. document.getElementById('binance-sol-status').className = data.binance_sol_error ? 'status-cell error-message' : 'status-cell';
  118. const diffOOSpotEl = document.getElementById('diff-oo-vs-spot-rfc-sol');
  119. diffOOSpotEl.textContent = formatPercentageForTable(data.diff_oo_vs_spot_rfc_sol_percentage);
  120. const valStr = diffOOSpotEl.textContent.replace('%','').replace('+',''); const val = parseFloat(valStr);
  121. if (!isNaN(val)) { diffOOSpotEl.className = val > 0 ? 'price-up' : (val < 0 ? 'price-down' : ''); } else { diffOOSpotEl.className = ''; }
  122. document.getElementById('last-updated').textContent = data.last_updated || "N/A";
  123. } catch (error) { console.error('Error fetching table data:', error); }
  124. }
  125. async function updatePlotlyCharts() {
  126. if(isPaused && (pricePlotInitialized || diffPlotInitialized) ) return;
  127. try {
  128. const response = await fetch('/plotly-chart-data'); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
  129. const chartDataResponse = await response.json();
  130. const priceChartConfig = chartDataResponse.price_chart; const diffChartConfig = chartDataResponse.diff_chart;
  131. const currentPriceLayout = priceChartDiv.layout; const currentDiffLayout = diffChartDiv.layout;
  132. if (priceChartConfig && priceChartConfig.data && priceChartConfig.layout) {
  133. if (pricePlotInitialized && !isPaused && currentPriceLayout && currentPriceLayout.xaxis && !currentPriceLayout.xaxis.autorange && currentPriceLayout.xaxis.range) {
  134. priceChartConfig.layout.xaxis.range = currentPriceLayout.xaxis.range; priceChartConfig.layout.xaxis.autorange = false;
  135. }
  136. Plotly.react(priceChartDiv, priceChartConfig.data, priceChartConfig.layout, {responsive: true});
  137. if (!pricePlotInitialized) { pricePlotInitialized = true; priceChartDiv.on('plotly_relayout', (eventData) => { syncPlotlyXAxes(priceChartDiv, diffChartDiv, eventData); }); }
  138. priceChartStatusDiv.textContent = `价格图表更新于: ${new Date().toLocaleTimeString()}`;
  139. } else { priceChartStatusDiv.textContent = "错误: 价格图表数据无效。"; }
  140. if (diffChartConfig && diffChartConfig.data && diffChartConfig.layout) {
  141. if (diffPlotInitialized && !isPaused && currentDiffLayout && currentDiffLayout.xaxis && !currentDiffLayout.xaxis.autorange && currentDiffLayout.xaxis.range) {
  142. diffChartConfig.layout.xaxis.range = currentDiffLayout.xaxis.range; diffChartConfig.layout.xaxis.autorange = false;
  143. }
  144. Plotly.react(diffChartDiv, diffChartConfig.data, diffChartConfig.layout, {responsive: true});
  145. if (!diffPlotInitialized) { diffPlotInitialized = true; diffChartDiv.on('plotly_relayout', (eventData) => { syncPlotlyXAxes(diffChartDiv, priceChartDiv, eventData); }); }
  146. diffChartStatusDiv.textContent = `价差图表更新于: ${new Date().toLocaleTimeString()}`;
  147. } else { diffChartStatusDiv.textContent = "错误: 价差图表数据无效。"; }
  148. } catch (error) { console.error('Error fetching or plotting Plotly data:', error); priceChartStatusDiv.textContent = `图表更新错误: ${error.message}`; diffChartStatusDiv.textContent = `图表更新错误: ${error.message}`; }
  149. }
  150. function togglePauseResume() {
  151. isPaused = !isPaused;
  152. if (isPaused) { if (dataUpdateIntervalID) clearInterval(dataUpdateIntervalID); dataUpdateIntervalID = null; pauseResumeButton.textContent = '继续刷新'; pauseResumeButton.classList.add('pause-button-active'); }
  153. else { pauseResumeButton.textContent = '暂停刷新'; pauseResumeButton.classList.remove('pause-button-active'); updateTableData(); updatePlotlyCharts(); dataUpdateIntervalID = setInterval(() => { updateTableData(); updatePlotlyCharts(); }, refreshIntervalMs); }
  154. }
  155. pauseResumeButton.addEventListener('click', togglePauseResume);
  156. updateTableData(); updatePlotlyCharts();
  157. if (!isPaused) { dataUpdateIntervalID = setInterval(() => { updateTableData(); updatePlotlyCharts(); }, refreshIntervalMs); }
  158. </script>
  159. </body>
  160. </html>