index_plotly.html 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  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>多平台价格与价差监控 (Plotly)</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: 450px; margin: 20px auto; } /* Plotly chart div */
  21. .controls-container { text-align: center; margin-bottom: 15px; margin-top: 5px; }
  22. .control-button { background-color: #007bff; color: white; border: none; padding: 8px 15px; font-size: 14px; border-radius: 5px; cursor: pointer; margin:0 5px; }
  23. .control-button:hover { background-color: #0056b3; }
  24. .pause-button-active { background-color: #ffc107; color: #333; }
  25. .platform-name {font-weight: bold;}
  26. #main-title { font-size: 1.8em; }
  27. h2 { font-size: 1.3em; }
  28. .status-line { text-align: center; margin-top: 10px; font-style: italic; color: grey; }
  29. </style>
  30. </head>
  31. <body>
  32. <div class="container">
  33. <h1 id="main-title">{{ target_asset }}/USDT 多平台价格监控</h1>
  34. <div class="controls-container">
  35. <button id="pause-resume-button" class="control-button">暂停刷新</button>
  36. </div>
  37. <table>
  38. <thead>
  39. <tr>
  40. <th>平台</th>
  41. <th>价格 (USDT)</th>
  42. <th class="status-cell">状态/错误</th>
  43. </tr>
  44. </thead>
  45. <tbody>
  46. <tr>
  47. <td class="platform-name">OpenOcean (BSC)</td>
  48. <td id="oo-price">加载中...</td>
  49. <td id="oo-status" class="status-cell"></td>
  50. </tr>
  51. <tr>
  52. <td class="platform-name" id="gate-spot-label">Gate.io 现货 ({{ spot_pair }})</td>
  53. <td id="gate-spot-price">加载中...</td>
  54. <td id="gate-spot-status" class="status-cell"></td>
  55. </tr>
  56. <tr>
  57. <td class="platform-name" id="gate-futures-label">Gate.io 期货 ({{ futures_contract }})</td>
  58. <td id="gate-futures-price">加载中...</td>
  59. <td id="gate-futures-status" class="status-cell"></td>
  60. </tr>
  61. </tbody>
  62. </table>
  63. <h2>价差百分比</h2>
  64. <table>
  65. <thead>
  66. <tr>
  67. <th>对比</th>
  68. <th>价差 (%)</th>
  69. </tr>
  70. </thead>
  71. <tbody>
  72. <tr>
  73. <td id="diff-label-oo-spot">OO vs Gate.io 现货 ({{ spot_pair }})</td>
  74. <td id="diff-oo-vs-spot">计算中...</td>
  75. </tr>
  76. <tr>
  77. <td id="diff-label-oo-futures">OO vs Gate.io 期货 ({{ futures_contract }})</td>
  78. <td id="diff-oo-vs-futures">计算中...</td>
  79. </tr>
  80. <tr>
  81. <td id="diff-label-spot-futures">Gate.io 现货 ({{ spot_pair }}) vs 期货 ({{ futures_contract }})</td>
  82. <td id="diff-spot-vs-futures">计算中...</td>
  83. </tr>
  84. </tbody>
  85. </table>
  86. <div class="timestamp">最后更新: <span id="last-updated">N/A</span></div>
  87. </div>
  88. <div class="container">
  89. <h2 id="price-chart-title">价格历史曲线</h2>
  90. <div id='priceHistoryChartPlotly' class="chart-container"></div>
  91. <div id="price-chart-status" class="status-line">加载价格图表...</div>
  92. </div>
  93. <div class="container">
  94. <h2 id="diff-chart-title">价差百分比历史曲线</h2>
  95. <div id='diffPercentageChartPlotly' class="chart-container"></div>
  96. <div id="diff-chart-status" class="status-line">加载价差图表...</div>
  97. </div>
  98. <script type='text/javascript'>
  99. const priceChartDiv = document.getElementById('priceHistoryChartPlotly');
  100. const diffChartDiv = document.getElementById('diffPercentageChartPlotly');
  101. const priceChartStatusDiv = document.getElementById('price-chart-status');
  102. const diffChartStatusDiv = document.getElementById('diff-chart-status');
  103. const pauseResumeButton = document.getElementById('pause-resume-button');
  104. const refreshIntervalMs = {{ refresh_interval_ms }};
  105. let dataUpdateIntervalID = null;
  106. let isPaused = false;
  107. let pricePlotInitialized = false;
  108. let diffPlotInitialized = false;
  109. // --- Helper function to format price (same as your Chart.js version) ---
  110. function formatPriceForTable(priceStr) {
  111. if (priceStr === null || priceStr === undefined || priceStr === "N/A" || String(priceStr).toLowerCase() === "n/a") return "N/A";
  112. const price = parseFloat(priceStr);
  113. if (isNaN(price)) return "N/A";
  114. if (price === 0) return "0.00000000";
  115. if (Math.abs(price) < 0.0000001 && price !== 0) return price.toExponential(3);
  116. if (Math.abs(price) < 0.001) return price.toFixed(8);
  117. if (Math.abs(price) < 1) return price.toFixed(6);
  118. return price.toFixed(4);
  119. }
  120. function formatPercentageForTable(percStr) {
  121. if (percStr === null || percStr === undefined || percStr === "N/A" || String(percStr).toLowerCase() === "n/a") return "N/A";
  122. if (String(percStr).includes('%')) return percStr; // Already formatted
  123. const perc = parseFloat(percStr);
  124. if (isNaN(perc)) return "N/A";
  125. return `${perc > 0 ? '+' : ''}${perc.toFixed(4)}%`;
  126. }
  127. async function updateTableData() {
  128. if (isPaused) return; // Don't update table if paused (charts are also paused via their own update)
  129. try {
  130. const response = await fetch('/table-data');
  131. if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
  132. const data = await response.json();
  133. document.getElementById('oo-price').textContent = formatPriceForTable(data.oo_price);
  134. document.getElementById('gate-spot-price').textContent = formatPriceForTable(data.gate_spot_price);
  135. document.getElementById('gate-futures-price').textContent = formatPriceForTable(data.gate_futures_price);
  136. document.getElementById('oo-status').textContent = data.oo_error || '正常';
  137. document.getElementById('gate-spot-status').textContent = data.gate_spot_error || '正常';
  138. document.getElementById('gate-futures-status').textContent = data.gate_futures_error || '正常';
  139. // Update error classes
  140. document.getElementById('oo-status').className = data.oo_error ? 'status-cell error-message' : 'status-cell';
  141. document.getElementById('gate-spot-status').className = data.gate_spot_error ? 'status-cell error-message' : 'status-cell';
  142. document.getElementById('gate-futures-status').className = data.gate_futures_error ? 'status-cell error-message' : 'status-cell';
  143. const diffOOSpotEl = document.getElementById('diff-oo-vs-spot');
  144. const diffOOFuturesEl = document.getElementById('diff-oo-vs-futures');
  145. const diffSpotFuturesEl = document.getElementById('diff-spot-vs-futures');
  146. diffOOSpotEl.textContent = formatPercentageForTable(data.diff_oo_vs_spot_percentage);
  147. diffOOFuturesEl.textContent = formatPercentageForTable(data.diff_oo_vs_futures_percentage);
  148. diffSpotFuturesEl.textContent = formatPercentageForTable(data.diff_spot_vs_futures_percentage);
  149. [diffOOSpotEl, diffOOFuturesEl, diffSpotFuturesEl].forEach(el => {
  150. const valStr = el.textContent.replace('%','').replace('+','');
  151. const val = parseFloat(valStr);
  152. if (!isNaN(val)) { el.className = val > 0 ? 'price-up' : (val < 0 ? 'price-down' : ''); }
  153. else { el.className = ''; }
  154. });
  155. document.getElementById('last-updated').textContent = data.last_updated || "N/A";
  156. } catch (error) {
  157. console.error('Error fetching table data:', error);
  158. // Optionally update status for table data errors
  159. }
  160. }
  161. async function updatePlotlyCharts() {
  162. if(isPaused && (pricePlotInitialized || diffPlotInitialized) ) return; // Don't fetch new chart data if paused and already drawn once
  163. try {
  164. const response = await fetch('/plotly-chart-data');
  165. if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
  166. const chartData = await response.json(); // This now contains { price_chart: ..., diff_chart: ... }
  167. if (chartData.price_chart && chartData.price_chart.data && chartData.price_chart.layout) {
  168. Plotly.react(priceChartDiv, chartData.price_chart.data, chartData.price_chart.layout, {responsive: true});
  169. if (!pricePlotInitialized) {
  170. console.log("Price chart initialized with Plotly.");
  171. pricePlotInitialized = true;
  172. }
  173. priceChartStatusDiv.textContent = `价格图表更新于: ${new Date().toLocaleTimeString()}`;
  174. } else {
  175. console.error("Received invalid price_chart data structure");
  176. priceChartStatusDiv.textContent = "错误: 价格图表数据无效。";
  177. }
  178. if (chartData.diff_chart && chartData.diff_chart.data && chartData.diff_chart.layout) {
  179. Plotly.react(diffChartDiv, chartData.diff_chart.data, chartData.diff_chart.layout, {responsive: true});
  180. if (!diffPlotInitialized) {
  181. console.log("Diff chart initialized with Plotly.");
  182. diffPlotInitialized = true;
  183. }
  184. diffChartStatusDiv.textContent = `价差图表更新于: ${new Date().toLocaleTimeString()}`;
  185. } else {
  186. console.error("Received invalid diff_chart data structure");
  187. diffChartStatusDiv.textContent = "错误: 价差图表数据无效。";
  188. }
  189. } catch (error) {
  190. console.error('Error fetching or plotting Plotly data:', error);
  191. priceChartStatusDiv.textContent = `图表更新错误: ${error.message}`;
  192. diffChartStatusDiv.textContent = `图表更新错误: ${error.message}`;
  193. }
  194. }
  195. function togglePauseResume() {
  196. isPaused = !isPaused;
  197. if (isPaused) {
  198. if (dataUpdateIntervalID) clearInterval(dataUpdateIntervalID);
  199. dataUpdateIntervalID = null;
  200. pauseResumeButton.textContent = '继续刷新';
  201. pauseResumeButton.classList.add('pause-button-active');
  202. console.log("Data refresh PAUSED");
  203. } else {
  204. pauseResumeButton.textContent = '暂停刷新';
  205. pauseResumeButton.classList.remove('pause-button-active');
  206. // Refresh immediately on resume
  207. updateTableData();
  208. updatePlotlyCharts();
  209. dataUpdateIntervalID = setInterval(() => {
  210. updateTableData();
  211. updatePlotlyCharts();
  212. }, refreshIntervalMs);
  213. console.log("Data refresh RESUMED");
  214. }
  215. }
  216. pauseResumeButton.addEventListener('click', togglePauseResume);
  217. // Initial load
  218. console.log("Page loaded. Initializing data and charts...");
  219. updateTableData();
  220. updatePlotlyCharts();
  221. if (!isPaused) {
  222. dataUpdateIntervalID = setInterval(() => {
  223. updateTableData();
  224. updatePlotlyCharts();
  225. }, refreshIntervalMs);
  226. }
  227. </script>
  228. </body>
  229. </html>