index_plotly_dynamic_ok.html 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  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 }} / {{ base_asset }}价格监控 (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; } .price-down { color: red; } .error-message { color: #c00; font-style: italic;}
  16. .status-cell { min-width: 100px; } .timestamp { font-size: 0.9em; color: #666; text-align: right; margin-top: 10px; }
  17. .chart-container { width: 95%; height: 400px; margin: 20px auto; }
  18. .diff-chart-container { height: 280px; }
  19. .controls-container { text-align: center; margin-bottom: 15px; margin-top: 5px; }
  20. .control-button { background-color: #007bff; color: white; border: none; padding: 8px 15px; font-size: 14px; border-radius: 5px; cursor: pointer; margin:0 5px; }
  21. .control-button:hover { background-color: #0056b3; } .pause-button-active { background-color: #ffc107; color: #333; }
  22. .platform-name {font-weight: bold;} #main-title { font-size: 1.8em; } h2 { font-size: 1.3em; }
  23. .status-line { text-align: center; margin-top: 10px; font-size:0.8em; font-style: italic; color: grey; }
  24. </style>
  25. </head>
  26. <body>
  27. <div class="container">
  28. <!-- 主标题显示 RATO/USDT (base/target) -->
  29. <h1 id="main-title">{{ target_asset }}/{{ base_asset }} 多平台价格监控</h1>
  30. <div class="controls-container"> <button id="pause-resume-button" class="control-button">暂停刷新</button> </div>
  31. <table>
  32. <thead>
  33. <!-- 表格列标题也显示 USDT (base_asset) -->
  34. <tr><th>平台</th><th>价格 ({{ base_asset }})</th><th class="status-cell">状态/错误</th></tr>
  35. </thead>
  36. <tbody>
  37. <tr> <!-- OpenOcean显示 RATO/USDT -->
  38. <td class="platform-name">OpenOcean ({{ target_asset }}/{{ base_asset }})</td>
  39. <td id="oo-price-usdt-per-target">加载中...</td><td id="oo-status" class="status-cell"></td>
  40. </tr>
  41. <tr> <!-- MEXC显示 RATO/USDT (转换后) -->
  42. <td class="platform-name" id="mexc-label">MEXC 现货 Bid1 ({{ mexc_pair_usdt }} → {{ target_asset }}/{{ base_asset }})</td>
  43. <td id="mexc-price-usdt-per-target-bid1">加载中...</td>
  44. <td id="mexc-status" class="status-cell"></td>
  45. </tr>
  46. </tbody>
  47. </table>
  48. <h2>利润 (基于 {{ target_asset }}/{{ base_asset }} 价格)</h2>
  49. <table>
  50. <thead><tr><th>对比</th><th>利润 (0.01代表1%)</th></tr></thead>
  51. <tbody>
  52. <tr><td>OpenOcean vs MEXC Bid1</td><td id="diff-oo-vs-mexc-bid1">计算中...</td></tr>
  53. </tbody>
  54. </table>
  55. <div class="timestamp">最后更新: <span id="last-updated">N/A</span></div>
  56. </div>
  57. <div class="container">
  58. <h2 id="price-chart-title">{{ target_asset }}/{{ base_asset }} 价格历史曲线</h2>
  59. <div id='priceHistoryChartPlotly' class="chart-container"></div>
  60. <div id="price-chart-status" class="status-line">加载价格图表...</div>
  61. </div>
  62. <div class="container">
  63. <h2 id="diff-chart-title">{{ target_asset }}/{{ base_asset }} 利润百分比历史曲线</h2>
  64. <div id='diffPercentageChartPlotly' class="chart-container diff-chart-container"></div>
  65. <div id="diff-chart-status" class="status-line">加载利润图表...</div>
  66. </div>
  67. <script type='text/javascript'>
  68. const priceChartDiv = document.getElementById('priceHistoryChartPlotly');
  69. const diffChartDiv = document.getElementById('diffPercentageChartPlotly');
  70. const priceChartStatusDiv = document.getElementById('price-chart-status');
  71. const diffChartStatusDiv = document.getElementById('diff-chart-status');
  72. const pauseResumeButton = document.getElementById('pause-resume-button');
  73. const refreshIntervalMs = {{ refresh_interval_ms }};
  74. let dataUpdateIntervalID = null, isPaused = false, pricePlotInitialized = false, diffPlotInitialized = false, isSyncingLayout = false;
  75. // JavaScript端的 TARGET_ASSET 和 BASE_ASSET 现在与模板一致,表示 RATO 和 USDT
  76. const TARGET_ASSET_JS = "{{ target_asset }}"; // e.g., RATO
  77. const BASE_ASSET_JS = "{{ base_asset }}"; // e.g., USDT
  78. // Updated formatPrice to handle USDT/TARGET (base/target) style formatting
  79. function formatPriceForTable(priceStr, precision = 8) {
  80. if (priceStr === null || priceStr === undefined || String(priceStr).toLowerCase() === "n/a") return "N/A";
  81. const price = parseFloat(priceStr);
  82. if (isNaN(price)) return "N/A";
  83. if (price === 0) return (0).toFixed(precision);
  84. // 对于 USDT/TARGET 这种价格,可能很小,也可能相对较大
  85. // 如果价格非常小 (例如 1 RATO = 0.00000001 USDT), 指数表示法可能更好
  86. // 如果价格相对"正常" (例如 1 RATO = 0.1 USDT), 固定小数位更好
  87. // 这里的阈值需要根据实际情况调整
  88. if (Math.abs(price) < 1e-6 && price !== 0) { // 如果1 TARGET 价值远小于 1 USDT
  89. return price.toExponential(3);
  90. }
  91. return price.toFixed(precision);
  92. }
  93. function formatPercentageForTable(percStr) {
  94. if (percStr === null || percStr === undefined || String(percStr).toLowerCase() === "n/a") return "N/A";
  95. if (String(percStr).includes('%')) return percStr;
  96. const perc = parseFloat(percStr);
  97. if (isNaN(perc)) return "N/A";
  98. return `${perc > 0 ? '+' : ''}${perc.toFixed(4)}`;
  99. }
  100. function syncPlotlyXAxes(sourceDiv, targetDiv, eventData) {
  101. if (isSyncingLayout) return;
  102. isSyncingLayout = true;
  103. const update = {};
  104. let newXRange = null;
  105. if (eventData && eventData['xaxis.autorange'] === true) {
  106. update['xaxis.autorange'] = true;
  107. } else if (eventData && eventData['xaxis.range[0]'] !== undefined && eventData['xaxis.range[1]'] !== undefined) {
  108. newXRange = [eventData['xaxis.range[0]'], eventData['xaxis.range[1]']];
  109. update['xaxis.range'] = newXRange;
  110. update['xaxis.autorange'] = false;
  111. } else {
  112. if (sourceDiv.layout && sourceDiv.layout.xaxis) {
  113. if (sourceDiv.layout.xaxis.autorange) {
  114. update['xaxis.autorange'] = true;
  115. } else if (sourceDiv.layout.xaxis.range) {
  116. newXRange = sourceDiv.layout.xaxis.range;
  117. update['xaxis.range'] = newXRange;
  118. update['xaxis.autorange'] = false;
  119. } else { isSyncingLayout = false; return; }
  120. } else { isSyncingLayout = false; return; }
  121. }
  122. Plotly.relayout(targetDiv, update).then(() => { isSyncingLayout = false; }).catch(e => { console.error("Error syncing layout:", e); isSyncingLayout = false; });
  123. }
  124. async function updateTableData() {
  125. if (isPaused) return;
  126. try {
  127. const response = await fetch('/table-data');
  128. if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
  129. const data = await response.json();
  130. document.getElementById('oo-price-usdt-per-target').textContent = formatPriceForTable(data.oo_price_usdt_per_target);
  131. document.getElementById('mexc-price-usdt-per-target-bid1').textContent = formatPriceForTable(data.mexc_price_usdt_per_target_bid1);
  132. document.getElementById('oo-status').textContent = data.oo_error || '正常';
  133. document.getElementById('mexc-status').textContent = data.mexc_error || '正常';
  134. document.getElementById('oo-status').className = data.oo_error ? 'status-cell error-message' : 'status-cell';
  135. document.getElementById('mexc-status').className = data.mexc_error ? 'status-cell error-message' : 'status-cell';
  136. const diffOOMexcBidEl = document.getElementById('diff-oo-vs-mexc-bid1');
  137. diffOOMexcBidEl.textContent = formatPercentageForTable(data.diff_oo_vs_mexc_bid1_percentage);
  138. const valStr = diffOOMexcBidEl.textContent.replace('%','').replace('+','');
  139. const val = parseFloat(valStr);
  140. if (!isNaN(val)) { diffOOMexcBidEl.className = val > 0 ? 'price-up' : (val < 0 ? 'price-down' : ''); }
  141. else { diffOOMexcBidEl.className = ''; }
  142. document.getElementById('last-updated').textContent = data.last_updated || "N/A";
  143. } catch (error) { console.error('Error fetching table data:', error); }
  144. }
  145. async function updatePlotlyCharts() {
  146. if(isPaused && (pricePlotInitialized || diffPlotInitialized) ) return;
  147. try {
  148. const response = await fetch('/plotly-chart-data');
  149. if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
  150. const chartDataResponse = await response.json();
  151. const priceChartConfig = chartDataResponse.price_chart;
  152. const diffChartConfig = chartDataResponse.diff_chart;
  153. const currentPriceLayout = priceChartDiv.layout;
  154. const currentDiffLayout = diffChartDiv.layout;
  155. if (priceChartConfig && priceChartConfig.data && priceChartConfig.layout) {
  156. if (pricePlotInitialized && !isPaused && currentPriceLayout && currentPriceLayout.xaxis && !currentPriceLayout.xaxis.autorange && currentPriceLayout.xaxis.range) {
  157. priceChartConfig.layout.xaxis.range = currentPriceLayout.xaxis.range;
  158. priceChartConfig.layout.xaxis.autorange = false;
  159. }
  160. Plotly.react(priceChartDiv, priceChartConfig.data, priceChartConfig.layout, {responsive: true});
  161. if (!pricePlotInitialized) {
  162. pricePlotInitialized = true;
  163. priceChartDiv.on('plotly_relayout', (eventData) => { syncPlotlyXAxes(priceChartDiv, diffChartDiv, eventData); });
  164. }
  165. priceChartStatusDiv.textContent = `价格图表 (${TARGET_ASSET_JS}/${BASE_ASSET_JS}) 更新于: ${new Date().toLocaleTimeString()}`;
  166. } else {
  167. priceChartStatusDiv.textContent = "错误: 价格图表数据无效。";
  168. }
  169. if (diffChartConfig && diffChartConfig.data && diffChartConfig.layout) {
  170. if (diffPlotInitialized && !isPaused && currentDiffLayout && currentDiffLayout.xaxis && !currentDiffLayout.xaxis.autorange && currentDiffLayout.xaxis.range) {
  171. diffChartConfig.layout.xaxis.range = currentDiffLayout.xaxis.range;
  172. diffChartConfig.layout.xaxis.autorange = false;
  173. }
  174. Plotly.react(diffChartDiv, diffChartConfig.data, diffChartConfig.layout, {responsive: true});
  175. if (!diffPlotInitialized) {
  176. diffPlotInitialized = true;
  177. diffChartDiv.on('plotly_relayout', (eventData) => { syncPlotlyXAxes(diffChartDiv, priceChartDiv, eventData); });
  178. }
  179. diffChartStatusDiv.textContent = `利润图表 (${TARGET_ASSET_JS}/${BASE_ASSET_JS}) 更新于: ${new Date().toLocaleTimeString()}`;
  180. } else {
  181. diffChartStatusDiv.textContent = "错误: 利润图表数据无效。";
  182. }
  183. } catch (error) {
  184. console.error('Error fetching or plotting Plotly data:', error);
  185. priceChartStatusDiv.textContent = `图表更新错误: ${error.message}`;
  186. diffChartStatusDiv.textContent = `图表更新错误: ${error.message}`;
  187. }
  188. }
  189. function togglePauseResume() {
  190. isPaused = !isPaused;
  191. if (isPaused) {
  192. if (dataUpdateIntervalID) clearInterval(dataUpdateIntervalID);
  193. dataUpdateIntervalID = null;
  194. pauseResumeButton.textContent = '继续刷新';
  195. pauseResumeButton.classList.add('pause-button-active');
  196. } else {
  197. pauseResumeButton.textContent = '暂停刷新';
  198. pauseResumeButton.classList.remove('pause-button-active');
  199. updateTableData(); // Immediately update on resume
  200. updatePlotlyCharts(); // Immediately update on resume
  201. dataUpdateIntervalID = setInterval(() => { updateTableData(); updatePlotlyCharts(); }, refreshIntervalMs);
  202. }
  203. }
  204. pauseResumeButton.addEventListener('click', togglePauseResume);
  205. updateTableData();
  206. updatePlotlyCharts();
  207. if (!isPaused) {
  208. dataUpdateIntervalID = setInterval(() => { updateTableData(); updatePlotlyCharts(); }, refreshIntervalMs);
  209. }
  210. </script>
  211. </body>
  212. </html>