快速上手(Python / JavaScript)
接入现货 API 第一小时会用到的可运行代码:给请求签名、读取账户、挂一笔远离盘口的订单再撤销、订阅盘口、检查成交在 Canton 上的结算状态。下面每段代码都取自开源 starter 仓库,仓库里还有完整的双边报价器和对账任务:
Starter 仓库: Rocky-DEX/Rocky-API-Maker-Bot —— Python 包
rocky_spot和一个零依赖的 TypeScript 客户端,Apache-2.0 协议。克隆后复制.env.example,运行examples/quickstart即可。
前提:一把 API key(HMAC 或 ED25519,见 鉴权)——连接钱包后在网页端 API Keys 页面自助创建,或调用 /v1/api-keys;Python 3.10+ 加 requests(Ed25519 需 pynacl),或 Node 22+(无依赖)。
一、给请求签名
网关校验的是最后一个 &signature= 之前的原始字节。把 query string 构造一次、签名、追加签名后原样发送——所有方法都是这 样,包括 POST /api/v3/order。请求体保持为空。
import hashlib, hmac, time, urllib.parse, requests
BASE, KEY, SECRET = "https://api.rocky.exchange", "your_key", "your_secret"
def signed_query(params: dict) -> str:
params = {k: v for k, v in params.items() if v is not None}
params["timestamp"] = int(time.time() * 1000)
payload = urllib.parse.urlencode(params)
sig = hmac.new(SECRET.encode(), payload.encode(), hashlib.sha256).hexdigest()
return f"{payload}&signature={sig}"
def signed(method: str, path: str, params: dict = {}):
r = requests.request(method, f"{BASE}{path}?{signed_query(params)}", headers={"X-MBX-APIKEY": KEY})
body = r.json()
if r.status_code >= 400:
raise RuntimeError(f"{r.status_code} {body}") # {"code": -2011, "msg": "..."}
return body
import { createHmac } from "node:crypto";
const BASE = "https://api.rocky.exchange", KEY = "your_key", SECRET = "your_secret";
function signedQuery(params: Record<string, string | number | undefined>): string {
const q = new URLSearchParams();
for (const [k, v] of Object.entries(params)) if (v !== undefined) q.append(k, String(v));
q.append("timestamp", String(Date.now()));
const payload = q.toString();
const sig = createHmac("sha256", SECRET).update(payload).digest("hex");
return `${payload}&signature=${sig}`;
}
async function signed<T>(method: string, path: string, params: Record<string, string | number | undefined> = {}): Promise<T> {
const r = await fetch(`${BASE}${path}?${signedQuery(params)}`, { method, headers: { "X-MBX-APIKEY": KEY } });
const body = await r.json();
if (!r.ok) throw new Error(`${r.status} ${JSON.stringify(body)}`);
return body as T;
}
Ed25519 key 签同样的 payload,签名以 base64 而不是 hex 发送;见 参考向量。starter 的 signing 模块两种都支持,并用公开向量自检。
二、账户与分段
acct = signed("GET", "/api/v3/account")
print(acct["settlement"]) # {"segment": "wallet", "enrolled": true, "frozen": false, "party": "..."}
print({b["asset"]: b["free"] for b in acct["balances"] if b["free"] != "0"})
const acct = await signed<{ canTrade: boolean; settlement: { segment: string; frozen: boolean }; balances: { asset: string; free: string }[] }>("GET", "/api/v3/account");
console.log(acct.settlement, acct.balances.filter((b) => b.free !== "0"));
绑定 party 且处于链上分段的账户 settlement.segment 为 wallet:余额是你 Canton 钱包的镜像,镜像对账冻结账户时 canTrade 变为 false。见 账户信息。
三、先读过滤器,再挂单
从 exchangeInfo 读取 tickSize、stepSize、minQty、minNotional,发送前先量化;价格不在 tick 上或名义金额低于下限会被拒绝(-1100 / -1013)。
from decimal import Decimal, ROUND_DOWN, ROUND_UP
info = requests.get(f"{BASE}/api/v3/exchangeInfo").json()
sym = next(s for s in info["symbols"] if s["symbol"] == "CBTC-USDCB")
f = {k: v for flt in sym["filters"] for k, v in flt.items() if k != "filterType"}
tick, step, min_notional = Decimal(f["tickSize"]), Decimal(f["stepSize"]), Decimal(f["minNotional"])
book = requests.get(f"{BASE}/api/v3/depth", params={"symbol": "CBTC-USDCB", "limit": 5}).json()
best_bid = Decimal(book["bids"][0][0])
price = (best_bid * Decimal("0.8") / tick).quantize(Decimal(1), rounding=ROUND_DOWN) * tick # 低于盘口 20%:会挂着
qty = (min_notional * Decimal("1.05") / price / step).quantize(Decimal(1), rounding=ROUND_UP) * step
ack = signed("POST", "/api/v3/order", {"symbol": "CBTC-USDCB", "side": "BUY", "type": "LIMIT",
"quantity": str(qty), "price": str(price), "newClientOrderId": "qs-1"})
print(ack["orderId"], ack["status"]) # NEW;受理响应从不包含成交
const info = await (await fetch(`${BASE}/api/v3/exchangeInfo`)).json();
const sym = info.symbols.find((s: any) => s.symbol === "CBTC-USDCB");
const f: Record<string, string> = Object.assign({}, ...sym.filters.map(({ filterType, ...rest }: any) => rest));
const tick = Number(f.tickSize), step = Number(f.stepSize), dp = (s: string) => (s.split(".")[1] ?? "").replace(/0+$/, "").length;
const book = await (await fetch(`${BASE}/api/v3/depth?symbol=CBTC-USDCB&limit=5`)).json();
const price = (Math.floor((Number(book.bids[0][0]) * 0.8) / tick) * tick).toFixed(dp(f.tickSize));
const qty = (Math.ceil((Number(f.minNotional) * 1.05) / Number(price) / step) * step).toFixed(dp(f.stepSize));
const ack = await signed<{ orderId: string; status: string }>("POST", "/api/v3/order",
{ symbol: "CBTC-USDCB", side: "BUY", type: "LIMIT", quantity: qty, price, newClientOrderId: "qs-1" });
console.log(ack.orderId, ack.status);
newClientOrderId 让重试的下单在 15 分钟内幂等;它不会存储在订单上。
四、撤一单,撤全部
signed("DELETE", "/api/v3/order", {"symbol": "CBTC-USDCB", "orderId": ack["orderId"]}) # 已不存在则 -2011
cancelled = signed("DELETE", "/api/v3/openOrders", {"symbol": "CBTC-USDCB"}) # kill switch:权重 1,不计入订单桶
print(len(cancelled), "orders cancelled")
await signed("DELETE", "/api/v3/order", { symbol: "CBTC-USDCB", orderId: ack.orderId });
const cancelled = await signed<unknown[]>("DELETE", "/api/v3/openOrders", { symbol: "CBTC-USDCB" });
console.log(cancelled.length, "orders cancelled");
把 DELETE /api/v3/openOrders 接到策略的每条失败路径上(行情丢失、异常、SIGTERM)。见 撤销全部挂单。
五、成交与链上结算
for t in signed("GET", "/api/v3/myTrades", {"symbol": "CBTC-USDCB", "limit": 20}):
print(t["orderId"], t["qty"], t["price"], t["settlementStatus"], t["cantonUpdateId"])
summary = signed("GET", "/api/v3/settlements/summary")["total"]
print(summary["counts"], "unsettled quote qty:", summary["unsettledQuoteQty"])
for row in signed("GET", "/api/v3/settlements", {"status": "failed", "limit": 50}):
print("FAILED", row["tradeId"], row["lastError"]) # 运维已收到告警;不要重复报价这部分库存
for (const t of await signed<any[]>("GET", "/api/v3/myTrades", { symbol: "CBTC-USDCB", limit: 20 }))
console.log(t.orderId, t.qty, t.price, t.settlementStatus, t.cantonUpdateId);
const { total } = await signed<{ total: { counts: Record<string, number>; unsettledQuoteQty: string } }>("GET", "/api/v3/settlements/summary");
console.log(total.counts, "unsettled quote qty:", total.unsettledQuoteQty);
每笔成交的 orderId 能对回被吃的那档报价。成交通常在数秒内经历 pending → processing → confirmed;confirmed 带有可在 Canton 上核验的 cantonUpdateId。钱包分段上的每笔成交都是一笔由你的 validator 确认并支付流量的 DvP——见 外部做市商指南 和 结算记录。
六、WebSocket 行情
import asyncio, json, websockets
async def main():
url = "wss://api.rocky.exchange/stream?streams=cbtc-usdcb@depth5/cbtc-usdcb@trade"
async with websockets.connect(url, ping_interval=None) as ws: # 网关每 20 秒 ping 一次
async for raw in ws:
msg = json.loads(raw)
if msg["stream"].endswith("@depth5"):
bids, asks = msg["data"]["bids"], msg["data"]["asks"]
print("mid", (float(bids[0][0]) + float(asks[0][0])) / 2)
else:
print("trade", msg["data"]["p"], msg["data"]["q"], "maker-sold" if msg["data"]["m"] else "maker-bought")
asyncio.run(main())
const ws = new WebSocket("wss://api.rocky.exchange/stream?streams=cbtc-usdcb@depth5/cbtc-usdcb@trade");
ws.onmessage = (ev) => {
const { stream, data } = JSON.parse(String(ev.data));
if (stream.endsWith("@depth5")) console.log("mid", (Number(data.bids[0][0]) + Number(data.asks[0][0])) / 2);
else console.log("trade", data.p, data.q, data.m ? "maker-sold" : "maker-bought");
};
ws.onclose = () => console.log("reconnect here");
流都是公开的;没有用户数据流,自己的状态请轮询 openOrders(权重 1)和 myTrades / settlements(权重 10)。见 WebSocket 流。
七、限流响应头
每个签名响应都带 x-mbx-used-weight-1m 和 x-mbx-order-count-1m。读取它们并在网关返回 429 -1003 之前主动退避;starter 客户端在每次调用后把它们暴露为属性。外部做市商使用 ext-mm 档位:每分钟 2400 权重、每 10 秒 100 单、每分钟 600 单;撤单免费。见 限流。