418 lines
14 KiB
HTML
418 lines
14 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Chan 数据提供商</title>
|
|
<style>
|
|
:root {
|
|
--bg: #0d1117;
|
|
--surface: #161b22;
|
|
--border: #30363d;
|
|
--text: #e6edf3;
|
|
--text-secondary: #8b949e;
|
|
--accent: #58a6ff;
|
|
--green: #3fb950;
|
|
--orange: #d29922;
|
|
--red: #f85149;
|
|
--purple: #bc8cff;
|
|
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
--mono: "SF Mono", "Fira Code", "Consolas", monospace;
|
|
}
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: var(--font);
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
}
|
|
.container { max-width: 1000px; margin: 0 auto; padding: 32px 24px; }
|
|
|
|
/* Header */
|
|
header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding-bottom: 20px;
|
|
border-bottom: 1px solid var(--border);
|
|
margin-bottom: 28px;
|
|
}
|
|
header .brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
}
|
|
header .logo {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 44px; height: 44px;
|
|
border-radius: 10px;
|
|
background: linear-gradient(135deg, #1c3d5a 0%, #2d1b5e 100%);
|
|
border: 1px solid var(--border);
|
|
font-size: 20px; font-weight: 700;
|
|
color: var(--accent);
|
|
}
|
|
header .brand h1 { font-size: 20px; font-weight: 600; }
|
|
header .brand h1 span { color: var(--accent); }
|
|
header .brand .sub { font-size: 12px; color: var(--text-secondary); }
|
|
.header-time {
|
|
font-family: var(--mono);
|
|
font-size: 13px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
/* Overview cards */
|
|
.overview {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
gap: 10px;
|
|
margin-bottom: 24px;
|
|
}
|
|
.ov-card {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 14px 16px;
|
|
text-align: center;
|
|
}
|
|
.ov-card .ov-label { font-size: 12px; color: var(--text-secondary); margin-bottom: 4px; }
|
|
.ov-card .ov-value {
|
|
font-family: var(--mono);
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
}
|
|
.ov-card .ov-value.green { color: var(--green); }
|
|
.ov-card .ov-value.orange { color: var(--orange); }
|
|
.ov-card .ov-value.accent { color: var(--accent); }
|
|
.ov-card .ov-value.purple { color: var(--purple); }
|
|
|
|
/* Section */
|
|
.section {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
margin-bottom: 12px;
|
|
overflow: hidden;
|
|
}
|
|
.section-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 12px 16px;
|
|
border-bottom: 1px solid var(--border);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
.section-header:hover { background: rgba(255,255,255,0.02); }
|
|
.section-header h2 { font-size: 14px; font-weight: 600; }
|
|
.section-header .count-badge {
|
|
font-size: 12px;
|
|
font-family: var(--mono);
|
|
color: var(--text-secondary);
|
|
background: var(--bg);
|
|
padding: 2px 8px;
|
|
border-radius: 10px;
|
|
}
|
|
.section-body { padding: 8px 16px 12px; }
|
|
|
|
/* Status dot */
|
|
.dot {
|
|
display: inline-block;
|
|
width: 8px; height: 8px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
.dot.green { background: var(--green); box-shadow: 0 0 6px #3fb95060; }
|
|
.dot.orange { background: var(--orange); box-shadow: 0 0 6px #d2992260; }
|
|
.dot.red { background: var(--red); box-shadow: 0 0 6px #f8514960; }
|
|
.dot.gray { background: var(--text-secondary); }
|
|
|
|
/* Symbol row */
|
|
.symbol-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 8px 0;
|
|
border-bottom: 1px solid rgba(48,54,61,0.5);
|
|
font-size: 14px;
|
|
}
|
|
.symbol-row:last-child { border-bottom: none; }
|
|
.symbol-row .name { font-family: var(--mono); font-size: 13px; min-width: 140px; }
|
|
.symbol-row .tag {
|
|
font-size: 11px;
|
|
font-family: var(--mono);
|
|
padding: 1px 6px;
|
|
border-radius: 4px;
|
|
background: rgba(88,166,255,0.1);
|
|
color: var(--accent);
|
|
}
|
|
|
|
/* TF chips */
|
|
.tf-list { display: flex; flex-wrap: wrap; gap: 4px; }
|
|
.tf-chip {
|
|
font-family: var(--mono);
|
|
font-size: 12px;
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
color: var(--text-secondary);
|
|
}
|
|
.tf-chip.base { border-color: #58a6ff40; color: var(--accent); }
|
|
.tf-chip.derived { border-color: #bc8cff40; color: var(--purple); }
|
|
|
|
/* Links bar */
|
|
.links-bar {
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
margin: 16px 0 24px;
|
|
}
|
|
.links-bar a {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 6px 14px;
|
|
border-radius: 6px;
|
|
font-size: 13px;
|
|
text-decoration: none;
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
color: var(--text);
|
|
transition: border-color 0.15s;
|
|
}
|
|
.links-bar a:hover { border-color: var(--accent); }
|
|
|
|
/* Footer */
|
|
footer {
|
|
border-top: 1px solid var(--border);
|
|
padding: 16px 0;
|
|
margin-top: 32px;
|
|
text-align: center;
|
|
color: var(--text-secondary);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.fade-in { animation: fadeIn 0.3s ease-in; }
|
|
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
|
|
<header>
|
|
<div class="brand">
|
|
<div class="logo">C</div>
|
|
<div>
|
|
<h1><span>Chan</span> 数据提供商</h1>
|
|
<div class="sub">加密货币 K 线数据服务</div>
|
|
</div>
|
|
</div>
|
|
<div class="header-time" id="header-time">—</div>
|
|
</header>
|
|
|
|
<!-- Overview -->
|
|
<div class="overview" id="overview">
|
|
<div class="ov-card"><div class="ov-label">服务状态</div><div class="ov-value" id="ov-status">加载中...</div></div>
|
|
<div class="ov-card"><div class="ov-label">交易所</div><div class="ov-value accent" id="ov-exchange">—</div></div>
|
|
<div class="ov-card"><div class="ov-label">交易对</div><div class="ov-value" id="ov-symbols">—</div></div>
|
|
<div class="ov-card"><div class="ov-label">基础周期</div><div class="ov-value accent" id="ov-base-tf">—</div></div>
|
|
<div class="ov-card"><div class="ov-label">衍生周期</div><div class="ov-value purple" id="ov-derived-tf">—</div></div>
|
|
<div class="ov-card"><div class="ov-label">数据就绪</div><div class="ov-value" id="ov-ready">—</div></div>
|
|
</div>
|
|
|
|
<!-- Links -->
|
|
<div class="links-bar">
|
|
<a href="/api/docs">📖 API 文档</a>
|
|
<a href="/docs">📋 Swagger UI</a>
|
|
<a href="/redoc">📄 ReDoc</a>
|
|
<a href="/api/candles?symbol=BTC/USDT:USDT&tf=1m&limit=5" target="_blank">📊 BTC 1m 示例</a>
|
|
<a href="/api/candles?symbol=ETH/USDT:USDT&tf=4h&limit=10" target="_blank">📊 ETH 4h 示例</a>
|
|
</div>
|
|
|
|
<!-- Symbols -->
|
|
<div class="section" id="section-symbols">
|
|
<div class="section-header" onclick="this.parentElement.classList.toggle('collapsed')">
|
|
<h2>📈 交易对</h2>
|
|
<span class="count-badge" id="sym-count">0</span>
|
|
</div>
|
|
<div class="section-body" id="symbol-list">
|
|
<div style="color:var(--text-secondary);font-size:13px;">加载中...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Timeframes -->
|
|
<div class="section">
|
|
<div class="section-header" onclick="this.parentElement.classList.toggle('collapsed')">
|
|
<h2>⏱ 时间周期</h2>
|
|
<span class="count-badge" id="tf-total">0</span>
|
|
</div>
|
|
<div class="section-body">
|
|
<div style="margin-bottom:8px;font-size:13px;color:var(--text-secondary);">基础周期(交易所直拉)</div>
|
|
<div class="tf-list" id="base-tf-list"></div>
|
|
<div style="margin:10px 0 8px;font-size:13px;color:var(--text-secondary);">衍生周期(内存合成)</div>
|
|
<div class="tf-list" id="derived-tf-list"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick query -->
|
|
<div class="section">
|
|
<div class="section-header">
|
|
<h2>⚡ 快速查询</h2>
|
|
</div>
|
|
<div class="section-body">
|
|
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:end;">
|
|
<div>
|
|
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:4px;">交易对</div>
|
|
<select id="q-symbol" style="background:var(--bg);border:1px solid var(--border);color:var(--text);padding:6px 10px;border-radius:6px;font-family:var(--mono);font-size:13px;"></select>
|
|
</div>
|
|
<div>
|
|
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:4px;">周期</div>
|
|
<select id="q-tf" style="background:var(--bg);border:1px solid var(--border);color:var(--text);padding:6px 10px;border-radius:6px;font-family:var(--mono);font-size:13px;"></select>
|
|
</div>
|
|
<div>
|
|
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:4px;">数量</div>
|
|
<select id="q-limit" style="background:var(--bg);border:1px solid var(--border);color:var(--text);padding:6px 10px;border-radius:6px;font-family:var(--mono);font-size:13px;">
|
|
<option>5</option><option selected>10</option><option>20</option><option>50</option>
|
|
</select>
|
|
</div>
|
|
<button onclick="quickQuery()" style="background:var(--accent);color:#fff;border:none;padding:6px 18px;border-radius:6px;cursor:pointer;font-size:13px;font-weight:600;">查询</button>
|
|
</div>
|
|
<pre id="q-result" style="margin-top:10px;display:none;"></pre>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<footer>
|
|
Chan Data Provider · Built with FastAPI + ccxt + pandas ·
|
|
更新于 <span id="footer-time">—</span>
|
|
</footer>
|
|
|
|
<script>
|
|
let healthData = null;
|
|
|
|
function fmtTime(ts) {
|
|
return new Date(ts).toLocaleString('zh-CN', { timeZone: 'UTC', hour12: false }) + ' UTC';
|
|
}
|
|
|
|
async function loadHealth() {
|
|
try {
|
|
const res = await fetch('/health');
|
|
healthData = await res.json();
|
|
renderHealth(healthData);
|
|
} catch {
|
|
document.getElementById('ov-status').textContent = '无法连接';
|
|
document.getElementById('ov-status').style.color = 'var(--red)';
|
|
document.getElementById('ov-ready').textContent = '断开';
|
|
document.getElementById('ov-ready').style.color = 'var(--red)';
|
|
}
|
|
}
|
|
|
|
function renderHealth(d) {
|
|
const now = Date.now();
|
|
document.getElementById('header-time').textContent = fmtTime(now);
|
|
document.getElementById('footer-time').textContent = fmtTime(now);
|
|
|
|
// Overview
|
|
const statusEl = document.getElementById('ov-status');
|
|
statusEl.textContent = '运行中';
|
|
statusEl.style.color = 'var(--green)';
|
|
|
|
document.getElementById('ov-exchange').textContent = d.exchange || '—';
|
|
|
|
const symCount = (d.symbols || []).length;
|
|
const symEl = document.getElementById('ov-symbols');
|
|
symEl.textContent = symCount + ' 个';
|
|
symEl.style.color = 'var(--accent)';
|
|
|
|
document.getElementById('ov-base-tf').textContent = (d.base_timeframes || []).length + ' 个';
|
|
document.getElementById('ov-derived-tf').textContent = (d.derived_timeframes || []).length + ' 个';
|
|
|
|
const readyEl = document.getElementById('ov-ready');
|
|
if (d.ready) {
|
|
readyEl.textContent = '已就绪';
|
|
readyEl.style.color = 'var(--green)';
|
|
} else {
|
|
readyEl.textContent = '同步中...';
|
|
readyEl.style.color = 'var(--orange)';
|
|
setTimeout(loadHealth, 2000);
|
|
}
|
|
|
|
// Symbols
|
|
const symList = document.getElementById('symbol-list');
|
|
const symCountEl = document.getElementById('sym-count');
|
|
symCountEl.textContent = symCount;
|
|
if (d.symbols && d.symbols.length > 0) {
|
|
symList.innerHTML = d.symbols.map(s => `
|
|
<div class="symbol-row">
|
|
<span class="dot green"></span>
|
|
<span class="name">${s}</span>
|
|
<span class="tag">${d.exchange || '—'}</span>
|
|
<a href="/api/candles?symbol=${encodeURIComponent(s)}&tf=1m&limit=5" target="_blank" style="margin-left:auto;font-size:12px;color:var(--accent);text-decoration:none;">1m →</a>
|
|
</div>
|
|
`).join('');
|
|
} else {
|
|
symList.innerHTML = '<div style="color:var(--text-secondary);font-size:13px;">暂无交易对</div>';
|
|
}
|
|
|
|
// Timeframes
|
|
document.getElementById('tf-total').textContent = (d.timeframes || []).length;
|
|
|
|
const baseList = document.getElementById('base-tf-list');
|
|
if (d.base_timeframes) {
|
|
baseList.innerHTML = d.base_timeframes.map(t => `<span class="tf-chip base">${t}</span>`).join('');
|
|
}
|
|
|
|
const derivedList = document.getElementById('derived-tf-list');
|
|
if (d.derived_timeframes) {
|
|
derivedList.innerHTML = d.derived_timeframes.map(t => `<span class="tf-chip derived">${t}</span>`).join('');
|
|
}
|
|
|
|
// Populate query selects
|
|
const symSelect = document.getElementById('q-symbol');
|
|
if (d.symbols && symSelect.options.length === 0) {
|
|
d.symbols.forEach(s => {
|
|
const opt = document.createElement('option');
|
|
opt.value = s;
|
|
opt.textContent = s;
|
|
symSelect.appendChild(opt);
|
|
});
|
|
}
|
|
|
|
const tfSelect = document.getElementById('q-tf');
|
|
if (d.timeframes && tfSelect.options.length === 0) {
|
|
d.timeframes.forEach(t => {
|
|
const opt = document.createElement('option');
|
|
opt.value = t;
|
|
opt.textContent = t;
|
|
tfSelect.appendChild(opt);
|
|
});
|
|
}
|
|
}
|
|
|
|
async function quickQuery() {
|
|
const symbol = document.getElementById('q-symbol').value;
|
|
const tf = document.getElementById('q-tf').value;
|
|
const limit = document.getElementById('q-limit').value;
|
|
const url = `/api/candles?symbol=${encodeURIComponent(symbol)}&tf=${tf}&limit=${limit}`;
|
|
const pre = document.getElementById('q-result');
|
|
pre.style.display = 'block';
|
|
pre.textContent = '查询中...';
|
|
try {
|
|
const res = await fetch(url);
|
|
const data = await res.json();
|
|
pre.textContent = JSON.stringify(data, null, 2);
|
|
} catch {
|
|
pre.textContent = '查询失败';
|
|
}
|
|
}
|
|
|
|
loadHealth();
|
|
setInterval(loadHealth, 10000);
|
|
</script>
|
|
</body>
|
|
</html>
|