| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>多平台价格与价差监控</title>
- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
- <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@2.0.1/dist/chartjs-plugin-zoom.min.js"></script>
- <style>
- 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; }
- table { width: 100%; border-collapse: collapse; margin-top: 20px; margin-bottom: 20px; }
- th, td { border: 1px solid #ddd; padding: 10px; text-align: left; font-size:0.9em; } /* Smaller padding/font for more data */
- th { background-color: #e9e9e9; }
- .price-up { color: green; }
- .price-down { color: red; }
- .error-message { color: #c00; font-style: italic; font-size: 0.9em; }
- .timestamp { font-size: 0.9em; color: #666; text-align: right; margin-top: 15px; }
- .chart-container { position: relative; height: 40vh; width: 90vw; margin: auto; margin-bottom: 10px; }
- .controls-container { text-align: center; margin-bottom: 20px; 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; }
- .pause-button-active:hover { background-color: #e0a800; }
- .platform-name {font-weight: bold;}
- </style>
- </head>
- <body>
- <div class="container">
- <h1 id="main-title">价格监控</h1>
- <div class="controls-container">
- <button id="pause-resume-button" class="control-button">暂停刷新</button>
- </div>
- <table>
- <thead>
- <tr>
- <th>平台</th>
- <th>价格 (USDT)</th>
- <th>状态/错误</th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td class="platform-name">OpenOcean (BSC)</td>
- <td id="oo-price">加载中...</td>
- <td id="oo-status" class="status-cell"></td>
- </tr>
- <tr>
- <td class="platform-name" id="gate-spot-label">Gate.io 现货</td>
- <td id="gate-spot-price">加载中...</td>
- <td id="gate-spot-status" class="status-cell"></td>
- </tr>
- <tr>
- <td class="platform-name" id="gate-futures-label">Gate.io 期货</td>
- <td id="gate-futures-price">加载中...</td>
- <td id="gate-futures-status" class="status-cell"></td>
- </tr>
- </tbody>
- </table>
- <h2>价差百分比</h2>
- <table>
- <thead>
- <tr>
- <th>对比</th>
- <th>价差 (%)</th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td id="diff-label-oo-spot">OO vs Gate.io 现货</td>
- <td id="diff-oo-vs-spot">计算中...</td>
- </tr>
- <tr>
- <td id="diff-label-oo-futures">OO vs Gate.io 期货</td>
- <td id="diff-oo-vs-futures">计算中...</td>
- </tr>
- <tr>
- <td id="diff-label-spot-futures">Gate.io 现货 vs 期货</td>
- <td id="diff-spot-vs-futures">计算中...</td>
- </tr>
- </tbody>
- </table>
- <div class="timestamp">最后更新: <span id="last-updated">N/A</span></div>
- </div>
- <div class="container">
- <h2 id="price-chart-title">价格历史曲线</h2>
- <div class="chart-container">
- <canvas id="priceHistoryChart"></canvas>
- </div>
- <div class="controls-container">
- <button id="reset-price-zoom-button" class="control-button">重置缩放</button>
- </div>
- </div>
- <div class="container">
- <h2 id="diff-chart-title">价差百分比历史曲线</h2>
- <div class="chart-container">
- <canvas id="diffPercentageChart"></canvas> <!-- Changed ID -->
- </div>
- <div class="controls-container">
- <button id="reset-diff-zoom-button" class="control-button">重置缩放</button>
- </div>
- </div>
- <script>
- let priceChartInstance = null;
- let diffChartInstance = null; // For percentage differences
- let dataUpdateIntervalID = null;
- let isPaused = false;
- const REFRESH_INTERVAL_MS = 1000;
- const pauseResumeButton = document.getElementById('pause-resume-button');
- let isSyncingZoomPan = false;
- // Get initial config from Flask if available
- const initialConfig = {
- GATEIO_SPOT_PAIR: "{{ config.GATEIO_SPOT_PAIR if config else 'SPOT_USDT' }}",
- GATEIO_FUTURES_CONTRACT: "{{ config.GATEIO_FUTURES_CONTRACT if config else 'FUTURES_USDT' }}",
- TARGET_ASSET_SYMBOL: "{{ config.TARGET_ASSET_SYMBOL if config else 'ASSET' }}"
- };
- let currentGateSpotPair = initialConfig.GATEIO_SPOT_PAIR;
- let currentGateFuturesContract = initialConfig.GATEIO_FUTURES_CONTRACT;
- let currentAssetSymbol = initialConfig.TARGET_ASSET_SYMBOL;
- function formatPrice(priceStr) {
- // (formatPrice function from previous example, can be reused)
- if (priceStr === null || priceStr === undefined || priceStr === "N/A") return "N/A";
- const price = parseFloat(priceStr);
- if (isNaN(price)) return "N/A";
- if (price === 0) return "0.000000";
- if (price < 0.000001 && price > 0) return price.toExponential(4);
- if (price < 0.01) return price.toFixed(8);
- if (price < 1) return price.toFixed(6);
- return price.toFixed(4);
- }
- function formatPercentage(percStr) {
- if (percStr === null || percStr === undefined || percStr === "N/A") return "N/A";
- // Assumes percStr is like "+0.1234%" or "-5.6789%"
- if (String(percStr).includes('%')) return percStr;
- const perc = parseFloat(percStr);
- if (isNaN(perc)) return "N/A";
- return `${perc > 0 ? '+' : ''}${perc.toFixed(4)}%`;
- }
- function syncXAxes(sourceChart, targetChart) { /* (Same as before) */
- if (isSyncingZoomPan || !sourceChart || !targetChart) return;
- isSyncingZoomPan = true;
- const sourceXScale = sourceChart.scales.x;
- const targetXScale = targetChart.scales.x;
- if (sourceXScale && targetXScale) {
- targetXScale.options.min = sourceXScale.min;
- targetXScale.options.max = sourceXScale.max;
- targetChart.update('none');
- }
- requestAnimationFrame(() => { isSyncingZoomPan = false; });
- }
- function initializeChart(ctx, chartIdentifier, datasetsConfig, initialLabels, yAxisTitle, chartTitleText, isPriceChart = true) {
- return new Chart(ctx, {
- type: 'line',
- data: { labels: initialLabels, datasets: datasetsConfig },
- options: {
- responsive: true, maintainAspectRatio: false, animation: { duration: 150 },
- scales: {
- x: { title: { display: true, text: '时间' } },
- y: {
- title: { display: true, text: yAxisTitle },
- beginAtZero: !isPriceChart ? false : undefined, // For diff chart, don't begin at zero
- ticks: {
- callback: function(value) {
- return isPriceChart ? formatPrice(String(value)) : parseFloat(value).toFixed(2) + '%';
- }
- }
- }
- },
- plugins: {
- legend: { position: 'top' }, title: { display: true, text: chartTitleText },
- tooltip: {
- callbacks: {
- label: function(context) {
- let label = context.dataset.label || '';
- if (label) label += ': ';
- if (context.parsed.y !== null) {
- label += isPriceChart ? formatPrice(String(context.parsed.y)) : parseFloat(context.parsed.y).toFixed(4) + '%';
- }
- return label;
- }
- }
- },
- zoom: { /* (Zoom config same as before) */
- pan: {
- enabled: true, mode: 'x', threshold: 5,
- onPanComplete({chart}) {
- if (chartIdentifier === 'price' && diffChartInstance) syncXAxes(chart, diffChartInstance);
- else if (chartIdentifier === 'diff' && priceChartInstance) syncXAxes(chart, priceChartInstance);
- }
- },
- zoom: {
- wheel: { enabled: true, speed: 0.1 }, pinch: { enabled: true },
- drag: { enabled: true, backgroundColor: 'rgba(0,123,255,0.25)'},
- mode: 'x',
- onZoomComplete({chart}) {
- if (chartIdentifier === 'price' && diffChartInstance) syncXAxes(chart, diffChartInstance);
- else if (chartIdentifier === 'diff' && priceChartInstance) syncXAxes(chart, priceChartInstance);
- }
- }
- }
- },
- elements: { point:{ radius: 1.5 } } // Smaller points for more lines
- }
- });
- }
- function updateChartData(chartInstance, newLabels, newDatasetsDataArray) {
- // (updateChartData function largely same, ensures it updates all datasets in newDatasetsDataArray)
- if (!chartInstance) return;
- const currentXMin = chartInstance.scales.x.min;
- const currentXMax = chartInstance.scales.x.max;
- let isZoomed = (currentXMin !== undefined && currentXMax !== undefined);
- if (isZoomed && chartInstance.data.labels.length > 0) {
- isZoomed = (currentXMin !== chartInstance.data.labels[0] || currentXMax !== chartInstance.data.labels[chartInstance.data.labels.length - 1]);
- }
- chartInstance.data.labels = newLabels;
- newDatasetsDataArray.forEach((datasetData, index) => {
- if(chartInstance.data.datasets[index]) { // Check if dataset exists
- chartInstance.data.datasets[index].data = datasetData;
- }
- });
- if (!isPaused) {
- chartInstance.options.scales.x.min = undefined;
- chartInstance.options.scales.x.max = undefined;
- } else {
- if(isZoomed) {
- chartInstance.options.scales.x.min = currentXMin;
- chartInstance.options.scales.x.max = currentXMax;
- } else {
- chartInstance.options.scales.x.min = undefined;
- chartInstance.options.scales.x.max = undefined;
- }
- }
- chartInstance.update('quiet');
- }
- function updateDisplayAndCharts() {
- fetch('/data')
- .then(response => response.json())
- .then(data => {
- const current = data.current;
- // Update global config vars if they changed (though unlikely for these)
- currentGateSpotPair = current.config_gate_spot_pair || currentGateSpotPair;
- currentGateFuturesContract = current.config_gate_futures_contract || currentGateFuturesContract;
- currentAssetSymbol = current.config_target_asset_symbol || currentAssetSymbol;
- // --- Update Titles and Labels ---
- document.getElementById('main-title').textContent = `${currentAssetSymbol}/USDT 多平台价格监控`;
- document.getElementById('gate-spot-label').textContent = `Gate.io 现货 (${currentGateSpotPair})`;
- document.getElementById('gate-futures-label').textContent = `Gate.io 期货 (${currentGateFuturesContract})`;
- document.getElementById('diff-label-oo-spot').textContent = `OO vs Gate.io 现货 (${currentGateSpotPair})`;
- document.getElementById('diff-label-oo-futures').textContent = `OO vs Gate.io 期货 (${currentGateFuturesContract})`;
- document.getElementById('diff-label-spot-futures').textContent = `Gate.io 现货 (${currentGateSpotPair}) vs 期货 (${currentGateFuturesContract})`;
- const priceChartTitle = `${currentAssetSymbol}/USDT 价格历史`;
- const priceChartYLabel = `价格 (${currentAssetSymbol}/USDT)`;
- const diffChartTitleText = "价差百分比历史";
- if (priceChartInstance) {
- priceChartInstance.options.plugins.title.text = priceChartTitle;
- priceChartInstance.options.scales.y.title.text = priceChartYLabel;
- }
- if (diffChartInstance) {
- diffChartInstance.options.plugins.title.text = diffChartTitleText;
- }
- // --- Update Table Data ---
- document.getElementById('oo-price').textContent = formatPrice(current.oo_price);
- document.getElementById('gate-spot-price').textContent = formatPrice(current.gate_spot_price);
- document.getElementById('gate-futures-price').textContent = formatPrice(current.gate_futures_price);
- document.getElementById('oo-status').textContent = current.oo_error ? `错误: ${current.oo_error}` : '正常';
- document.getElementById('gate-spot-status').textContent = current.gate_spot_error ? `错误: ${current.gate_spot_error}` : '正常';
- document.getElementById('gate-futures-status').textContent = current.gate_futures_error ? `错误: ${current.gate_futures_error}` : '正常';
- // Update error message classes
- document.getElementById('oo-status').className = current.oo_error ? 'status-cell error-message' : 'status-cell';
- document.getElementById('gate-spot-status').className = current.gate_spot_error ? 'status-cell error-message' : 'status-cell';
- document.getElementById('gate-futures-status').className = current.gate_futures_error ? 'status-cell error-message' : 'status-cell';
- // Update diff percentages
- const diffOOSpotEl = document.getElementById('diff-oo-vs-spot');
- const diffOOFuturesEl = document.getElementById('diff-oo-vs-futures');
- const diffSpotFuturesEl = document.getElementById('diff-spot-vs-futures');
- diffOOSpotEl.textContent = formatPercentage(current.diff_oo_vs_spot_percentage);
- diffOOFuturesEl.textContent = formatPercentage(current.diff_oo_vs_futures_percentage);
- diffSpotFuturesEl.textContent = formatPercentage(current.diff_spot_vs_futures_percentage);
- [diffOOSpotEl, diffOOFuturesEl, diffSpotFuturesEl].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 = current.last_updated || "N/A";
- // --- Update Charts ---
- const history = data.history;
- // Price History Chart
- const priceCtx = document.getElementById('priceHistoryChart').getContext('2d');
- const priceDatasets = [
- { label: 'OpenOcean', data: history.prices.oo, borderColor: 'rgb(75, 192, 192)', tension: 0.1, fill: false },
- { label: `Gate Spot (${currentGateSpotPair})`, data: history.prices.spot, borderColor: 'rgb(255, 99, 132)', tension: 0.1, fill: false },
- { label: `Gate Futures (${currentGateFuturesContract})`, data: history.prices.futures, borderColor: 'rgb(54, 162, 235)', tension: 0.1, fill: false }
- ];
- if (!priceChartInstance) {
- priceChartInstance = initializeChart(priceCtx, 'price', priceDatasets, history.prices.labels, priceChartYLabel, priceChartTitle, true);
- } else {
- priceChartInstance.data.datasets[1].label = `Gate Spot (${currentGateSpotPair})`;
- priceChartInstance.data.datasets[2].label = `Gate Futures (${currentGateFuturesContract})`;
- updateChartData(priceChartInstance, history.prices.labels, [history.prices.oo, history.prices.spot, history.prices.futures]);
- }
- // Diff Percentage Chart
- const diffCtx = document.getElementById('diffPercentageChart').getContext('2d');
- const diffDatasets = [
- { label: `OO vs Spot (${currentGateSpotPair})`, data: history.diffs.oo_vs_spot, borderColor: 'rgb(255, 159, 64)', tension: 0.1, fill: false },
- { label: `OO vs Futures (${currentGateFuturesContract})`, data: history.diffs.oo_vs_futures, borderColor: 'rgb(153, 102, 255)', tension: 0.1, fill: false },
- { label: `Spot (${currentGateSpotPair}) vs Futures (${currentGateFuturesContract})`, data: history.diffs.spot_vs_futures, borderColor: 'rgb(75, 192, 75)', tension: 0.1, fill: false }
- ];
- if (!diffChartInstance) {
- diffChartInstance = initializeChart(diffCtx, 'diff', diffDatasets, history.diffs.labels, '价差 (%)', diffChartTitleText, false);
- } else {
- diffChartInstance.data.datasets[0].label = `OO vs Spot (${currentGateSpotPair})`;
- diffChartInstance.data.datasets[1].label = `OO vs Futures (${currentGateFuturesContract})`;
- diffChartInstance.data.datasets[2].label = `Spot (${currentGateSpotPair}) vs Futures (${currentGateFuturesContract})`;
- updateChartData(diffChartInstance, history.diffs.labels, [history.diffs.oo_vs_spot, history.diffs.oo_vs_futures, history.diffs.spot_vs_futures]);
- }
- })
- .catch(error => {
- console.error('Error fetching data for all platforms:', error);
- // Display a general error, or individual errors if available.
- });
- }
- function togglePauseResume() { /* (Same as before) */
- isPaused = !isPaused;
- if (isPaused) {
- clearInterval(dataUpdateIntervalID); dataUpdateIntervalID = null;
- pauseResumeButton.textContent = '继续刷新'; pauseResumeButton.classList.add('pause-button-active');
- } else {
- pauseResumeButton.textContent = '暂停刷新'; pauseResumeButton.classList.remove('pause-button-active');
- updateDisplayAndCharts();
- dataUpdateIntervalID = setInterval(updateDisplayAndCharts, REFRESH_INTERVAL_MS);
- }
- }
- function resetAllZooms() { /* (Same as before, ensure it updates both charts) */
- if (priceChartInstance) {
- priceChartInstance.resetZoom(); priceChartInstance.options.scales.x.min = undefined; priceChartInstance.options.scales.x.max = undefined;
- }
- if (diffChartInstance) {
- diffChartInstance.resetZoom(); diffChartInstance.options.scales.x.min = undefined; diffChartInstance.options.scales.x.max = undefined;
- }
- if(isPaused){ updateDisplayAndCharts(); }
- else {
- if (priceChartInstance) priceChartInstance.update('none');
- if (diffChartInstance) diffChartInstance.update('none');
- }
- }
- pauseResumeButton.addEventListener('click', togglePauseResume);
- document.getElementById('reset-price-zoom-button').addEventListener('click', resetAllZooms);
- document.getElementById('reset-diff-zoom-button').addEventListener('click', resetAllZooms);
- updateDisplayAndCharts(); // Initial load
- if (!isPaused) {
- dataUpdateIntervalID = setInterval(updateDisplayAndCharts, REFRESH_INTERVAL_MS);
- }
- </script>
- </body>
- </html>
|