练手盘机器人接入说明
面向站外开发者。读完本文即可实现一个会自动下单的机器人,无需阅读本站源码。
本文是练手盘接口的唯一对外口径。字段、公式、限频数值、错误码均照实抄录, 若与源码不符以源码为准(但也请提 issue —— 那就是本文过期了)。
0. 一句话说清
没有专用的「机器人接口」。 机器人就是一个普通的 core+ 账号,
走和对人完全一样的 HTTP 接口 —— 和 docs/bot/chat-bot.md、docs/bot/checkin-bot.md
是同一个模型。
你需要的只有两件事:
- 一个 core+ 账号(§2)
- 一条会话 cookie(§2)
三个接口就能跑起来:
| 想做什么 | 接口 | 在哪节 |
|---|---|---|
| 看行情 | GET /api/fish/trade/quote、GET /api/fish/trade/candles |
§3.1 |
| 开仓(买入) | POST /api/fish/trade/buy |
§3.2 |
| 平仓(卖出 / 兑现) | POST /api/fish/trade/sell |
§3.3 |
无需申请 token、无需白名单、无需站长开任何开关。
⚠️ 先读 §4(成交价从哪来)。 那是本文最容易被误读的一节, 也是最容易让一个「看起来对」的策略持续亏钱的一节。
1. 能力边界(先读这段)
练手盘是本站的一个模拟盘:用站内货币「鱼干」按真实行情做多标的, 可以加杠杆,盈亏按行情结算。只做多、不可做空、没有挂单 —— 开仓与平仓都是市价立即成交。
| 范围 | 能读 | 能写 | 说明 |
|---|---|---|---|
| 行情(报价 / K 线) | ✅ | —— | 需 core+ 登录;是展示数据,见 §4 |
| 开仓(买入) | —— | ✅ | 需 core+ 且未被禁言;可加杠杆 |
| 平仓(卖出) | —— | ✅ | 只判 core+(禁言不挡平仓,见 §3.3) |
| 列出自己当前的持仓 | ❌ | —— | 没有这个接口,见 §8.2 |
| 别人的持仓 / 别人的盈亏 | ❌ | ❌ | 练手盘只有统计聚合页,没有逐仓接口 |
| 挂单 / 限价 / 止盈止损 / 做空 | ❌ | ❌ | 都没有,是刻意的:本站只做「市价 + 只做多」 |
| 提现 / 转账 | —— | —— | 那是另一族接口,见 docs/bot/fish-bot.md |
支持的标的(symbol 的取值,区分大小写):
symbol |
页面上的短名 |
|---|---|
BTCUSDT |
BTC |
ETHUSDT |
ETH |
杠杆档位:1(不传就是这个)/ 2 / 3 / 5 / 10 / 20,
另有彩票档 100。白名单之外一律 400(不夹到最近的档位,
见 §6)—— 要 20 倍却静默拿到 10 倍,是一个看不见的错。
⚠️ 机器人享受不到任何豁免:它同样会被禁言(禁言期间开仓返回
403 你已被禁言,暂时无法使用练手盘)、同样受限频约束、同样会被强平。这是刻意设计 —— 出问题时站长能像处理普通用户一样处理它,包括让它手里的会话立即失效 (禁言会作废旧会话,见 §2.1 第 2 点)。
2. 准备账号与鉴权
2.1 账号
练手盘全部接口需要 core+ 且登录。注册出来是 user,要提权一次:
- 注册时带有效邀请码 → 直接是 core(推荐,全自动);
- 让站长在服务器上执行:
npm run cli -- promote-core mybot
完整流程(注册字段、人机验证、错误码)见 docs/bot/chat-bot.md §2。
登录换 cookie(本文也把最小可跑的部分抄在下面,不必跳转):
POST /api/auth/login
Content-Type: application/json
{ "username": "mybot", "password": "********" }
响应头:
Set-Cookie: raricy_session=<JWT>; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000
之后每个请求带上 Cookie: raricy_session=<JWT> 即可。
两个必须处理的点:
- Cookie 30 天过期。 收到
401就重新登录一次。 - 会话会被主动作废。 改密码、被强制下线、被禁言 / 重置密码都会让旧 Cookie
立刻失效(服务端有
session_version快照机制)—— 表现同样是401。 ⚠️ 被降权(core → user)不属于这一类:会话不会失效, 你会拿到403 需要核心用户权限。别把它当会话失效去重登,重登也还是 403。
2.2 三个与「普通接口」不同的规矩
| 规矩 | 说明 |
|---|---|
| 只走 https | 练手盘的页面与四个接口绑在一条协议闸上:明文 http 一律到不了(回环地址除外,那是本地开发的活路)。你的脚本请打 https:// |
| 专注模式会全面封锁 | 账号开了专注模式(focusMode)时,这四个接口全部 403(含只读的行情)。这不是故障 —— 他在设置页一键可关。别自动去关它,那是账号本人的偏好 |
| 禁言只挡开仓 | 见 §3.3 的说明。行情与平仓照常可达 |
📌 CSRF 校验对脚本无影响:本站对写请求校验
Origin/Referer同源, 但两者都缺失时放行,非浏览器客户端天然通过。 所以不要自作主张伪造一个Origin,写错反而会被判定为跨源而403。
3. 接口
所有接口返回外层信封 {"code": 200, "message": "...", ...}(HTTP 状态码与 code 一致)。
写接口是 POST + Content-Type: application/json。
3.1 行情(★ 展示数据,不可用于成交 ★)
GET /api/fish/trade/quote —— 当前报价
GET /api/fish/trade/quote
Cookie: raricy_session=<JWT>
{
"code": 200,
"message": "ok",
"ok": true,
"quotes": [
{
"symbol": "BTCUSDT",
"display": "BTC",
"price": 68412.35,
"change_percent": 1.24,
"stale": false,
"age_ms": 3200,
"source": "stream"
}
],
"fee_rate": 0.0002,
"min_stake": 1
}
| 字段 | 口径 |
|---|---|
ok |
行情到底能不能用。上游拉不到且缓存也空时为 false(那时 quotes 是空数组,price 一定缺席) |
price |
缓存价。服务端每 15 秒向交易所拉一次;age_ms 是它此刻有多旧 |
stale |
这一帧已经陈旧(年龄超过 40 秒 ≈ 漏掉两轮轮询)→ true,「数据可能不是最新的」 |
source |
stream = 常驻行情流那一帧(约 50ms 级);poll = 15 秒的 REST 轮询。仅供排障,别拿它当策略输入 |
fee_rate |
平仓手续费率,0.0002 = 0.02%(只收一次,见 §4.3) |
min_stake |
单笔最少投入(鱼干) |
GET /api/fish/trade/candles —— K 线
GET /api/fish/trade/candles?symbol=BTCUSDT&interval=1h
| 参数 | 取值 | 说明 |
|---|---|---|
symbol |
BTCUSDT / ETHUSDT |
不在名单 → 400 |
interval |
1m 5m 15m 1h 4h 1d |
省略 = 1h;不在白名单 → 400,不退回默认档 |
{
"code": 200, "message": "ok", "ok": true,
"symbol": "BTCUSDT", "display": "BTC", "interval": "1h",
"candles": [
[1759456800000, 68321.5, 68530.0, 68100.2, 68412.35, 128.441]
]
}
一根 K 线是定长六元组,顺序固定:
0 = openTime 1 = open 2 = high 3 = low 4 = close 5 = volume
⚠️ openTime 是交易所给的真实 UTC 毫秒,与 Date.now() 是同一把尺子。
但它不是本站其它接口里那些 created_at / occurred_at 用的钟 —— 后者是
「UTC+8 墙上时间贴 Z 标签」,比标准 UTC 快 8 小时(见 §3.3 的说明)。两者绝不能相减。
最多返回 1000 根(各档覆盖跨度:1m ≈ 16 小时 / 5m ≈ 3.5 天 / 15m ≈ 10 天 /
1h ≈ 41 天 / 4h ≈ 166 天 / 1d ≈ 2.7 年),不做懒加载 —— 拖到最早一根就停住。
📌 两个行情接口都不限频,但别拿它当高频数据源:它们本质是给站内页面用的 缓存展示(
/quote由页面每秒轮询一次,服务端只按 15 秒刷新上游)。 如果你要的是给策略吃的实时行情,直接连交易所的公开行情 —— 那比打我们这层 缓存更快、更细,而且不消耗你和站点的任何配额。练手盘不是行情源。
3.2 开仓 —— POST /api/fish/trade/buy
POST /api/fish/trade/buy
Content-Type: application/json
{
"symbol": "BTCUSDT",
"amount": 10,
"leverage": 5,
"idempotency_key": "mrk-20261003-0001"
}
| 字段 | 规则 |
|---|---|
symbol |
必填,见 §1 的标的表 |
amount |
必填,投入的鱼干数(不是名义本金)。> 0、最多 4 位小数、最少 1 条。可为数字或数字字符串 |
leverage |
可选,不传 = 1。白名单见 §1,否则 400 |
idempotency_key |
可选,强烈建议给。1–48 位,仅 A-Z a-z 0-9 _ . : -。语义见 §7 |
成功:
{
"code": 200,
"message": "已买入 10 条小鱼干的 BTCUSDT",
"position": {
"id": "c1a2b3d4-e5f6-...",
"symbol": "BTCUSDT",
"stake": 10,
"entry_price": 68412.35,
"leverage": 5,
"liquidation_price": 54729.88,
"opened_at": "2026-10-03T14:22:03.000Z"
},
"balance": 32.5,
"replayed": false
}
stake是投入的鱼干;leverage与liquidation_price一定会回给你, 别自己拿开仓价乘一遍 —— 那是开仓那一刻写死的数,与强平引擎用的是同一个;liquidation_price在 1 倍仓上恒为0(意思是「不会爆仓」,不是「价格为 0」);entry_price是这一笔的实际成交价(§4);replayed恒在(新成交为false)—— 不要靠它缺省来判断是不是重放;position.id请自己存下来 —— 平仓要用它,而本站没有「列出我的持仓」的接口(§8.2)。
杠杆仓要求强平引擎活着。 引擎没在跑时,leverage > 1 的开仓返回 503
(1 倍不受影响)。这是刻意的:卖一个兑现不了的产品比不卖更糟 ——
引擎不在时,穿过爆仓价的仓位没人清算,而系统既不报错也不提示。
3.3 平仓 —— POST /api/fish/trade/sell
POST /api/fish/trade/sell
Content-Type: application/json
{ "position_id": "c1a2b3d4-e5f6-..." }
整仓平仓,没有部分平仓。 没有幂等键 —— 平仓天生幂等:仓位一旦结清, 再平就是重放(服务端回读当初结算的数额原样回报,不动钱),所以这里比 §3.2 少一个参数。
成功(承接 §3.2 那笔:投入 10、5 倍杠杆):
{
"code": 200,
"message": "已卖出 BTCUSDT,+1.2381 条小鱼干",
"position_id": "c1a2b3d4-e5f6-...",
"symbol": "BTCUSDT",
"payout": 11.2381,
"profit": 1.2381,
"exit_price": 70120.5,
"liquidated": false,
"balance": 43.7381,
"replayed": false
}
(开仓价 68412.35 → 平仓价 70120.5 是 +2.5%,5 倍杠杆把毛收益放大到 +12.5%, 再扣掉按名义本金收的手续费,实发 11.2381、净 +12.38% —— 算式见 §4.3。)
四种结果都要能处理,它们都是 code: 200:
| 场景 | 特征 | message 大意 |
|---|---|---|
| 正常平仓 | profit 可正可负 |
已卖出 …,+12.4 条小鱼干 |
| 早就爆仓了 | liquidated: true,payout 为 0 |
该仓位已爆仓(保证金归零,强平已结清) |
| 重复提交 | replayed: true |
该仓位已经卖过了(重复请求,未重复结算) |
| 盈利时重放 | replayed: true |
—— |
⚠️
liquidated那一档别并进「卖出成功」里。 用户(以及你的策略) 可能是「看到跌穿了、点卖出」才发现仓位早就被强平了 —— 这时回一句「已卖出,-100 条小鱼干」会让人以为是自己卖掉的。 判据就是liquidated字段,不是profit < 0。
禁言不挡平仓(与开仓刻意不同)。禁言是「不能说话」,不该顺带变成「不能止损」—— 被禁言期间行情照走,若这里也挡,用户手上已经开着的仓位就一股也卖不掉, 只能看着浮亏扩大且没有自救手段(禁言还会作废旧会话,重新登录也一样)。 所以这张表是不对称的:
| 接口 | 判 core+ | 判禁言 | 判专注模式 |
|---|---|---|---|
buy |
✅ | ✅ | ✅ |
sell |
✅ | ❌ | ✅ |
quote / candles |
✅ | ❌ | ✅ |
⚠️ 专注模式不做这种不对称:开启者在四个接口上一律拿不到东西。 别照着「禁言那栏有空」把它也放过 —— 理由是专注本人一键可关,不会把人困在仓位里。
4. 成交价从哪来(★ 读两遍 ★)
4.1 成交价与展示价是两回事
GET /quote 给的 price 是缓存展示价(最多 15 秒旧)。
开仓与平仓的成交价不是它 —— 服务端在下单那一刻重新向交易所现取一个价,
用那个价成交。
下单时你不需要(也不能)传价格。 请求体里没有价格字段,价格完全由服务端定。
4.2 这对策略意味着什么
- 不要用
quote的价当作「我会成交在多少」。你看到 A 就下单, 成交可能在 A 上下任意一跳,而这不是可以套利的时差 —— 服务端用的是下单那一刻的实时价,缓存刷新得慢不影响成交价。(这正是设计。) - 你在响应里拿到的
entry_price/exit_price才是真实成交价, 回测与记账都要用它。 - 想减少滑点带来的意外,就别在剧烈波动时下单 —— 但不存在 「按展示价成交」这个选项,接口不会为了任何调用方开这个口子。
4.3 结算公式
平仓的实发金额按下面这个式子算(它同时是页面上「预计到手」用的那一个):
ratio = 平仓成交价 / 开仓成交价
notional = 投入 × 杠杆 ← 名义本金
毛额 = 投入 + notional × (ratio − 1)
手续费 = notional × ratio × fee_rate (fee_rate = 0.0002)
实发 payout = max(0, floor(毛额 − 手续费))
盈亏 profit = 实发 − 投入
四条要从式子里读出来的事:
- 手续费按名义本金收,不是按投入收 —— 所以杠杆越高,费率感受越重: 1 倍约 0.02%、10 倍约 0.2%、100 倍约 2%(一次平仓)。
- 开仓免费、持有免费 —— 只有平仓收这一道。
- 实发下限是 0,所以亏损最多亏掉投入,不会变成欠款。
⚠️ 爆仓与手动平仓走的是同一个
max(0, …)—— 两条路给你的数一样。 别指望「不平仓、等它爆」能留下什么。 - 金额最少刻度是 0.0001 鱼干,实发向下取整。
爆仓价 = 开仓价 × (1 − 1/杠杆)(1 倍恒为 0,即不会爆)。
现价触及它时仓位会被后台强平引擎结清(不需要你做任何事,也不需要你再调 sell)——
结算用的是那个爆仓价本身,不是触发时的现价,所以「什么时候被发现」不影响
「结算成多少」。
5. 限频
| 维度 | 配额 | 说明 |
|---|---|---|
| 下单(开仓 + 平仓)· 每账号 | 20 次 / 分钟 | 开仓与平仓共用同一个桶 |
| 下单(开仓 + 平仓)· 每账号 | 300 次 / 24 小时 | 同上 |
行情 quote / candles |
不限频 | 见 §3.1 末尾的提醒:它们是给页面用的缓存 |
三条纪律:
- 重放不消耗额度。 带着同一个
idempotency_key重试、或平一个已经结清的仓位, 服务端在校验/重放这一步就回报了,不会扣你的分钟/日额度。 - 参数校验失败也不消耗额度(金额非法、标的非法、杠杆非法、幂等键格式非法都在 计数之前被拒)。余额不足会消耗(它在计数之后才判)。
- 超限一律
429。20 次/分钟是一个偏低的上限 —— 它是按「人手点」的量级定的。 如果你的策略需要更高频,先找站长,别靠while(true)硬撞。
⚠️ 这两条配额是练手盘唯一的频率约束,且按账号计。 一个账号撞满了, 不影响另一个账号。但同一个账号所有开仓与平仓都从这一个桶里扣。
6. 错误码
| HTTP | 何时 | 机器人该怎么做 |
|---|---|---|
| 400 | 标的非法 / 金额非法或超 4 位小数 / 少于 1 条 / 杠杆不在白名单 / 幂等键格式非法 / 缺少 position_id / 小鱼干不足 |
改参数,别重试 |
| 401 | 没有会话,或会话已失效 | 重新登录一次 |
| 403 | 不是 core+(需要核心用户权限)/ 被禁言(开仓)/ 开了专注模式 / 协议闸(打了 http) |
停手或改协议,别重试 |
| 404 | position_id 不存在或不是你的仓位 |
检查 id。⚠️ 不是自己的仓位一律 404 而非 403 —— 那等于确认「这个 id 存在」 |
| 429 | 见 §5 | 退避;不要立刻重试 |
| 500 | 本站服务端故障 | 别当成「这笔没发生」 —— 用 §7 的键重发,或先查流水确认 |
| 503 | 行情源当前不可用(开仓 / 平仓拒单);或强平引擎未运行(杠杆开仓) | 稍后再试。这一档绝不含糊成交:取不到价就拒单,不降级用缓存价 |
报错文案会念出可选项:杠杆非法时文案里带着完整白名单,别去猜。
7. 幂等与重试(读两遍)
7.1 开仓:带 idempotency_key
开仓不会凭参数去重 —— 同一账号同一标的同一金额买两次是完全正常的操作 (分批建仓)。所以服务端不能猜,必须由你给键。
| 再发一次同样的请求 | 结果 |
|---|---|
| 键相同 | 返回原结果(replayed: true),不再取价、不再扣款、不消耗额度 |
| 键不同 | 那就是新的一笔 |
所以:超时、断连、5xx 之后,直接用同一个键重发,这是安全的。
键的生成纪律(与 docs/bot/fish-bot.md §6 同源):
- 1–48 位,仅
A-Z a-z 0-9 _ . : -(例如mrk-20261003-0001); - 键要跟着「这笔业务」走,而不是跟着「这次请求」走 —— 你的策略决定「开这一仓」时就定下键,之后所有重试都用它;
- 不要用「时间戳」这种每次都不同的东西当键 —— 那就等于没带;
- 键只在你自己账号的命名空间里唯一:两个账号用同一个字符串是安全的, 服务端会把身份混进键里(否则甲买过之后,乙用同一个键会被当成重放、拿到甲那笔的结果)。
⚠️ 不带键时,服务端会随机生成一个。此时每次成功返回 200 = 一笔真实成交, 而超时的请求可能已经成交、你的重发就是第二笔。这条只能由调用方保证。
7.2 平仓:不需要键
平仓天然幂等(§3.3):仓位结清后再平是重放,回读既有结果。重复提交不会多发钱。
7.3 一个重试模板
开仓 / 平仓超时:
→ 用同一把键重发,直到拿到确定的 200/4xx(429 要退避,403/401 要停手或重登)
→ 若始终拿不到(本站长时间 5xx):
用 GET /api/fish/balance?type=market_all 查流水确认(见 §8.1 的注意事项),
或直接凭 position_id 调 sell —— 已结清会走重放,安全
8. 两个静默陷阱
8.1 流水不是练手盘的交易记录
你要查历史交易,可以走 GET /api/fish/balance?type=market_all(或
docs/bot/fish-bot.md §3.3 那两条带 market_all 筛选的口)。
⚠️ 但不要用流水去复算盈亏或交易笔数。 有两种交易根本不写流水: 被强平的仓位(保证金归零),以及实发为
0的正常平仓。按流水求和会让你 的盈亏偏乐观、笔数与胜率偏小 —— 而这一切不报任何错。盈亏的唯一可靠来源是 §3.2 / §3.3 响应里的
entry_price/exit_price/stake/payout/profit。 请把它们记到你自己的库里。
8.2 没有「列出我的持仓」接口
本站不提供返回当前持仓列表的 JSON 接口。开仓响应里的 position.id 是
你唯一能拿到 id 的地方(除非你自己存)。这意味着:
- 请把
position.id持久化到自己的存储里,否则进程重启后你就平不掉那些仓了; - 万一丢了 id,站内页面
/fish/trade能看到你持有的仓位(页面会列出它们), 但那是给人看的 HTML,不是接口; - 后台的强平引擎不需要你提供 id —— 现价触及爆仓价时它会自己结清, 所以你「找不到 id」的杠杆仓不会被永远晾着,但普通仓不会自动平,会一直开着。
9. 一个完整的例子
BASE=https://raricy.com
USER=mybot
PASS=$BOT_PASSWORD # 环境变量,别写死在脚本里
JAR=$(mktemp) # cookie jar
# 0. 登录换 cookie
curl -s -c "$JAR" -X POST "$BASE/api/auth/login" \
-H 'Content-Type: application/json' \
-d "{\"username\":\"$USER\",\"password\":\"$PASS\"}"
# 1. 看看行情(展示数据;成交价以响应里的 entry_price 为准)
curl -s -b "$JAR" "$BASE/api/fish/trade/quote"
# 2. 开仓:投入 10 条鱼干、5 倍杠杆、带上幂等键
curl -s -b "$JAR" -X POST "$BASE/api/fish/trade/buy" \
-H 'Content-Type: application/json' \
-d '{"symbol":"BTCUSDT","amount":10,"leverage":5,
"idempotency_key":"mrk-20261003-0001"}'
# → 记下返回的 position.id
# 3. 平仓(把 <position_id> 换成上一步的值)
curl -s -b "$JAR" -X POST "$BASE/api/fish/trade/sell" \
-H 'Content-Type: application/json' \
-d '{"position_id":"<position_id>"}'
# 4. 看余额与练手盘流水(注意 §8.1:别用流水复算盈亏)
curl -s -b "$JAR" "$BASE/api/fish/balance?type=market_all"
📌 本站的时间戳格式:
opened_at等字段是标准 ISO-8601(真 UTC)。 而fish流水里的created_at是「UTC+8 墙上时间贴Z标签」的假 Z, 比标准 UTC 快 8 小时 —— 两者别混用。完整说明见docs/bot/fish-bot.md§3.3。 K 线的openTime又是第三把钟(交易所给的真 UTC 毫秒),见 §3.1。