|
|
@@ -0,0 +1,660 @@
|
|
|
+<!DOCTYPE html>
|
|
|
+<html lang="zh-CN">
|
|
|
+<head>
|
|
|
+ <meta charset="UTF-8">
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
+ <title>Lead-Lag 数据面板</title>
|
|
|
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
|
+ <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
|
|
+ <style>
|
|
|
+ * {
|
|
|
+ margin: 0;
|
|
|
+ padding: 0;
|
|
|
+ box-sizing: border-box;
|
|
|
+ }
|
|
|
+
|
|
|
+ body {
|
|
|
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ min-height: 100vh;
|
|
|
+ padding: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .container {
|
|
|
+ max-width: 1400px;
|
|
|
+ margin: 0 auto;
|
|
|
+ background: rgba(255, 255, 255, 0.95);
|
|
|
+ border-radius: 20px;
|
|
|
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ .header {
|
|
|
+ background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
|
|
|
+ color: white;
|
|
|
+ padding: 30px;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .header h1 {
|
|
|
+ font-size: 2.5em;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ font-weight: 300;
|
|
|
+ }
|
|
|
+
|
|
|
+ .header p {
|
|
|
+ font-size: 1.1em;
|
|
|
+ opacity: 0.9;
|
|
|
+ }
|
|
|
+
|
|
|
+ .controls {
|
|
|
+ padding: 30px;
|
|
|
+ background: #f8f9fa;
|
|
|
+ border-bottom: 1px solid #e9ecef;
|
|
|
+ }
|
|
|
+
|
|
|
+ .control-group {
|
|
|
+ display: flex;
|
|
|
+ gap: 20px;
|
|
|
+ align-items: center;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .control-item {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .control-item label {
|
|
|
+ font-weight: 600;
|
|
|
+ color: #495057;
|
|
|
+ font-size: 0.9em;
|
|
|
+ }
|
|
|
+
|
|
|
+ select, input {
|
|
|
+ padding: 12px 16px;
|
|
|
+ border: 2px solid #e9ecef;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-size: 1em;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ background: white;
|
|
|
+ }
|
|
|
+
|
|
|
+ select:focus, input:focus {
|
|
|
+ outline: none;
|
|
|
+ border-color: #667eea;
|
|
|
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn {
|
|
|
+ padding: 12px 24px;
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ border-radius: 8px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 1em;
|
|
|
+ font-weight: 600;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ margin-top: 24px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn:hover {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn:disabled {
|
|
|
+ opacity: 0.6;
|
|
|
+ cursor: not-allowed;
|
|
|
+ transform: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .content {
|
|
|
+ padding: 30px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stats-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
|
+ gap: 20px;
|
|
|
+ margin-bottom: 30px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-card {
|
|
|
+ background: white;
|
|
|
+ padding: 25px;
|
|
|
+ border-radius: 12px;
|
|
|
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
|
|
|
+ border-left: 4px solid #667eea;
|
|
|
+ transition: transform 0.3s ease;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-card:hover {
|
|
|
+ transform: translateY(-5px);
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-card h3 {
|
|
|
+ color: #495057;
|
|
|
+ font-size: 0.9em;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ text-transform: uppercase;
|
|
|
+ letter-spacing: 0.5px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-card .value {
|
|
|
+ font-size: 1.8em;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #2c3e50;
|
|
|
+ margin-bottom: 5px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-card .unit {
|
|
|
+ font-size: 0.8em;
|
|
|
+ color: #6c757d;
|
|
|
+ }
|
|
|
+
|
|
|
+ .positive {
|
|
|
+ color: #28a745 !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .negative {
|
|
|
+ color: #dc3545 !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .chart-container {
|
|
|
+ background: white;
|
|
|
+ padding: 25px;
|
|
|
+ border-radius: 12px;
|
|
|
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
|
|
|
+ margin-bottom: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .chart-container h3 {
|
|
|
+ margin-bottom: 20px;
|
|
|
+ color: #495057;
|
|
|
+ font-size: 1.2em;
|
|
|
+ }
|
|
|
+
|
|
|
+ .chart-wrapper {
|
|
|
+ position: relative;
|
|
|
+ height: 400px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .loading {
|
|
|
+ text-align: center;
|
|
|
+ padding: 50px;
|
|
|
+ color: #6c757d;
|
|
|
+ font-size: 1.1em;
|
|
|
+ }
|
|
|
+
|
|
|
+ .error {
|
|
|
+ background: #f8d7da;
|
|
|
+ color: #721c24;
|
|
|
+ padding: 15px;
|
|
|
+ border-radius: 8px;
|
|
|
+ margin: 20px 0;
|
|
|
+ border: 1px solid #f5c6cb;
|
|
|
+ }
|
|
|
+
|
|
|
+ .last-update {
|
|
|
+ text-align: center;
|
|
|
+ color: #6c757d;
|
|
|
+ font-size: 0.9em;
|
|
|
+ margin-top: 20px;
|
|
|
+ padding: 15px;
|
|
|
+ background: #f8f9fa;
|
|
|
+ border-radius: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ @media (max-width: 768px) {
|
|
|
+ .control-group {
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: stretch;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stats-grid {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
+
|
|
|
+ .header h1 {
|
|
|
+ font-size: 2em;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+ <div class="container">
|
|
|
+ <div class="header">
|
|
|
+ <h1>Lead-Lag 数据面板</h1>
|
|
|
+ <p>Lighter vs Binance 价格差异分析</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="controls">
|
|
|
+ <div class="control-group">
|
|
|
+ <div class="control-item">
|
|
|
+ <label for="symbolSelect">选择币对:</label>
|
|
|
+ <select id="symbolSelect">
|
|
|
+ <option value="">请选择币对...</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ <div class="control-item">
|
|
|
+ <label for="timeRange">时间范围:</label>
|
|
|
+ <select id="timeRange">
|
|
|
+ <option value="1">1小时</option>
|
|
|
+ <option value="6">6小时</option>
|
|
|
+ <option value="24" selected>24小时</option>
|
|
|
+ <option value="72">3天</option>
|
|
|
+ <option value="168">7天</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ <div class="control-item">
|
|
|
+ <label for="autoRefresh">自动刷新:</label>
|
|
|
+ <select id="autoRefresh">
|
|
|
+ <option value="0">关闭</option>
|
|
|
+ <option value="5">5秒</option>
|
|
|
+ <option value="10" selected>10秒</option>
|
|
|
+ <option value="30">30秒</option>
|
|
|
+ <option value="60">1分钟</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ <button class="btn" onclick="loadData()">刷新数据</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="content">
|
|
|
+ <div id="loading" class="loading">请选择币对以查看数据</div>
|
|
|
+ <div id="error" class="error" style="display: none;"></div>
|
|
|
+
|
|
|
+ <div id="statsContainer" style="display: none;">
|
|
|
+ <div class="stats-grid">
|
|
|
+ <div class="stat-card">
|
|
|
+ <h3>当前标记价格差</h3>
|
|
|
+ <div class="value" id="currentMarkPriceDiff">--</div>
|
|
|
+ <div class="unit">USDT</div>
|
|
|
+ </div>
|
|
|
+ <div class="stat-card">
|
|
|
+ <h3>当前价格差</h3>
|
|
|
+ <div class="value" id="currentPriceDiff">--</div>
|
|
|
+ <div class="unit">USDT</div>
|
|
|
+ </div>
|
|
|
+ <div class="stat-card">
|
|
|
+ <h3>标记价格差百分比</h3>
|
|
|
+ <div class="value" id="currentMarkPriceDiffPct">--</div>
|
|
|
+ <div class="unit">%</div>
|
|
|
+ </div>
|
|
|
+ <div class="stat-card">
|
|
|
+ <h3>价格差百分比</h3>
|
|
|
+ <div class="value" id="currentPriceDiffPct">--</div>
|
|
|
+ <div class="unit">%</div>
|
|
|
+ </div>
|
|
|
+ <div class="stat-card">
|
|
|
+ <h3>平均标记价格差</h3>
|
|
|
+ <div class="value" id="avgMarkPriceDiff">--</div>
|
|
|
+ <div class="unit">USDT</div>
|
|
|
+ </div>
|
|
|
+ <div class="stat-card">
|
|
|
+ <h3>数据记录数</h3>
|
|
|
+ <div class="value" id="recordCount">--</div>
|
|
|
+ <div class="unit">条</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div id="chartsContainer" style="display: none;">
|
|
|
+ <div class="chart-container">
|
|
|
+ <h3>价格对比图</h3>
|
|
|
+ <div class="chart-wrapper">
|
|
|
+ <canvas id="priceChart"></canvas>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="chart-container">
|
|
|
+ <h3>价格差异图</h3>
|
|
|
+ <div class="chart-wrapper">
|
|
|
+ <canvas id="diffChart"></canvas>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div id="lastUpdate" class="last-update" style="display: none;"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <script>
|
|
|
+ let priceChart = null;
|
|
|
+ let diffChart = null;
|
|
|
+ let autoRefreshInterval = null;
|
|
|
+ const API_BASE = 'http://localhost:5000/api';
|
|
|
+
|
|
|
+ // 初始化
|
|
|
+ document.addEventListener('DOMContentLoaded', function() {
|
|
|
+ loadSymbols();
|
|
|
+ setupEventListeners();
|
|
|
+ });
|
|
|
+
|
|
|
+ function setupEventListeners() {
|
|
|
+ document.getElementById('symbolSelect').addEventListener('change', function() {
|
|
|
+ if (this.value) {
|
|
|
+ loadData();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ document.getElementById('timeRange').addEventListener('change', function() {
|
|
|
+ const symbol = document.getElementById('symbolSelect').value;
|
|
|
+ if (symbol) {
|
|
|
+ loadData();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ document.getElementById('autoRefresh').addEventListener('change', function() {
|
|
|
+ setupAutoRefresh();
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ async function loadSymbols() {
|
|
|
+ try {
|
|
|
+ const response = await axios.get(`${API_BASE}/symbols`);
|
|
|
+ const symbols = response.data.symbols;
|
|
|
+
|
|
|
+ const select = document.getElementById('symbolSelect');
|
|
|
+ select.innerHTML = '<option value="">请选择币对...</option>';
|
|
|
+
|
|
|
+ symbols.forEach(symbol => {
|
|
|
+ const option = document.createElement('option');
|
|
|
+ option.value = symbol;
|
|
|
+ option.textContent = symbol;
|
|
|
+ select.appendChild(option);
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ showError('加载币对列表失败: ' + error.message);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async function loadData() {
|
|
|
+ const symbol = document.getElementById('symbolSelect').value;
|
|
|
+ const hours = document.getElementById('timeRange').value;
|
|
|
+
|
|
|
+ if (!symbol) {
|
|
|
+ showError('请先选择币对');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ showLoading();
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 并行加载数据和统计信息
|
|
|
+ const [dataResponse, statsResponse, latestResponse] = await Promise.all([
|
|
|
+ axios.get(`${API_BASE}/data/${symbol}?hours=${hours}&limit=1000`),
|
|
|
+ axios.get(`${API_BASE}/stats/${symbol}?hours=${hours}`),
|
|
|
+ axios.get(`${API_BASE}/latest/${symbol}`)
|
|
|
+ ]);
|
|
|
+
|
|
|
+ const data = dataResponse.data.data;
|
|
|
+ const stats = statsResponse.data;
|
|
|
+ const latest = latestResponse.data;
|
|
|
+
|
|
|
+ updateStats(stats, latest);
|
|
|
+ updateCharts(data, symbol);
|
|
|
+ updateLastUpdateTime();
|
|
|
+
|
|
|
+ document.getElementById('statsContainer').style.display = 'block';
|
|
|
+ document.getElementById('chartsContainer').style.display = 'block';
|
|
|
+ document.getElementById('lastUpdate').style.display = 'block';
|
|
|
+ document.getElementById('loading').style.display = 'none';
|
|
|
+ document.getElementById('error').style.display = 'none';
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ showError('加载数据失败: ' + error.message);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function updateStats(stats, latest) {
|
|
|
+ // 更新当前数据
|
|
|
+ updateStatValue('currentMarkPriceDiff', latest.mark_price_diff);
|
|
|
+ updateStatValue('currentPriceDiff', latest.price_diff);
|
|
|
+ updateStatValue('currentMarkPriceDiffPct', latest.mark_price_diff_pct, '%');
|
|
|
+ updateStatValue('currentPriceDiffPct', latest.price_diff_pct, '%');
|
|
|
+
|
|
|
+ // 更新统计数据
|
|
|
+ updateStatValue('avgMarkPriceDiff', stats.avg_mark_price_diff);
|
|
|
+ document.getElementById('recordCount').textContent = stats.record_count || '--';
|
|
|
+ }
|
|
|
+
|
|
|
+ function updateStatValue(elementId, value, suffix = '') {
|
|
|
+ const element = document.getElementById(elementId);
|
|
|
+ if (value !== null && value !== undefined) {
|
|
|
+ const formattedValue = typeof value === 'number' ? value.toFixed(4) : value;
|
|
|
+ element.textContent = formattedValue + suffix;
|
|
|
+ element.className = 'value ' + (value > 0 ? 'positive' : value < 0 ? 'negative' : '');
|
|
|
+ } else {
|
|
|
+ element.textContent = '--';
|
|
|
+ element.className = 'value';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function updateCharts(data, symbol) {
|
|
|
+ if (!data || data.length === 0) {
|
|
|
+ showError('没有可用的数据');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const labels = data.map(item => new Date(item.timestamp).toLocaleTimeString());
|
|
|
+
|
|
|
+ // 价格对比图
|
|
|
+ updatePriceChart(labels, data, symbol);
|
|
|
+
|
|
|
+ // 价格差异图
|
|
|
+ updateDiffChart(labels, data, symbol);
|
|
|
+ }
|
|
|
+
|
|
|
+ function updatePriceChart(labels, data, symbol) {
|
|
|
+ const ctx = document.getElementById('priceChart').getContext('2d');
|
|
|
+
|
|
|
+ if (priceChart) {
|
|
|
+ priceChart.destroy();
|
|
|
+ }
|
|
|
+
|
|
|
+ priceChart = new Chart(ctx, {
|
|
|
+ type: 'line',
|
|
|
+ data: {
|
|
|
+ labels: labels,
|
|
|
+ datasets: [
|
|
|
+ {
|
|
|
+ label: 'Binance 标记价格',
|
|
|
+ data: data.map(item => item.binance_mark_price),
|
|
|
+ borderColor: '#ff6384',
|
|
|
+ backgroundColor: 'rgba(255, 99, 132, 0.1)',
|
|
|
+ tension: 0.1,
|
|
|
+ pointRadius: 0
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: 'Lighter 标记价格',
|
|
|
+ data: data.map(item => item.lighter_mark_price),
|
|
|
+ borderColor: '#36a2eb',
|
|
|
+ backgroundColor: 'rgba(54, 162, 235, 0.1)',
|
|
|
+ tension: 0.1,
|
|
|
+ pointRadius: 0
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: 'Binance 最新价格',
|
|
|
+ data: data.map(item => item.binance_price),
|
|
|
+ borderColor: '#ffce56',
|
|
|
+ backgroundColor: 'rgba(255, 206, 86, 0.1)',
|
|
|
+ tension: 0.1,
|
|
|
+ pointRadius: 0
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: 'Lighter 最新价格',
|
|
|
+ data: data.map(item => item.lighter_price),
|
|
|
+ borderColor: '#4bc0c0',
|
|
|
+ backgroundColor: 'rgba(75, 192, 192, 0.1)',
|
|
|
+ tension: 0.1,
|
|
|
+ pointRadius: 0
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ options: {
|
|
|
+ responsive: true,
|
|
|
+ maintainAspectRatio: false,
|
|
|
+ plugins: {
|
|
|
+ title: {
|
|
|
+ display: true,
|
|
|
+ text: `${symbol} 价格对比`
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ position: 'top'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ scales: {
|
|
|
+ y: {
|
|
|
+ beginAtZero: false,
|
|
|
+ title: {
|
|
|
+ display: true,
|
|
|
+ text: '价格 (USDT)'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ x: {
|
|
|
+ title: {
|
|
|
+ display: true,
|
|
|
+ text: '时间'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function updateDiffChart(labels, data, symbol) {
|
|
|
+ const ctx = document.getElementById('diffChart').getContext('2d');
|
|
|
+
|
|
|
+ if (diffChart) {
|
|
|
+ diffChart.destroy();
|
|
|
+ }
|
|
|
+
|
|
|
+ diffChart = new Chart(ctx, {
|
|
|
+ type: 'line',
|
|
|
+ data: {
|
|
|
+ labels: labels,
|
|
|
+ datasets: [
|
|
|
+ {
|
|
|
+ label: '标记价格差 (USDT)',
|
|
|
+ data: data.map(item => item.mark_price_diff),
|
|
|
+ borderColor: '#ff6384',
|
|
|
+ backgroundColor: 'rgba(255, 99, 132, 0.1)',
|
|
|
+ tension: 0.1,
|
|
|
+ pointRadius: 0,
|
|
|
+ yAxisID: 'y'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '价格差 (USDT)',
|
|
|
+ data: data.map(item => item.price_diff),
|
|
|
+ borderColor: '#36a2eb',
|
|
|
+ backgroundColor: 'rgba(54, 162, 235, 0.1)',
|
|
|
+ tension: 0.1,
|
|
|
+ pointRadius: 0,
|
|
|
+ yAxisID: 'y'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '标记价格差 (%)',
|
|
|
+ data: data.map(item => item.mark_price_diff_pct),
|
|
|
+ borderColor: '#ffce56',
|
|
|
+ backgroundColor: 'rgba(255, 206, 86, 0.1)',
|
|
|
+ tension: 0.1,
|
|
|
+ pointRadius: 0,
|
|
|
+ yAxisID: 'y1'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '价格差 (%)',
|
|
|
+ data: data.map(item => item.price_diff_pct),
|
|
|
+ borderColor: '#4bc0c0',
|
|
|
+ backgroundColor: 'rgba(75, 192, 192, 0.1)',
|
|
|
+ tension: 0.1,
|
|
|
+ pointRadius: 0,
|
|
|
+ yAxisID: 'y1'
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ options: {
|
|
|
+ responsive: true,
|
|
|
+ maintainAspectRatio: false,
|
|
|
+ plugins: {
|
|
|
+ title: {
|
|
|
+ display: true,
|
|
|
+ text: `${symbol} 价格差异`
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ position: 'top'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ scales: {
|
|
|
+ y: {
|
|
|
+ type: 'linear',
|
|
|
+ display: true,
|
|
|
+ position: 'left',
|
|
|
+ title: {
|
|
|
+ display: true,
|
|
|
+ text: '价格差 (USDT)'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ y1: {
|
|
|
+ type: 'linear',
|
|
|
+ display: true,
|
|
|
+ position: 'right',
|
|
|
+ title: {
|
|
|
+ display: true,
|
|
|
+ text: '价格差 (%)'
|
|
|
+ },
|
|
|
+ grid: {
|
|
|
+ drawOnChartArea: false,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ x: {
|
|
|
+ title: {
|
|
|
+ display: true,
|
|
|
+ text: '时间'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function setupAutoRefresh() {
|
|
|
+ const interval = parseInt(document.getElementById('autoRefresh').value);
|
|
|
+
|
|
|
+ if (autoRefreshInterval) {
|
|
|
+ clearInterval(autoRefreshInterval);
|
|
|
+ autoRefreshInterval = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (interval > 0) {
|
|
|
+ autoRefreshInterval = setInterval(() => {
|
|
|
+ const symbol = document.getElementById('symbolSelect').value;
|
|
|
+ if (symbol) {
|
|
|
+ loadData();
|
|
|
+ }
|
|
|
+ }, interval * 1000);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function showLoading() {
|
|
|
+ document.getElementById('loading').style.display = 'block';
|
|
|
+ document.getElementById('loading').textContent = '正在加载数据...';
|
|
|
+ document.getElementById('error').style.display = 'none';
|
|
|
+ }
|
|
|
+
|
|
|
+ function showError(message) {
|
|
|
+ document.getElementById('error').textContent = message;
|
|
|
+ document.getElementById('error').style.display = 'block';
|
|
|
+ document.getElementById('loading').style.display = 'none';
|
|
|
+ }
|
|
|
+
|
|
|
+ function updateLastUpdateTime() {
|
|
|
+ const now = new Date();
|
|
|
+ document.getElementById('lastUpdate').textContent = `最后更新: ${now.toLocaleString()}`;
|
|
|
+ }
|
|
|
+ </script>
|
|
|
+</body>
|
|
|
+</html>
|