| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>{{ target_asset }}/{{ base_asset }} 价格监控 (Plotly)</title>
- <script src='https://cdn.plot.ly/plotly-latest.min.js'></script>
- <style>
- /* CSS与之前一致 */
- body { font-family: Arial, sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
- .container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); margin-bottom: 20px; }
- h1, h2 { text-align: center; color: #333; margin-top:10px; margin-bottom:15px; }
- table { width: 100%; border-collapse: collapse; margin-top: 10px; margin-bottom: 15px; }
- th, td { border: 1px solid #ddd; padding: 8px; text-align: left; font-size:0.85em; word-break: break-all;}
- th { background-color: #e9e9e9; white-space: nowrap; }
- .price-up { color: green; } .price-down { color: red; } .error-message { color: #c00; font-style: italic;}
- .status-cell { min-width: 100px; } .timestamp { font-size: 0.9em; color: #666; text-align: right; margin-top: 10px; }
- .chart-container { width: 95%; height: 400px; margin: 20px auto; }
- .diff-chart-container { height: 280px; }
- .controls-container { text-align: center; margin-bottom: 15px; margin-top: 5px; }
- .control-button { background-color: #007bff; color: white; border: none; padding: 8px 15px; font-size: 14px; border-radius: 5px; cursor: pointer; margin:0 5px; }
- .control-button:hover { background-color: #0056b3; } .pause-button-active { background-color: #ffc107; color: #333; }
- .platform-name {font-weight: bold;} #main-title { font-size: 1.8em; } h2 { font-size: 1.3em; }
- .status-line { text-align: center; margin-top: 10px; font-size:0.8em; font-style: italic; color: grey; }
- </style>
- </head>
- <body>
- <div class="container">
- <h1 id="main-title">{{ target_asset }}/{{ base_asset }} 多平台价格监控</h1>
- <div class="controls-container"> <button id="pause-resume-button" class="control-button">暂停刷新</button> </div>
- <table>
- <thead> <tr><th>平台</th><th>价格 ({{ base_asset }})</th><th class="status-cell">状态/错误</th></tr> </thead>
- <tbody>
- <tr><td class="platform-name">OpenOcean ({{ target_asset }}/{{ base_asset }})</td><td id="oo-target-vs-base-price">加载中...</td><td id="oo-status" class="status-cell"></td></tr>
- <tr> <!-- MEXC Row, now might show Ask1 and Bid1 or just Ask1 -->
- <td class="platform-name" id="mexc-contract-label">MEXC 合约 ({{ mexc_contract_pair_usdt }} → {{ target_asset }}/{{ base_asset }})</td>
- <td>
- Ask1: <span id="mexc-target-vs-base-price-ask1">加载中...</span><br>
- Bid1: <span id="mexc-target-vs-base-price-bid1">加载中...</span>
- </td>
- <td id="mexc-status" class="status-cell"></td>
- </tr>
- <tr><td class="platform-name">参考汇率 (Binance)</td><td id="binance-base-vs-usdt-price" title="{{ binance_bridge_pair }}">加载中... (USDT)</td><td id="binance-status" class="status-cell"></td></tr>
- </tbody>
- </table>
- <h2>价差百分比 (基于 {{ target_asset }}/{{ base_asset }} 价格)</h2>
- <table>
- <thead><tr><th>对比</th><th>价差 (%)</th></tr></thead>
- <tbody>
- <tr><td>OO vs MEXC 合约 Ask1</td><td id="diff-oo-vs-mexc-ask1">计算中...</td></tr>
- <tr><td>OO vs MEXC 合约 Bid1</td><td id="diff-oo-vs-mexc-bid1">计算中...</td></tr>
- </tbody>
- </table>
- <div class="timestamp">最后更新: <span id="last-updated">N/A</span></div>
- </div>
- <div class="container"> <h2 id="price-chart-title">{{ target_asset }}/{{ base_asset }} 价格历史曲线</h2> <div id='priceHistoryChartPlotly' class="chart-container"></div> <div id="price-chart-status" class="status-line">加载价格图表...</div> </div>
- <div class="container"> <h2 id="diff-chart-title">{{ target_asset }}/{{ base_asset }} 价差百分比历史曲线</h2> <div id='diffPercentageChartPlotly' class="chart-container diff-chart-container"></div> <div id="diff-chart-status" class="status-line">加载价差图表...</div> </div>
- <script type='text/javascript'>
- const priceChartDiv = document.getElementById('priceHistoryChartPlotly'); const diffChartDiv = document.getElementById('diffPercentageChartPlotly');
- const priceChartStatusDiv = document.getElementById('price-chart-status'); const diffChartStatusDiv = document.getElementById('diff-chart-status');
- const pauseResumeButton = document.getElementById('pause-resume-button');
- const refreshIntervalMs = {{ refresh_interval_ms }};
- let dataUpdateIntervalID = null, isPaused = false, pricePlotInitialized = false, diffPlotInitialized = false, isSyncingLayout = false;
- const TARGET_ASSET = "{{ target_asset }}"; const BASE_ASSET = "{{ base_asset }}";
- function formatPriceForTable(priceStr, precision = 8) { /* ... */ if (priceStr === null || priceStr === undefined || String(priceStr).toLowerCase() === "n/a") return "N/A"; const price = parseFloat(priceStr); if (isNaN(price)) return "N/A"; if (price === 0) return (0).toFixed(precision); if (Math.abs(price) < 1e-9 && price !== 0) return price.toExponential(3); return price.toFixed(precision); }
- function formatPercentageForTable(percStr) { /* ... */ if (percStr === null || percStr === undefined || String(percStr).toLowerCase() === "n/a") return "N/A"; if (String(percStr).includes('%')) return percStr; const perc = parseFloat(percStr); if (isNaN(perc)) return "N/A"; return `${perc > 0 ? '+' : ''}${perc.toFixed(4)}%`; }
- function syncPlotlyXAxes(sourceDiv, targetDiv, eventData) { /* ... */ if (isSyncingLayout) return; isSyncingLayout = true; const update = {}; let newXRange = null; if (eventData && eventData['xaxis.autorange'] === true) { update['xaxis.autorange'] = true; } else if (eventData && eventData['xaxis.range[0]'] !== undefined && eventData['xaxis.range[1]'] !== undefined) { newXRange = [eventData['xaxis.range[0]'], eventData['xaxis.range[1]']]; update['xaxis.range'] = newXRange; update['xaxis.autorange'] = false; } else { if (sourceDiv.layout && sourceDiv.layout.xaxis) { if (sourceDiv.layout.xaxis.autorange) { update['xaxis.autorange'] = true; } else if (sourceDiv.layout.xaxis.range) { newXRange = sourceDiv.layout.xaxis.range; update['xaxis.range'] = newXRange; update['xaxis.autorange'] = false; } else { isSyncingLayout = false; return; } } else { isSyncingLayout = false; return; } } Plotly.relayout(targetDiv, update).then(() => { isSyncingLayout = false; }).catch(e => { console.error("Error syncing layout:", e); isSyncingLayout = false; }); }
- async function updateTableData() {
- if (isPaused) return;
- try {
- const response = await fetch('/table-data'); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
- const data = await response.json();
- document.getElementById('oo-target-vs-base-price').textContent = formatPriceForTable(data.oo_target_vs_base_price);
- document.getElementById('mexc-target-vs-base-price-ask1').textContent = formatPriceForTable(data.mexc_target_vs_base_price_ask1);
- document.getElementById('mexc-target-vs-base-price-bid1').textContent = formatPriceForTable(data.mexc_target_vs_base_price_bid1);
- document.getElementById('binance-base-vs-usdt-price').textContent = formatPriceForTable(data.binance_base_vs_usdt_price, 4);
- document.getElementById('oo-status').textContent = data.oo_error || '正常';
- document.getElementById('mexc-status').textContent = data.mexc_error || '正常';
- document.getElementById('binance-status').textContent = data.binance_error || '正常';
- document.getElementById('oo-status').className = data.oo_error ? 'status-cell error-message' : 'status-cell';
- document.getElementById('mexc-status').className = data.mexc_error ? 'status-cell error-message' : 'status-cell';
- document.getElementById('binance-status').className = data.binance_error ? 'status-cell error-message' : 'status-cell';
- const diffOOMexcAskEl = document.getElementById('diff-oo-vs-mexc-ask1');
- const diffOOMexcBidEl = document.getElementById('diff-oo-vs-mexc-bid1');
- diffOOMexcAskEl.textContent = formatPercentageForTable(data.diff_oo_vs_mexc_ask1_percentage);
- diffOOMexcBidEl.textContent = formatPercentageForTable(data.diff_oo_vs_mexc_bid1_percentage);
- [diffOOMexcAskEl, diffOOMexcBidEl].forEach(el => {
- const valStr = el.textContent.replace('%','').replace('+',''); const val = parseFloat(valStr);
- if (!isNaN(val)) { el.className = val > 0 ? 'price-up' : (val < 0 ? 'price-down' : ''); } else { el.className = ''; }
- });
- document.getElementById('last-updated').textContent = data.last_updated || "N/A";
- } catch (error) { console.error('Error fetching table data:', error); }
- }
- async function updatePlotlyCharts() { /* ...与之前相同,但后端会提供新的trace... */ if(isPaused && (pricePlotInitialized || diffPlotInitialized) ) return; try { const response = await fetch('/plotly-chart-data'); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const chartDataResponse = await response.json(); const priceChartConfig = chartDataResponse.price_chart; const diffChartConfig = chartDataResponse.diff_chart; const currentPriceLayout = priceChartDiv.layout; const currentDiffLayout = diffChartDiv.layout; if (priceChartConfig && priceChartConfig.data && priceChartConfig.layout) { if (pricePlotInitialized && !isPaused && currentPriceLayout && currentPriceLayout.xaxis && !currentPriceLayout.xaxis.autorange && currentPriceLayout.xaxis.range) { priceChartConfig.layout.xaxis.range = currentPriceLayout.xaxis.range; priceChartConfig.layout.xaxis.autorange = false; } Plotly.react(priceChartDiv, priceChartConfig.data, priceChartConfig.layout, {responsive: true}); if (!pricePlotInitialized) { pricePlotInitialized = true; priceChartDiv.on('plotly_relayout', (eventData) => { syncPlotlyXAxes(priceChartDiv, diffChartDiv, eventData); }); } priceChartStatusDiv.textContent = `价格图表 (${TARGET_ASSET}/${BASE_ASSET}) 更新于: ${new Date().toLocaleTimeString()}`; } else { priceChartStatusDiv.textContent = "错误: 价格图表数据无效。"; } if (diffChartConfig && diffChartConfig.data && diffChartConfig.layout) { if (diffPlotInitialized && !isPaused && currentDiffLayout && currentDiffLayout.xaxis && !currentDiffLayout.xaxis.autorange && currentDiffLayout.xaxis.range) { diffChartConfig.layout.xaxis.range = currentDiffLayout.xaxis.range; diffChartConfig.layout.xaxis.autorange = false; } Plotly.react(diffChartDiv, diffChartConfig.data, diffChartConfig.layout, {responsive: true}); if (!diffPlotInitialized) { diffPlotInitialized = true; diffChartDiv.on('plotly_relayout', (eventData) => { syncPlotlyXAxes(diffChartDiv, priceChartDiv, eventData); }); } diffChartStatusDiv.textContent = `价差图表 (${TARGET_ASSET}/${BASE_ASSET}) 更新于: ${new Date().toLocaleTimeString()}`; } else { diffChartStatusDiv.textContent = "错误: 价差图表数据无效。"; } } catch (error) { console.error('Error fetching or plotting Plotly data:', error); priceChartStatusDiv.textContent = `图表更新错误: ${error.message}`; diffChartStatusDiv.textContent = `图表更新错误: ${error.message}`; } }
- function togglePauseResume() { /* ... */ isPaused = !isPaused; if (isPaused) { if (dataUpdateIntervalID) clearInterval(dataUpdateIntervalID); dataUpdateIntervalID = null; pauseResumeButton.textContent = '继续刷新'; pauseResumeButton.classList.add('pause-button-active'); } else { pauseResumeButton.textContent = '暂停刷新'; pauseResumeButton.classList.remove('pause-button-active'); updateTableData(); updatePlotlyCharts(); dataUpdateIntervalID = setInterval(() => { updateTableData(); updatePlotlyCharts(); }, refreshIntervalMs); } }
- pauseResumeButton.addEventListener('click', togglePauseResume); updateTableData(); updatePlotlyCharts(); if (!isPaused) { dataUpdateIntervalID = setInterval(() => { updateTableData(); updatePlotlyCharts(); }, refreshIntervalMs); }
- </script>
- </body>
- </html>
|