|
@@ -3,7 +3,7 @@
|
|
|
<head>
|
|
<head>
|
|
|
<meta charset="UTF-8">
|
|
<meta charset="UTF-8">
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
- <title>价格与价差监控 Binance-OpenOcean</title>
|
|
|
|
|
|
|
+ <title>多平台价格与价差监控</title>
|
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<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>
|
|
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@2.0.1/dist/chartjs-plugin-zoom.min.js"></script>
|
|
|
<style>
|
|
<style>
|
|
@@ -11,54 +11,24 @@
|
|
|
.container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); margin-bottom: 20px; }
|
|
.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; }
|
|
h1, h2 { text-align: center; color: #333; }
|
|
|
table { width: 100%; border-collapse: collapse; margin-top: 20px; margin-bottom: 20px; }
|
|
table { width: 100%; border-collapse: collapse; margin-top: 20px; margin-bottom: 20px; }
|
|
|
- th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
|
|
|
|
|
|
|
+ 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; }
|
|
th { background-color: #e9e9e9; }
|
|
|
.price-up { color: green; }
|
|
.price-up { color: green; }
|
|
|
.price-down { color: red; }
|
|
.price-down { color: red; }
|
|
|
.error-message { color: #c00; font-style: italic; font-size: 0.9em; }
|
|
.error-message { color: #c00; font-style: italic; font-size: 0.9em; }
|
|
|
- .status-cell { /* For status messages, not just errors */ }
|
|
|
|
|
.timestamp { font-size: 0.9em; color: #666; text-align: right; margin-top: 15px; }
|
|
.timestamp { font-size: 0.9em; color: #666; text-align: right; margin-top: 15px; }
|
|
|
- .chart-container {
|
|
|
|
|
- position: relative;
|
|
|
|
|
- height: 45vh;
|
|
|
|
|
- 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;
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- text-decoration: none;
|
|
|
|
|
- display: inline-block;
|
|
|
|
|
- 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;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ .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>
|
|
</style>
|
|
|
</head>
|
|
</head>
|
|
|
<body>
|
|
<body>
|
|
|
- <!-- HTML 结构与之前相同,此处省略 -->
|
|
|
|
|
<div class="container">
|
|
<div class="container">
|
|
|
- <h1>USDT 价格监控</h1>
|
|
|
|
|
|
|
+ <h1 id="main-title">价格监控</h1>
|
|
|
<div class="controls-container">
|
|
<div class="controls-container">
|
|
|
<button id="pause-resume-button" class="control-button">暂停刷新</button>
|
|
<button id="pause-resume-button" class="control-button">暂停刷新</button>
|
|
|
</div>
|
|
</div>
|
|
@@ -66,24 +36,48 @@
|
|
|
<thead>
|
|
<thead>
|
|
|
<tr>
|
|
<tr>
|
|
|
<th>平台</th>
|
|
<th>平台</th>
|
|
|
- <th>价格</th>
|
|
|
|
|
|
|
+ <th>价格 (USDT)</th>
|
|
|
<th>状态/错误</th>
|
|
<th>状态/错误</th>
|
|
|
</tr>
|
|
</tr>
|
|
|
</thead>
|
|
</thead>
|
|
|
<tbody>
|
|
<tbody>
|
|
|
<tr>
|
|
<tr>
|
|
|
- <td>OpenOcean (BSC)</td>
|
|
|
|
|
|
|
+ <td class="platform-name">OpenOcean (BSC)</td>
|
|
|
<td id="oo-price">加载中...</td>
|
|
<td id="oo-price">加载中...</td>
|
|
|
<td id="oo-status" class="status-cell"></td>
|
|
<td id="oo-status" class="status-cell"></td>
|
|
|
</tr>
|
|
</tr>
|
|
|
<tr>
|
|
<tr>
|
|
|
- <td>Gate.io (Spot)</td>
|
|
|
|
|
- <td id="gate-price">加载中...</td>
|
|
|
|
|
- <td id="gate-status" class="status-cell"></td>
|
|
|
|
|
|
|
+ <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>
|
|
|
<tr>
|
|
<tr>
|
|
|
- <td><b>价差百分比</b></td>
|
|
|
|
|
- <td id="diff-percentage" colspan="2">计算中...</td>
|
|
|
|
|
|
|
+ <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>
|
|
</tr>
|
|
|
</tbody>
|
|
</tbody>
|
|
|
</table>
|
|
</table>
|
|
@@ -91,7 +85,7 @@
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div class="container">
|
|
<div class="container">
|
|
|
- <h2>价格历史曲线</h2>
|
|
|
|
|
|
|
+ <h2 id="price-chart-title">价格历史曲线</h2>
|
|
|
<div class="chart-container">
|
|
<div class="chart-container">
|
|
|
<canvas id="priceHistoryChart"></canvas>
|
|
<canvas id="priceHistoryChart"></canvas>
|
|
|
</div>
|
|
</div>
|
|
@@ -101,9 +95,9 @@
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div class="container">
|
|
<div class="container">
|
|
|
- <h2>价差百分比历史曲线</h2>
|
|
|
|
|
|
|
+ <h2 id="diff-chart-title">价差百分比历史曲线</h2>
|
|
|
<div class="chart-container">
|
|
<div class="chart-container">
|
|
|
- <canvas id="diffHistoryChart"></canvas>
|
|
|
|
|
|
|
+ <canvas id="diffPercentageChart"></canvas> <!-- Changed ID -->
|
|
|
</div>
|
|
</div>
|
|
|
<div class="controls-container">
|
|
<div class="controls-container">
|
|
|
<button id="reset-diff-zoom-button" class="control-button">重置缩放</button>
|
|
<button id="reset-diff-zoom-button" class="control-button">重置缩放</button>
|
|
@@ -112,140 +106,142 @@
|
|
|
|
|
|
|
|
<script>
|
|
<script>
|
|
|
let priceChartInstance = null;
|
|
let priceChartInstance = null;
|
|
|
- let diffChartInstance = null;
|
|
|
|
|
|
|
+ let diffChartInstance = null; // For percentage differences
|
|
|
|
|
|
|
|
let dataUpdateIntervalID = null;
|
|
let dataUpdateIntervalID = null;
|
|
|
let isPaused = false;
|
|
let isPaused = false;
|
|
|
- const REFRESH_INTERVAL_MS = 5000;
|
|
|
|
|
|
|
+ const REFRESH_INTERVAL_MS = 1000;
|
|
|
const pauseResumeButton = document.getElementById('pause-resume-button');
|
|
const pauseResumeButton = document.getElementById('pause-resume-button');
|
|
|
-
|
|
|
|
|
- let isSyncingZoomPan = false; // 标志位,防止同步死循环
|
|
|
|
|
|
|
+ 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) {
|
|
function formatPrice(priceStr) {
|
|
|
|
|
+ // (formatPrice function from previous example, can be reused)
|
|
|
if (priceStr === null || priceStr === undefined || priceStr === "N/A") return "N/A";
|
|
if (priceStr === null || priceStr === undefined || priceStr === "N/A") return "N/A";
|
|
|
const price = parseFloat(priceStr);
|
|
const price = parseFloat(priceStr);
|
|
|
- return isNaN(price) ? "N/A" : price.toFixed(6);
|
|
|
|
|
|
|
+ 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)}%`;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 函数:从源图表同步X轴到目标图表
|
|
|
|
|
- function syncXAxes(sourceChart, targetChart) {
|
|
|
|
|
- if (isSyncingZoomPan || !sourceChart || !targetChart) return; // 如果正在同步或图表未定义,则跳过
|
|
|
|
|
-
|
|
|
|
|
- isSyncingZoomPan = true; // 设置标志位
|
|
|
|
|
-
|
|
|
|
|
|
|
+ function syncXAxes(sourceChart, targetChart) { /* (Same as before) */
|
|
|
|
|
+ if (isSyncingZoomPan || !sourceChart || !targetChart) return;
|
|
|
|
|
+ isSyncingZoomPan = true;
|
|
|
const sourceXScale = sourceChart.scales.x;
|
|
const sourceXScale = sourceChart.scales.x;
|
|
|
const targetXScale = targetChart.scales.x;
|
|
const targetXScale = targetChart.scales.x;
|
|
|
-
|
|
|
|
|
if (sourceXScale && targetXScale) {
|
|
if (sourceXScale && targetXScale) {
|
|
|
- targetXScale.options.min = sourceXScale.min; // 同步X轴的最小值
|
|
|
|
|
- targetXScale.options.max = sourceXScale.max; // 同步X轴的最大值
|
|
|
|
|
- targetChart.update('none'); // 更新目标图表,'none' 表示不执行动画
|
|
|
|
|
|
|
+ targetXScale.options.min = sourceXScale.min;
|
|
|
|
|
+ targetXScale.options.max = sourceXScale.max;
|
|
|
|
|
+ targetChart.update('none');
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // 短暂延迟后重置标志位,允许下一次同步
|
|
|
|
|
- // 如果不加延迟,快速连续操作可能会有问题
|
|
|
|
|
- requestAnimationFrame(() => {
|
|
|
|
|
- isSyncingZoomPan = false;
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ requestAnimationFrame(() => { isSyncingZoomPan = false; });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- function initializeChart(ctx, chartIdentifier, chartType, datasetsConfig, initialLabels, titleText) {
|
|
|
|
|
- const chartInstance = new Chart(ctx, {
|
|
|
|
|
|
|
+ function initializeChart(ctx, chartIdentifier, datasetsConfig, initialLabels, yAxisTitle, chartTitleText, isPriceChart = true) {
|
|
|
|
|
+ return new Chart(ctx, {
|
|
|
type: 'line',
|
|
type: 'line',
|
|
|
- data: {
|
|
|
|
|
- labels: initialLabels,
|
|
|
|
|
- datasets: datasetsConfig
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ data: { labels: initialLabels, datasets: datasetsConfig },
|
|
|
options: {
|
|
options: {
|
|
|
- responsive: true,
|
|
|
|
|
- maintainAspectRatio: false,
|
|
|
|
|
- animation: {
|
|
|
|
|
- duration: 150
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ responsive: true, maintainAspectRatio: false, animation: { duration: 150 },
|
|
|
scales: {
|
|
scales: {
|
|
|
- x: {
|
|
|
|
|
- title: { display: true, text: '时间' },
|
|
|
|
|
- // 我们将通过回调函数同步 min/max,这里不设置
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ x: { title: { display: true, text: '时间' } },
|
|
|
y: {
|
|
y: {
|
|
|
- title: { display: true, text: chartType === 'price' ? '价格 (USDT)' : '价差 (%)' },
|
|
|
|
|
- beginAtZero: chartType === 'diff' ? false : undefined
|
|
|
|
|
|
|
+ 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: {
|
|
plugins: {
|
|
|
- legend: { position: 'top' },
|
|
|
|
|
- title: { display: true, text: titleText },
|
|
|
|
|
- zoom: {
|
|
|
|
|
- pan: {
|
|
|
|
|
- enabled: true,
|
|
|
|
|
- mode: 'x', // 只在X轴上平移 (Y轴通常不需要同步)
|
|
|
|
|
- threshold: 5,
|
|
|
|
|
- onPanComplete({chart}) { // 平移完成回调
|
|
|
|
|
- if (chartIdentifier === 'price' && diffChartInstance) {
|
|
|
|
|
- syncXAxes(chart, diffChartInstance);
|
|
|
|
|
- } else if (chartIdentifier === 'diff' && priceChartInstance) {
|
|
|
|
|
- syncXAxes(chart, priceChartInstance);
|
|
|
|
|
|
|
+ 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: {
|
|
zoom: {
|
|
|
- wheel: { enabled: true, speed: 0.1 }, // 调整滚轮速度
|
|
|
|
|
- pinch: { enabled: true },
|
|
|
|
|
|
|
+ wheel: { enabled: true, speed: 0.1 }, pinch: { enabled: true },
|
|
|
drag: { enabled: true, backgroundColor: 'rgba(0,123,255,0.25)'},
|
|
drag: { enabled: true, backgroundColor: 'rgba(0,123,255,0.25)'},
|
|
|
- mode: 'x', // 只在X轴上缩放
|
|
|
|
|
- onZoomComplete({chart}) { // 缩放完成回调
|
|
|
|
|
- if (chartIdentifier === 'price' && diffChartInstance) {
|
|
|
|
|
- syncXAxes(chart, diffChartInstance);
|
|
|
|
|
- } else if (chartIdentifier === 'diff' && priceChartInstance) {
|
|
|
|
|
- syncXAxes(chart, priceChartInstance);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ mode: 'x',
|
|
|
|
|
+ onZoomComplete({chart}) {
|
|
|
|
|
+ if (chartIdentifier === 'price' && diffChartInstance) syncXAxes(chart, diffChartInstance);
|
|
|
|
|
+ else if (chartIdentifier === 'diff' && priceChartInstance) syncXAxes(chart, priceChartInstance);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
- elements: {
|
|
|
|
|
- point:{ radius: 2 }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ elements: { point:{ radius: 1.5 } } // Smaller points for more lines
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
- return chartInstance;
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- function updateChartData(chartInstance, newLabels, newDatasetsData) {
|
|
|
|
|
- if (!chartInstance) return; // 确保图表实例存在
|
|
|
|
|
-
|
|
|
|
|
|
|
+ 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 currentXMin = chartInstance.scales.x.min;
|
|
|
const currentXMax = chartInstance.scales.x.max;
|
|
const currentXMax = chartInstance.scales.x.max;
|
|
|
- const isZoomed = (currentXMin !== undefined && currentXMax !== undefined &&
|
|
|
|
|
- (currentXMin !== chartInstance.data.labels[0] || currentXMax !== chartInstance.data.labels[chartInstance.data.labels.length - 1]));
|
|
|
|
|
|
|
+ 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;
|
|
chartInstance.data.labels = newLabels;
|
|
|
- newDatasetsData.forEach((datasetData, index) => {
|
|
|
|
|
- chartInstance.data.datasets[index].data = datasetData;
|
|
|
|
|
|
|
+ newDatasetsDataArray.forEach((datasetData, index) => {
|
|
|
|
|
+ if(chartInstance.data.datasets[index]) { // Check if dataset exists
|
|
|
|
|
+ chartInstance.data.datasets[index].data = datasetData;
|
|
|
|
|
+ }
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // 如果图表之前是缩放状态,并且新的标签范围与旧的缩放范围不完全匹配,
|
|
|
|
|
- // 尝试保持缩放,否则新的数据可能会导致缩放重置。
|
|
|
|
|
- // 但这里简单处理:如果暂停,不更新 x 轴的 min/max,让用户控制。
|
|
|
|
|
- // 如果正在刷新,则让图表根据新数据自动调整。
|
|
|
|
|
- // 若要更精细控制 (如保持缩放比例滚动),会更复杂。
|
|
|
|
|
if (!isPaused) {
|
|
if (!isPaused) {
|
|
|
- // 当数据刷新时,如果用户是手动缩放的,我们不应该重置它
|
|
|
|
|
- // Chart.js 会在数据更新时自动调整范围,除非 min/max 固定
|
|
|
|
|
- // 为了允许新数据扩展X轴,我们不在这里设置 min/max
|
|
|
|
|
- // 除非我们想实现固定窗口滚动
|
|
|
|
|
|
|
+ chartInstance.options.scales.x.min = undefined;
|
|
|
|
|
+ chartInstance.options.scales.x.max = undefined;
|
|
|
} else {
|
|
} else {
|
|
|
- // 如果暂停了,并且图表被用户缩放了,保持这个缩放
|
|
|
|
|
if(isZoomed) {
|
|
if(isZoomed) {
|
|
|
chartInstance.options.scales.x.min = currentXMin;
|
|
chartInstance.options.scales.x.min = currentXMin;
|
|
|
chartInstance.options.scales.x.max = currentXMax;
|
|
chartInstance.options.scales.x.max = currentXMax;
|
|
|
} else {
|
|
} else {
|
|
|
- // 如果没缩放,允许图表自动调整(尽管暂停时数据不会变)
|
|
|
|
|
chartInstance.options.scales.x.min = undefined;
|
|
chartInstance.options.scales.x.min = undefined;
|
|
|
chartInstance.options.scales.x.max = undefined;
|
|
chartInstance.options.scales.x.max = undefined;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
chartInstance.update('quiet');
|
|
chartInstance.update('quiet');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -254,137 +250,140 @@
|
|
|
.then(response => response.json())
|
|
.then(response => response.json())
|
|
|
.then(data => {
|
|
.then(data => {
|
|
|
const current = data.current;
|
|
const current = data.current;
|
|
|
- // ... (表格数据更新部分与之前相同,此处省略以保持简洁) ...
|
|
|
|
|
- document.getElementById('oo-price').textContent = formatPrice(current.oo_price);
|
|
|
|
|
- document.getElementById('gate-price').textContent = formatPrice(current.gate_price);
|
|
|
|
|
-
|
|
|
|
|
- const diffEl = document.getElementById('diff-percentage');
|
|
|
|
|
- if (current.difference_percentage && current.difference_percentage !== "N/A") {
|
|
|
|
|
- diffEl.textContent = current.difference_percentage;
|
|
|
|
|
- const diffValue = parseFloat(current.difference_percentage.replace('%', ''));
|
|
|
|
|
- if (!isNaN(diffValue)) {
|
|
|
|
|
- if (diffValue > 0) diffEl.className = 'price-up';
|
|
|
|
|
- else if (diffValue < 0) diffEl.className = 'price-down';
|
|
|
|
|
- else diffEl.className = '';
|
|
|
|
|
- } else {
|
|
|
|
|
- diffEl.className = '';
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- diffEl.textContent = current.difference_percentage || "N/A";
|
|
|
|
|
- diffEl.className = '';
|
|
|
|
|
|
|
+ // 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;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const ooStatusEl = document.getElementById('oo-status');
|
|
|
|
|
- ooStatusEl.textContent = current.oo_error ? `错误: ${current.oo_error}` : '正常';
|
|
|
|
|
- ooStatusEl.className = current.oo_error ? 'status-cell error-message' : 'status-cell ';
|
|
|
|
|
-
|
|
|
|
|
- const gateStatusEl = document.getElementById('gate-status');
|
|
|
|
|
- gateStatusEl.textContent = current.gate_error ? `错误: ${current.gate_error}` : '正常';
|
|
|
|
|
- gateStatusEl.className = current.gate_error ? 'status-cell error-message' : 'status-cell ';
|
|
|
|
|
|
|
+ // --- 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";
|
|
document.getElementById('last-updated').textContent = current.last_updated || "N/A";
|
|
|
|
|
|
|
|
|
|
+ // --- Update Charts ---
|
|
|
const history = data.history;
|
|
const history = data.history;
|
|
|
|
|
|
|
|
- // --- 价格历史图表 ---
|
|
|
|
|
|
|
+ // Price History Chart
|
|
|
const priceCtx = document.getElementById('priceHistoryChart').getContext('2d');
|
|
const priceCtx = document.getElementById('priceHistoryChart').getContext('2d');
|
|
|
const priceDatasets = [
|
|
const priceDatasets = [
|
|
|
{ label: 'OpenOcean', data: history.prices.oo, borderColor: 'rgb(75, 192, 192)', tension: 0.1, fill: false },
|
|
{ label: 'OpenOcean', data: history.prices.oo, borderColor: 'rgb(75, 192, 192)', tension: 0.1, fill: false },
|
|
|
- { label: 'Gate.io', data: history.prices.gate, borderColor: 'rgb(255, 99, 132)', 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) {
|
|
if (!priceChartInstance) {
|
|
|
- priceChartInstance = initializeChart(priceCtx, 'price', 'price', priceDatasets, history.prices.labels, '价格历史');
|
|
|
|
|
|
|
+ priceChartInstance = initializeChart(priceCtx, 'price', priceDatasets, history.prices.labels, priceChartYLabel, priceChartTitle, true);
|
|
|
} else {
|
|
} else {
|
|
|
- updateChartData(priceChartInstance, history.prices.labels, [history.prices.oo, history.prices.gate]);
|
|
|
|
|
|
|
+ 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]);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // --- 价差历史图表 ---
|
|
|
|
|
- const diffCtx = document.getElementById('diffHistoryChart').getContext('2d');
|
|
|
|
|
- const diffDatasets = [{ label: '价差百分比 (OO vs Gate)', data: history.difference.values, borderColor: 'rgb(54, 162, 235)', tension: 0.1, fill: false }];
|
|
|
|
|
|
|
+ // 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) {
|
|
if (!diffChartInstance) {
|
|
|
- diffChartInstance = initializeChart(diffCtx, 'diff', 'diff', diffDatasets, history.difference.labels, '价差百分比历史');
|
|
|
|
|
|
|
+ diffChartInstance = initializeChart(diffCtx, 'diff', diffDatasets, history.diffs.labels, '价差 (%)', diffChartTitleText, false);
|
|
|
} else {
|
|
} else {
|
|
|
- updateChartData(diffChartInstance, history.difference.labels, [history.difference.values]);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 初始加载或取消暂停时، 手动同步一次 (如果 X 轴不同步)
|
|
|
|
|
- // 避免初始加载时两个图表的 x 轴范围不一样
|
|
|
|
|
- if (priceChartInstance && diffChartInstance &&
|
|
|
|
|
- (priceChartInstance.scales.x.min !== diffChartInstance.scales.x.min ||
|
|
|
|
|
- priceChartInstance.scales.x.max !== diffChartInstance.scales.x.max) &&
|
|
|
|
|
- (!isPaused || !dataUpdateIntervalID ) // 只有在首次加载或从暂停恢复时才这样做
|
|
|
|
|
- ) {
|
|
|
|
|
- // console.log("Initial or resume sync needed");
|
|
|
|
|
- // syncXAxes(priceChartInstance, diffChartInstance); // 让价格图表作为主导
|
|
|
|
|
|
|
+ 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 => {
|
|
.catch(error => {
|
|
|
- console.error('Error fetching data:', error);
|
|
|
|
|
- // ... (错误处理与之前相同) ...
|
|
|
|
|
|
|
+ console.error('Error fetching data for all platforms:', error);
|
|
|
|
|
+ // Display a general error, or individual errors if available.
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- function togglePauseResume() {
|
|
|
|
|
|
|
+ function togglePauseResume() { /* (Same as before) */
|
|
|
isPaused = !isPaused;
|
|
isPaused = !isPaused;
|
|
|
if (isPaused) {
|
|
if (isPaused) {
|
|
|
- clearInterval(dataUpdateIntervalID);
|
|
|
|
|
- dataUpdateIntervalID = null;
|
|
|
|
|
- pauseResumeButton.textContent = '继续刷新';
|
|
|
|
|
- pauseResumeButton.classList.add('pause-button-active');
|
|
|
|
|
- console.log("Data refresh PAUSED");
|
|
|
|
|
|
|
+ clearInterval(dataUpdateIntervalID); dataUpdateIntervalID = null;
|
|
|
|
|
+ pauseResumeButton.textContent = '继续刷新'; pauseResumeButton.classList.add('pause-button-active');
|
|
|
} else {
|
|
} else {
|
|
|
- pauseResumeButton.textContent = '暂停刷新';
|
|
|
|
|
- pauseResumeButton.classList.remove('pause-button-active');
|
|
|
|
|
- updateDisplayAndCharts(); // Refresh immediately
|
|
|
|
|
|
|
+ pauseResumeButton.textContent = '暂停刷新'; pauseResumeButton.classList.remove('pause-button-active');
|
|
|
|
|
+ updateDisplayAndCharts();
|
|
|
dataUpdateIntervalID = setInterval(updateDisplayAndCharts, REFRESH_INTERVAL_MS);
|
|
dataUpdateIntervalID = setInterval(updateDisplayAndCharts, REFRESH_INTERVAL_MS);
|
|
|
- console.log("Data refresh RESUMED");
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- function resetAllZooms() {
|
|
|
|
|
|
|
+ function resetAllZooms() { /* (Same as before, ensure it updates both charts) */
|
|
|
if (priceChartInstance) {
|
|
if (priceChartInstance) {
|
|
|
- priceChartInstance.resetZoom();
|
|
|
|
|
- // 重置后,手动清除 options 里的 min/max,确保下次数据更新能自动调整
|
|
|
|
|
- priceChartInstance.options.scales.x.min = undefined;
|
|
|
|
|
- priceChartInstance.options.scales.x.max = undefined;
|
|
|
|
|
- priceChartInstance.update('none'); // 立即应用
|
|
|
|
|
|
|
+ priceChartInstance.resetZoom(); priceChartInstance.options.scales.x.min = undefined; priceChartInstance.options.scales.x.max = undefined;
|
|
|
}
|
|
}
|
|
|
if (diffChartInstance) {
|
|
if (diffChartInstance) {
|
|
|
- diffChartInstance.resetZoom();
|
|
|
|
|
- diffChartInstance.options.scales.x.min = undefined;
|
|
|
|
|
- diffChartInstance.options.scales.x.max = undefined;
|
|
|
|
|
- diffChartInstance.update('none'); // 立即应用
|
|
|
|
|
|
|
+ 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');
|
|
|
}
|
|
}
|
|
|
- // 重置后,如果两个图表X轴范围不同,需要再次同步
|
|
|
|
|
- // 简单起见,下次 updateDisplayAndCharts 时会自动尝试同步(如果需要)
|
|
|
|
|
- // 或者我们可以在 updateDisplayAndCharts 中,如果 isZoomed 为 false,则不设置 min/max
|
|
|
|
|
- if (priceChartInstance && diffChartInstance) {
|
|
|
|
|
- // 在这里,我们希望两个图表都显示完整的数据范围
|
|
|
|
|
- // 所以清空min/max后,让下次数据更新(如果是暂停状态,手动触发一次)来重建
|
|
|
|
|
- if(isPaused){ // 如果是暂停状态,主动更新一下以确保X轴重置并数据重绘
|
|
|
|
|
- updateDisplayAndCharts();
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // --- Event Listeners ---
|
|
|
|
|
pauseResumeButton.addEventListener('click', togglePauseResume);
|
|
pauseResumeButton.addEventListener('click', togglePauseResume);
|
|
|
|
|
+ document.getElementById('reset-price-zoom-button').addEventListener('click', resetAllZooms);
|
|
|
|
|
+ document.getElementById('reset-diff-zoom-button').addEventListener('click', resetAllZooms);
|
|
|
|
|
|
|
|
- document.getElementById('reset-price-zoom-button').addEventListener('click', () => {
|
|
|
|
|
- resetAllZooms(); // 现在一个按钮重置所有,并尝试同步
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- document.getElementById('reset-diff-zoom-button').addEventListener('click', () => {
|
|
|
|
|
- resetAllZooms(); // 现在一个按钮重置所有,并尝试同步
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // Initial data load and start interval
|
|
|
|
|
- updateDisplayAndCharts();
|
|
|
|
|
|
|
+ updateDisplayAndCharts(); // Initial load
|
|
|
if (!isPaused) {
|
|
if (!isPaused) {
|
|
|
dataUpdateIntervalID = setInterval(updateDisplayAndCharts, REFRESH_INTERVAL_MS);
|
|
dataUpdateIntervalID = setInterval(updateDisplayAndCharts, REFRESH_INTERVAL_MS);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
</script>
|
|
</script>
|
|
|
</body>
|
|
</body>
|
|
|
</html>
|
|
</html>
|