518 lines
17 KiB
HTML
518 lines
17 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 数据提供商 - API 文档</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;
|
||
padding: 0;
|
||
}
|
||
.container { max-width: 960px; margin: 0 auto; padding: 24px 20px; }
|
||
|
||
/* Header */
|
||
header {
|
||
border-bottom: 1px solid var(--border);
|
||
padding: 32px 0 24px;
|
||
margin-bottom: 32px;
|
||
}
|
||
header h1 { font-size: 28px; font-weight: 600; margin-bottom: 8px; }
|
||
header h1 span { color: var(--accent); }
|
||
header .subtitle { color: var(--text-secondary); font-size: 15px; }
|
||
header .badge {
|
||
display: inline-block;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 6px;
|
||
padding: 2px 10px;
|
||
font-size: 13px;
|
||
font-family: var(--mono);
|
||
color: var(--text-secondary);
|
||
margin-top: 12px;
|
||
}
|
||
header .badge span { color: var(--green); }
|
||
|
||
/* Section */
|
||
section { margin-bottom: 40px; }
|
||
section h2 {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
margin-bottom: 16px;
|
||
padding-bottom: 8px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
section h3 {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
margin: 20px 0 8px;
|
||
color: var(--accent);
|
||
}
|
||
|
||
/* Endpoint card */
|
||
.endpoint {
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
margin-bottom: 16px;
|
||
overflow: hidden;
|
||
}
|
||
.endpoint-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 14px 16px;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
.endpoint-header:hover { background: rgba(255,255,255,0.03); }
|
||
.method {
|
||
display: inline-block;
|
||
font-family: var(--mono);
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
min-width: 56px;
|
||
text-align: center;
|
||
}
|
||
.method.get { background: #1c3d5a; color: var(--accent); }
|
||
.method.ws { background: #2d1b5e; color: var(--purple); }
|
||
.endpoint-path {
|
||
font-family: var(--mono);
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: var(--text);
|
||
}
|
||
.endpoint-desc {
|
||
font-size: 13px;
|
||
color: var(--text-secondary);
|
||
margin-left: auto;
|
||
text-align: right;
|
||
}
|
||
.endpoint-body {
|
||
padding: 0 16px 16px;
|
||
border-top: 1px solid var(--border);
|
||
display: none;
|
||
}
|
||
.endpoint.open .endpoint-body { display: block; }
|
||
.endpoint-body > div { margin-top: 12px; }
|
||
|
||
/* Table */
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 14px;
|
||
}
|
||
th, td {
|
||
text-align: left;
|
||
padding: 8px 12px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
th { color: var(--text-secondary); font-weight: 500; font-size: 12px; text-transform: uppercase; }
|
||
td { font-family: var(--mono); font-size: 13px; }
|
||
td.optional { color: var(--text-secondary); font-size: 12px; }
|
||
td .type { color: var(--orange); }
|
||
td .type-num { color: var(--accent); }
|
||
|
||
/* Code block */
|
||
pre {
|
||
background: #010409;
|
||
border: 1px solid var(--border);
|
||
border-radius: 6px;
|
||
padding: 12px 16px;
|
||
overflow-x: auto;
|
||
font-family: var(--mono);
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
margin: 8px 0;
|
||
}
|
||
code { font-family: var(--mono); font-size: 13px; }
|
||
pre .comment { color: #8b949e; }
|
||
pre .string { color: #a5d6ff; }
|
||
pre .key { color: #79c0ff; }
|
||
pre .num { color: #79c0ff; }
|
||
pre .null { color: #d2a8ff; }
|
||
pre .bool { color: #d2a8ff; }
|
||
|
||
/* WS message box */
|
||
.ws-box {
|
||
background: #010409;
|
||
border: 1px solid var(--border);
|
||
border-radius: 6px;
|
||
padding: 12px 16px;
|
||
margin: 8px 0;
|
||
}
|
||
.ws-box .label {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--text-secondary);
|
||
text-transform: uppercase;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
p { font-size: 14px; color: var(--text-secondary); margin-bottom: 8px; }
|
||
ul { padding-left: 20px; font-size: 14px; color: var(--text-secondary); }
|
||
li { margin-bottom: 4px; }
|
||
a { color: var(--accent); text-decoration: none; }
|
||
a:hover { text-decoration: underline; }
|
||
|
||
.note {
|
||
background: rgba(210, 153, 34, 0.1);
|
||
border: 1px solid rgba(210, 153, 34, 0.3);
|
||
border-radius: 6px;
|
||
padding: 10px 14px;
|
||
font-size: 13px;
|
||
color: var(--orange);
|
||
margin: 8px 0;
|
||
}
|
||
|
||
.toc { margin-bottom: 32px; }
|
||
.toc a {
|
||
display: inline-block;
|
||
padding: 4px 12px;
|
||
margin: 2px 0;
|
||
font-size: 14px;
|
||
color: var(--accent);
|
||
}
|
||
|
||
footer {
|
||
border-top: 1px solid var(--border);
|
||
padding: 20px 0;
|
||
text-align: center;
|
||
color: var(--text-secondary);
|
||
font-size: 13px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
|
||
<header>
|
||
<h1><span>Chan</span> 数据提供商</h1>
|
||
<p class="subtitle">加密货币 K 线数据 HTTP + WebSocket API</p>
|
||
<div class="badge">v1.0.0 | <span>binance</span> | port 9009</div>
|
||
</header>
|
||
|
||
<nav class="toc">
|
||
<a href="#root">GET /</a>
|
||
<a href="#health">GET /health</a>
|
||
<a href="#timeframes">GET /timeframes</a>
|
||
<a href="#candles">GET /api/candles</a>
|
||
<a href="#websocket">WebSocket /ws</a>
|
||
<a href="#timeframes-ref">时间周期参考</a>
|
||
</nav>
|
||
|
||
<!-- ============ GET / ============ -->
|
||
<section id="root">
|
||
<h2>服务信息</h2>
|
||
<div class="endpoint open">
|
||
<div class="endpoint-header" onclick="this.parentElement.classList.toggle('open')">
|
||
<span class="method get">GET</span>
|
||
<span class="endpoint-path">/</span>
|
||
<span class="endpoint-desc">服务基本信息</span>
|
||
</div>
|
||
<div class="endpoint-body">
|
||
<p>返回服务名称、交易所、交易对列表、可用周期及就绪状态。</p>
|
||
<h3>响应</h3>
|
||
<pre>{
|
||
<span class="key">"service"</span>: <span class="string">"Data Provider"</span>,
|
||
<span class="key">"exchange"</span>: <span class="string">"binance"</span>,
|
||
<span class="key">"symbols"</span>: [<span class="string">"BTC/USDT:USDT"</span>, <span class="string">"ETH/USDT:USDT"</span>, ...],
|
||
<span class="key">"base_timeframes"</span>: [<span class="string">"1m"</span>, <span class="string">"1h"</span>, <span class="string">"1d"</span>, <span class="string">"1w"</span>],
|
||
<span class="key">"derived_timeframes"</span>: [<span class="string">"5m"</span>, <span class="string">"15m"</span>, <span class="string">"4h"</span>, ...],
|
||
<span class="key">"timeframes"</span>: [<span class="string">"1m"</span>, <span class="string">"1h"</span>, ..., <span class="string">"5m"</span>, <span class="string">"15m"</span>, ...],
|
||
<span class="key">"ready"</span>: <span class="bool">true</span>
|
||
}</pre>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ GET /health ============ -->
|
||
<section id="health">
|
||
<h2>健康检查</h2>
|
||
<div class="endpoint open">
|
||
<div class="endpoint-header" onclick="this.parentElement.classList.toggle('open')">
|
||
<span class="method get">GET</span>
|
||
<span class="endpoint-path">/health</span>
|
||
<span class="endpoint-desc">存活检查</span>
|
||
</div>
|
||
<div class="endpoint-body">
|
||
<p>返回服务健康状态,与 <code>/</code> 相同结构,适合负载均衡探测器。</p>
|
||
<h3>响应</h3>
|
||
<pre>{
|
||
<span class="key">"status"</span>: <span class="string">"ok"</span>,
|
||
<span class="key">"exchange"</span>: <span class="string">"binance"</span>,
|
||
<span class="key">"symbols"</span>: [<span class="string">"BTC/USDT:USDT"</span>, ...],
|
||
<span class="key">"ready"</span>: <span class="bool">true</span>,
|
||
...
|
||
}</pre>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ GET /timeframes ============ -->
|
||
<section id="timeframes">
|
||
<h2>可用周期</h2>
|
||
<div class="endpoint open">
|
||
<div class="endpoint-header" onclick="this.parentElement.classList.toggle('open')">
|
||
<span class="method get">GET</span>
|
||
<span class="endpoint-path">/timeframes</span>
|
||
<span class="endpoint-desc">列出所有时间周期</span>
|
||
</div>
|
||
<div class="endpoint-body">
|
||
<p>返回基础周期(交易所直接拉取)和衍生周期(合成生成)的完整列表。</p>
|
||
<h3>响应</h3>
|
||
<pre>{
|
||
<span class="key">"base_timeframes"</span>: [<span class="string">"1m"</span>, <span class="string">"1h"</span>, <span class="string">"1d"</span>, <span class="string">"1w"</span>],
|
||
<span class="key">"derived_timeframes"</span>: [<span class="string">"5m"</span>, <span class="string">"15m"</span>, <span class="string">"4h"</span>, ...],
|
||
<span class="key">"timeframes"</span>: [<span class="string">"1m"</span>, <span class="string">"1h"</span>, ..., <span class="string">"5m"</span>, <span class="string">"15m"</span>, ...]
|
||
}</pre>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ GET /api/candles ============ -->
|
||
<section id="candles">
|
||
<h2>查询 K 线</h2>
|
||
<div class="endpoint open">
|
||
<div class="endpoint-header" onclick="this.parentElement.classList.toggle('open')">
|
||
<span class="method get">GET</span>
|
||
<span class="endpoint-path">/api/candles</span>
|
||
<span class="endpoint-desc">获取 OHLCV K 线数据</span>
|
||
</div>
|
||
<div class="endpoint-body">
|
||
|
||
<table>
|
||
<tr><th>参数</th><th>类型</th><th>必填</th><th>说明</th></tr>
|
||
<tr>
|
||
<td>symbol</td>
|
||
<td><span class="type">string</span></td>
|
||
<td>是</td>
|
||
<td>交易对,如 <code>BTC/USDT:USDT</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>tf</td>
|
||
<td><span class="type">string</span></td>
|
||
<td>否</td>
|
||
<td>时间周期,默认 <code>1m</code>。支持基础及衍生周期</td>
|
||
</tr>
|
||
<tr>
|
||
<td>start</td>
|
||
<td><span class="type-num">int</span></td>
|
||
<td class="optional">可选</td>
|
||
<td>开始时间戳(毫秒)</td>
|
||
</tr>
|
||
<tr>
|
||
<td>end</td>
|
||
<td><span class="type-num">int</span></td>
|
||
<td class="optional">可选</td>
|
||
<td>结束时间戳(毫秒)</td>
|
||
</tr>
|
||
<tr>
|
||
<td>limit</td>
|
||
<td><span class="type-num">int</span></td>
|
||
<td class="optional">可选</td>
|
||
<td>限制返回的 K 线数量(返回最后 N 根)</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<div class="note">若不传 start/end,返回内存中全部数据(可能很多),建议搭配 limit 使用。</div>
|
||
|
||
<h3>请求示例</h3>
|
||
<pre><span class="comment"># 获取 BTC 最近 100 根 5 分钟 K 线</span>
|
||
GET /api/candles?symbol=BTC/USDT:USDT&tf=5m&limit=100
|
||
|
||
<span class="comment"># 指定时间范围</span>
|
||
GET /api/candles?symbol=ETH/USDT:USDT&tf=1h&start=1704067200000&end=1704153600000
|
||
|
||
<span class="comment"># 获取 4 小时周期(衍生周期)</span>
|
||
GET /api/candles?symbol=SOL/USDT:USDT&tf=4h&limit=50</pre>
|
||
|
||
<h3>响应</h3>
|
||
<p>返回 OHLCV 对象数组:</p>
|
||
<pre>[
|
||
{
|
||
<span class="key">"timestamp"</span>: <span class="num">1704067200000</span>,
|
||
<span class="key">"datetime"</span>: <span class="string">"2024-01-01T00:00:00Z"</span>,
|
||
<span class="key">"open"</span>: <span class="num">42850.12</span>,
|
||
<span class="key">"high"</span>: <span class="num">43100.00</span>,
|
||
<span class="key">"low"</span>: <span class="num">42780.50</span>,
|
||
<span class="key">"close"</span>: <span class="num">43050.80</span>,
|
||
<span class="key">"volume"</span>: <span class="num">125.34</span>
|
||
},
|
||
...
|
||
]</pre>
|
||
|
||
<h3>字段说明</h3>
|
||
<table>
|
||
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
|
||
<tr><td>timestamp</td><td><span class="type-num">int</span></td><td>UTC 毫秒时间戳</td></tr>
|
||
<tr><td>datetime</td><td><span class="type">string</span></td><td>ISO 8601 格式(末尾 Z)</td></tr>
|
||
<tr><td>open</td><td><span class="type-num">float</span></td><td>开盘价</td></tr>
|
||
<tr><td>high</td><td><span class="type-num">float</span></td><td>最高价</td></tr>
|
||
<tr><td>low</td><td><span class="type-num">float</span></td><td>最低价</td></tr>
|
||
<tr><td>close</td><td><span class="type-num">float</span></td><td>收盘价</td></tr>
|
||
<tr><td>volume</td><td><span class="type-num">float</span></td><td>成交量</td></tr>
|
||
</table>
|
||
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ WebSocket ============ -->
|
||
<section id="websocket">
|
||
<h2>WebSocket 实时推送</h2>
|
||
<div class="endpoint open">
|
||
<div class="endpoint-header" onclick="this.parentElement.classList.toggle('open')">
|
||
<span class="method ws">WS</span>
|
||
<span class="endpoint-path">/ws</span>
|
||
<span class="endpoint-desc">实时 K 线订阅</span>
|
||
</div>
|
||
<div class="endpoint-body">
|
||
|
||
<p>连接 WebSocket 后,通过 JSON 消息进行订阅管理。服务端在数据更新时主动推送最新 K 线。</p>
|
||
|
||
<h3>客户端 → 服务端</h3>
|
||
|
||
<div class="ws-box">
|
||
<div class="label">订阅 K 线</div>
|
||
<pre>{
|
||
<span class="key">"action"</span>: <span class="string">"subscribe"</span>,
|
||
<span class="key">"symbol"</span>: <span class="string">"BTC/USDT:USDT"</span>,
|
||
<span class="key">"timeframe"</span>: <span class="string">"1m"</span>
|
||
}</pre>
|
||
</div>
|
||
|
||
<div class="ws-box">
|
||
<div class="label">取消订阅</div>
|
||
<pre>{
|
||
<span class="key">"action"</span>: <span class="string">"unsubscribe"</span>,
|
||
<span class="key">"symbol"</span>: <span class="string">"BTC/USDT:USDT"</span>,
|
||
<span class="key">"timeframe"</span>: <span class="string">"1m"</span>
|
||
}</pre>
|
||
</div>
|
||
|
||
<div class="ws-box">
|
||
<div class="label">心跳 Ping</div>
|
||
<pre>{ <span class="key">"action"</span>: <span class="string">"ping"</span> }</pre>
|
||
</div>
|
||
|
||
<h3>服务端 → 客户端</h3>
|
||
|
||
<div class="ws-box">
|
||
<div class="label">订阅确认</div>
|
||
<pre>{
|
||
<span class="key">"type"</span>: <span class="string">"subscribed"</span>,
|
||
<span class="key">"symbol"</span>: <span class="string">"BTC/USDT:USDT"</span>,
|
||
<span class="key">"timeframe"</span>: <span class="string">"1m"</span>
|
||
}</pre>
|
||
</div>
|
||
|
||
<div class="ws-box">
|
||
<div class="label">初始快照(订阅后立即推送最近 500 根 K 线)</div>
|
||
<pre>{
|
||
<span class="key">"type"</span>: <span class="string">"snapshot"</span>,
|
||
<span class="key">"symbol"</span>: <span class="string">"BTC/USDT:USDT"</span>,
|
||
<span class="key">"timeframe"</span>: <span class="string">"1m"</span>,
|
||
<span class="key">"data"</span>: [ ... ]
|
||
}</pre>
|
||
</div>
|
||
|
||
<div class="ws-box">
|
||
<div class="label">K 线更新(增量推送最近 2 根)</div>
|
||
<pre>{
|
||
<span class="key">"type"</span>: <span class="string">"kline"</span>,
|
||
<span class="key">"symbol"</span>: <span class="string">"BTC/USDT:USDT"</span>,
|
||
<span class="key">"timeframe"</span>: <span class="string">"1m"</span>,
|
||
<span class="key">"data"</span>: [ ... ]
|
||
}</pre>
|
||
</div>
|
||
|
||
<div class="ws-box">
|
||
<div class="label">Pong 响应</div>
|
||
<pre>{ <span class="key">"type"</span>: <span class="string">"pong"</span> }</pre>
|
||
</div>
|
||
|
||
<div class="ws-box">
|
||
<div class="label">错误消息</div>
|
||
<pre>{ <span class="key">"type"</span>: <span class="string">"error"</span>, <span class="key">"message"</span>: <span class="string">"..."</span> }</pre>
|
||
</div>
|
||
|
||
<h3>JavaScript 示例</h3>
|
||
<pre><span class="comment">// 连接</span>
|
||
<span class="key">const</span> ws = <span class="string">new WebSocket("ws://localhost:9009/ws")</span>;
|
||
|
||
ws.<span class="key">onopen</span> = () => {
|
||
<span class="comment">// 订阅 BTC 1m K 线</span>
|
||
ws.send(JSON.stringify({
|
||
action: <span class="string">"subscribe"</span>,
|
||
symbol: <span class="string">"BTC/USDT:USDT"</span>,
|
||
timeframe: <span class="string">"1m"</span>
|
||
}));
|
||
};
|
||
|
||
ws.<span class="key">onmessage</span> = (event) => {
|
||
<span class="key">const</span> msg = JSON.parse(event.data);
|
||
<span class="key">if</span> (msg.type === <span class="string">"kline"</span>) {
|
||
console.log(msg.data); <span class="comment">// 最新 K 线数组</span>
|
||
}
|
||
};</pre>
|
||
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ 时间周期参考 ============ -->
|
||
<section id="timeframes-ref">
|
||
<h2>时间周期参考</h2>
|
||
<p>以下是完整的周期对照表:</p>
|
||
|
||
<table>
|
||
<tr><th>基础周期</th><th>合成衍生周期</th></tr>
|
||
<tr><td><code>1m</code></td><td><code>2m, 3m, 4m, 5m, 10m, 15m, 20m, 25m, 30m, 45m</code></td></tr>
|
||
<tr><td><code>1h</code></td><td><code>2h, 3h, 4h, 5h, 6h, 7h, 8h, 9h, 10h, 11h, 12h, 16h, 20h</code></td></tr>
|
||
<tr><td><code>1d</code></td><td><code>2d, 3d, 4d, 5d, 6d</code></td></tr>
|
||
<tr><td><code>1w</code></td><td><code>2w, 3w</code></td></tr>
|
||
</table>
|
||
|
||
<p>衍生周期由对应基础周期的 K 线通过 OHLCV 聚合合成,查询方式与基础周期完全一致。</p>
|
||
</section>
|
||
|
||
<footer>
|
||
Chan Data Provider — Built with FastAPI + ccxt + pandas
|
||
</footer>
|
||
|
||
</div>
|
||
|
||
<script>
|
||
<span class="comment">// 展开/折叠端点详情</span>
|
||
document.querySelectorAll('.endpoint-header').forEach(el => {
|
||
el.addEventListener('click', () => {
|
||
el.parentElement.classList.toggle('open');
|
||
});
|
||
});
|
||
<span class="comment">// 默认展开所有端点</span>
|
||
document.querySelectorAll('.endpoint').forEach(el => el.classList.add('open'));
|
||
</script>
|
||
</body>
|
||
</html>
|