|
|
@@ -9,21 +9,24 @@
|
|
|
<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; }
|
|
|
+ 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; 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; }
|
|
|
+ .error-message { color: #c00; font-style: italic; }
|
|
|
+ .status-cell { min-width: 100px; } /* Give status cells some min width */
|
|
|
+ .timestamp { font-size: 0.9em; color: #666; text-align: right; margin-top: 10px; }
|
|
|
+ .chart-container { position: relative; height: 38vh; width: 95vw; margin: auto; margin-bottom: 10px; } /* Slightly smaller height for more charts if needed */
|
|
|
+ .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; }
|
|
|
.pause-button-active:hover { background-color: #e0a800; }
|
|
|
.platform-name {font-weight: bold;}
|
|
|
+ #main-title { font-size: 1.8em; }
|
|
|
+ h2 { font-size: 1.3em; }
|
|
|
</style>
|
|
|
</head>
|
|
|
<body>
|
|
|
@@ -37,7 +40,7 @@
|
|
|
<tr>
|
|
|
<th>平台</th>
|
|
|
<th>价格 (USDT)</th>
|
|
|
- <th>状态/错误</th>
|
|
|
+ <th class="status-cell">状态/错误</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
@@ -97,7 +100,7 @@
|
|
|
<div class="container">
|
|
|
<h2 id="diff-chart-title">价差百分比历史曲线</h2>
|
|
|
<div class="chart-container">
|
|
|
- <canvas id="diffPercentageChart"></canvas> <!-- Changed ID -->
|
|
|
+ <canvas id="diffPercentageChart"></canvas>
|
|
|
</div>
|
|
|
<div class="controls-container">
|
|
|
<button id="reset-diff-zoom-button" class="control-button">重置缩放</button>
|
|
|
@@ -106,15 +109,14 @@
|
|
|
|
|
|
<script>
|
|
|
let priceChartInstance = null;
|
|
|
- let diffChartInstance = null; // For percentage differences
|
|
|
+ let diffChartInstance = null;
|
|
|
|
|
|
let dataUpdateIntervalID = null;
|
|
|
let isPaused = false;
|
|
|
- const REFRESH_INTERVAL_MS = 1000;
|
|
|
+ const REFRESH_INTERVAL_MS = 1000; // 1秒刷新一次
|
|
|
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' }}",
|
|
|
@@ -125,31 +127,30 @@
|
|
|
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";
|
|
|
+ if (priceStr === null || priceStr === undefined || priceStr === "N/A" || priceStr.toLowerCase() === "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);
|
|
|
+ if (price === 0) return "0.00000000"; // More precision for 0
|
|
|
+ if (Math.abs(price) < 0.0000001 && price !== 0) return price.toExponential(3);
|
|
|
+ if (Math.abs(price) < 0.001) return price.toFixed(8);
|
|
|
+ if (Math.abs(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;
|
|
|
+ if (percStr === null || percStr === undefined || percStr === "N/A" || percStr.toLowerCase() === "n/a") return "N/A";
|
|
|
+ if (String(percStr).includes('%')) return percStr; // Already formatted
|
|
|
const perc = parseFloat(percStr);
|
|
|
if (isNaN(perc)) return "N/A";
|
|
|
return `${perc > 0 ? '+' : ''}${perc.toFixed(4)}%`;
|
|
|
}
|
|
|
|
|
|
- function syncXAxes(sourceChart, targetChart) { /* (Same as before) */
|
|
|
+ function syncXAxes(sourceChart, targetChart) {
|
|
|
if (isSyncingZoomPan || !sourceChart || !targetChart) return;
|
|
|
isSyncingZoomPan = true;
|
|
|
const sourceXScale = sourceChart.scales.x;
|
|
|
const targetXScale = targetChart.scales.x;
|
|
|
- if (sourceXScale && targetXScale) {
|
|
|
+ if (sourceXScale && targetXScale && sourceXScale.min !== undefined && sourceXScale.max !== undefined) { // Ensure min/max are defined
|
|
|
targetXScale.options.min = sourceXScale.min;
|
|
|
targetXScale.options.max = sourceXScale.max;
|
|
|
targetChart.update('none');
|
|
|
@@ -162,12 +163,12 @@
|
|
|
type: 'line',
|
|
|
data: { labels: initialLabels, datasets: datasetsConfig },
|
|
|
options: {
|
|
|
- responsive: true, maintainAspectRatio: false, animation: { duration: 150 },
|
|
|
+ responsive: true, maintainAspectRatio: false, animation: { duration: 0 }, // Disable animation for faster updates
|
|
|
scales: {
|
|
|
x: { title: { display: true, text: '时间' } },
|
|
|
y: {
|
|
|
title: { display: true, text: yAxisTitle },
|
|
|
- beginAtZero: !isPriceChart ? false : undefined, // For diff chart, don't begin at zero
|
|
|
+ beginAtZero: !isPriceChart ? false : undefined,
|
|
|
ticks: {
|
|
|
callback: function(value) {
|
|
|
return isPriceChart ? formatPrice(String(value)) : parseFloat(value).toFixed(2) + '%';
|
|
|
@@ -178,6 +179,7 @@
|
|
|
plugins: {
|
|
|
legend: { position: 'top' }, title: { display: true, text: chartTitleText },
|
|
|
tooltip: {
|
|
|
+ mode: 'index', intersect: false,
|
|
|
callbacks: {
|
|
|
label: function(context) {
|
|
|
let label = context.dataset.label || '';
|
|
|
@@ -189,18 +191,15 @@
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
- zoom: { /* (Zoom config same as before) */
|
|
|
- pan: {
|
|
|
- enabled: true, mode: 'x', threshold: 5,
|
|
|
+ zoom: {
|
|
|
+ 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',
|
|
|
+ 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);
|
|
|
@@ -208,13 +207,12 @@
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
- elements: { point:{ radius: 1.5 } } // Smaller points for more lines
|
|
|
+ elements: { point:{ radius: 1.5, hoverRadius: 3 } }
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
@@ -225,7 +223,7 @@
|
|
|
|
|
|
chartInstance.data.labels = newLabels;
|
|
|
newDatasetsDataArray.forEach((datasetData, index) => {
|
|
|
- if(chartInstance.data.datasets[index]) { // Check if dataset exists
|
|
|
+ if(chartInstance.data.datasets[index]) {
|
|
|
chartInstance.data.datasets[index].data = datasetData;
|
|
|
}
|
|
|
});
|
|
|
@@ -242,7 +240,7 @@
|
|
|
chartInstance.options.scales.x.max = undefined;
|
|
|
}
|
|
|
}
|
|
|
- chartInstance.update('quiet');
|
|
|
+ chartInstance.update('none'); // Use 'none' for no animation for faster update
|
|
|
}
|
|
|
|
|
|
function updateDisplayAndCharts() {
|
|
|
@@ -250,16 +248,13 @@
|
|
|
.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})`;
|
|
|
@@ -276,48 +271,39 @@
|
|
|
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').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 : '正常';
|
|
|
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 = ''; }
|
|
|
+ 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 }
|
|
|
+ { label: 'OpenOcean', data: history.prices.oo, borderColor: 'rgb(75, 192, 192)', tension: 0.1, fill: false, borderWidth: 1.5 },
|
|
|
+ { label: `Gate Spot (${currentGateSpotPair})`, data: history.prices.spot, borderColor: 'rgb(255, 99, 132)', tension: 0.1, fill: false, borderWidth: 1.5 },
|
|
|
+ { label: `Gate Futures (${currentGateFuturesContract})`, data: history.prices.futures, borderColor: 'rgb(54, 162, 235)', tension: 0.1, fill: false, borderWidth: 1.5 }
|
|
|
];
|
|
|
if (!priceChartInstance) {
|
|
|
priceChartInstance = initializeChart(priceCtx, 'price', priceDatasets, history.prices.labels, priceChartYLabel, priceChartTitle, true);
|
|
|
@@ -327,12 +313,11 @@
|
|
|
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 }
|
|
|
+ { label: `OO vs Spot (${currentGateSpotPair})`, data: history.diffs.oo_vs_spot, borderColor: 'rgb(255, 159, 64)', tension: 0.1, fill: false, borderWidth: 1.5 },
|
|
|
+ { label: `OO vs Futures (${currentGateFuturesContract})`, data: history.diffs.oo_vs_futures, borderColor: 'rgb(153, 102, 255)', tension: 0.1, fill: false, borderWidth: 1.5 },
|
|
|
+ { label: `Spot (${currentGateSpotPair}) vs Futures (${currentGateFuturesContract})`, data: history.diffs.spot_vs_futures, borderColor: 'rgb(75, 192, 75)', tension: 0.1, fill: false, borderWidth: 1.5 }
|
|
|
];
|
|
|
if (!diffChartInstance) {
|
|
|
diffChartInstance = initializeChart(diffCtx, 'diff', diffDatasets, history.diffs.labels, '价差 (%)', diffChartTitleText, false);
|
|
|
@@ -342,36 +327,34 @@
|
|
|
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) */
|
|
|
+ function togglePauseResume() {
|
|
|
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();
|
|
|
+ updateDisplayAndCharts(); // Refresh immediately on resume
|
|
|
dataUpdateIntervalID = setInterval(updateDisplayAndCharts, REFRESH_INTERVAL_MS);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- function resetAllZooms() { /* (Same as before, ensure it updates both charts) */
|
|
|
+ function resetAllZooms() {
|
|
|
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(isPaused){ updateDisplayAndCharts(); } // If paused, manually trigger redraw
|
|
|
+ else { // If not paused, next interval will update, or force faster view update
|
|
|
+ if (priceChartInstance) priceChartInstance.update('none'); // 'none' to avoid animation interference
|
|
|
if (diffChartInstance) diffChartInstance.update('none');
|
|
|
}
|
|
|
}
|
|
|
@@ -380,10 +363,13 @@
|
|
|
document.getElementById('reset-price-zoom-button').addEventListener('click', resetAllZooms);
|
|
|
document.getElementById('reset-diff-zoom-button').addEventListener('click', resetAllZooms);
|
|
|
|
|
|
+ console.log("Frontend Initializing...");
|
|
|
+ console.log("Initial Config from Flask:", initialConfig);
|
|
|
+
|
|
|
updateDisplayAndCharts(); // Initial load
|
|
|
if (!isPaused) {
|
|
|
dataUpdateIntervalID = setInterval(updateDisplayAndCharts, REFRESH_INTERVAL_MS);
|
|
|
}
|
|
|
</script>
|
|
|
</body>
|
|
|
-</html>
|
|
|
+</html>
|