文档索引

练手盘机器人接入说明

面向站外开发者。读完本文即可实现一个会自动下单的机器人,无需阅读本站源码。

本文是练手盘接口的唯一对外口径。字段、公式、限频数值、错误码均照实抄录, 若与源码不符以源码为准(但也请提 issue —— 那就是本文过期了)。

0. 一句话说清

没有专用的「机器人接口」。 机器人就是一个普通的 core+ 账号, 走和对人完全一样的 HTTP 接口 —— 和 docs/bot/chat-bot.md、docs/bot/checkin-bot.md 是同一个模型。

你需要的只有两件事:

  1. 一个 core+ 账号(§2)
  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> 即可。

两个必须处理的点:

  1. Cookie 30 天过期。 收到 401 就重新登录一次。
  2. 会话会被主动作废。 改密码、被强制下线、被禁言 / 重置密码都会让旧 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. 手续费按名义本金收,不是按投入收 —— 所以杠杆越高,费率感受越重: 1 倍约 0.02%、10 倍约 0.2%、100 倍约 2%(一次平仓)。
  2. 开仓免费、持有免费 —— 只有平仓收这一道。
  3. 实发下限是 0,所以亏损最多亏掉投入,不会变成欠款。 ⚠️ 爆仓与手动平仓走的是同一个 max(0, …) —— 两条路给你的数一样。 别指望「不平仓、等它爆」能留下什么。
  4. 金额最少刻度是 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。

本页内容来自仓库 docs/bot/trade-bot.md·在 GitHub 上查看