概念介绍
本节介绍接入 Outcomes API 前需要理解的核心业务概念,包括 xp(基础资产)、事件、市场、结果、价格、Split / Merge、镜像订单簿、订单簿、持仓、结算以及关键 ID。
理解这些概念后,开发者可以更清楚地判断:
- 如何从事件找到可交易市场
- 如何选择 YES / NO outcome
- 为什么下单、撤单、查询行情时通常使用
assetId - Split / Merge 与镜像订单簿之间的关系
- Binary Market 与 NegRisk Market 的差异
1. 预测市场
预测市场是一种围绕未来事件结果进行交易的市场。
每个市场通常对应一个明确的问题,例如:
Germany 在某场比赛会赢吗?
用户可以根据自己的判断交易该问题下的不同结果。最常见的结果是 YES 和 NO。
示例:
| 对象 | 示例 |
|---|---|
| Market | Germany 会赢吗? |
| YES | Germany 会赢 |
| NO | Germany 不会赢 |
可以简单理解为:
| 交易方向 | 含义 |
|---|---|
| 买入 YES | 用户认为该事件结果会发生 |
| 买入 NO | 用户认为该事件结果不会发生 |
2. 核心对象模型
Outcomes API 中的核心对象关系如下:
- Event
- Market
- YES outcome
- NO outcome
可以简单理解为:
| 对象 | 含义 |
|---|---|
| Event | 真实世界事件 |
| Market | Event 下的具体交易问题 |
| Outcome | Market 下可以交易的结果,例如 YES / NO |
| assetId | 某个 outcome 的可交易资产 ID |
开发者接入时,通常遵循下面的链路:
- 查询
Event - 获取该
Event下的Market - 选择某个
Market下的YES或NOoutcome - 使用该 outcome 对应的
assetId进行交易
3. Event 与 Market
事件(Event)是预测市场的上层组织单位,表示一个真实世界事件。
示例:
| 对象 | 示例 |
|---|---|
| Event | Germany vs. Curacao |
一个 Event 下可以包含多个 Market:
| Market | 问题 |
|---|---|
| Market 1 | Germany 会赢吗? |
| Market 2 | 两队会平局吗? |
| Market 3 | Curacao 会赢吗? |
用户实际交易的不是 Event 本身,而是某个 Market 下的 YES 或 NO outcome。
4. 结果 Outcome
结果(Outcome)是 Market 下的可交易对象。
在二元市场中,每个 Market 通常有两个 outcome:
| Outcome | 含义 |
|---|---|
| YES | 该 Market 的结果会发生 |
| NO | 该 Market 的结果不会发生 |
每个 outcome 都会有自己的 assetId。开发者下单时,最终使用的是目标 outcome 对应的 assetId。
示例:
marketId:100001question:Germany 会赢吗?yesOutcome:assetId=100049000,price=0.65noOutcome:assetId=100049001,price=0.35
如果用户想买入 YES,则使用 yesOutcome.assetId。
如果用户想买入 NO,则使用 noOutcome.assetId。
5. 价格与概率
预测市场中的价格是 0 到 1 之间的小数。价格可以理解为市场对某个结果发生概率的实时估计。
例如:
| Outcome | 当前价格 | 可以理解为 |
|---|---|---|
| YES | 0.65 |
市场当前认为 YES 发生的概率约为 65% |
| NO | 0.35 |
市场当前认为 NO 发生的概率约为 35% |
为什么价格≈概率:胜出的 outcome 结算为 1 xp、未胜出结算为 0 xp,因此持有一个 outcome 的期望价值 = 1 xp × 该结果发生的概率。在有效市场中,价格会趋近这一期望值,所以价格可以近似看作市场对该结果发生概率的估计。
价格由市场交易形成,不代表最终结果。最终结果以市场结算为准。
6. YES / NO 的互补关系
YES 和 NO 是同一个 Binary Market 的两面。一个结果发生时,另一个结果就不会发生。
在理想情况下,二者价格之和接近 1。
| Outcome | 价格 |
|---|---|
| YES | 0.65 |
| NO | 0.35 |
| YES + NO | 1.00 |
这种关系可以概括为:
YES + NO ≈ 1
开发者不需要手动处理 YES / NO 的价格换算。下单时只需要选择目标 outcome,并使用该 outcome 对应的 assetId。
7. Split / Merge 机制
Split / Merge 是 YES / NO 互补关系背后的基础机制。
对于一个 Binary Market,YES 和 NO 可以看作一组成对的条件结果。系统可以通过 Split 和 Merge 在 xp 与 YES / NO outcome 之间进行转换。
7.1 Split
Split 是将一份 xp 拆分为等量的 YES 和 NO outcome。
1 xp → 1 YES + 1 NO
可以理解为:用户把一份完整的市场权益拆成了该市场下的两个互补结果。
| 操作前 | 操作后 |
|---|---|
| 1 xp | 1 YES + 1 NO |
7.2 Merge
Merge 是 Split 的逆操作。用户可以将等量的 YES 和 NO outcome 合并回 xp。
1 YES + 1 NO → 1 xp
只有当 YES 和 NO 数量相等时,才可以进行 Merge。
| 操作前 | 操作后 |
|---|---|
| 1 YES + 1 NO | 1 xp |
这个机制保证了 YES 和 NO 的互补关系:
YES + NO ≈ 1
也就是说,在同一个 Binary Market 中,YES 和 NO 不是完全独立的资产,而是同一组条件结果的两面。
8. 镜像订单簿
基于 YES / NO 的互补关系,系统可以使用镜像订单簿统一处理 YES 和 NO 的交易。
对于同一个 Binary Market:
| 用户操作 | 经济效果等价于 |
|---|---|
买入 YES @ 0.60 |
卖出 NO @ 0.40 |
买入 NO @ 0.30 |
卖出 YES @ 0.70 |
原因是:
YES 价格 + NO 价格 ≈ 1
因此,系统可以在底层用一套统一订单簿同时支持 YES 和 NO 的交易。
开发者不需要手动拆分或换算镜像订单。下单时只需要选择用户希望交易的 outcome,并使用该 outcome 对应的 assetId。系统会在订单簿层处理 YES / NO 的等价关系。
9. 订单簿
Outcomes 使用中央限价订单簿,即 CLOB。
CLOB 是 Central Limit Order Book 的缩写,表示中央限价订单簿。市场价格不是平台直接设定的,而是由用户之间的挂单和成交形成。
订单簿包含买卖两侧:
| 方向 | 含义 |
|---|---|
| Bid | 买方愿意买入的价格 |
| Ask | 卖方愿意卖出的价格 |
示例:
| Side | Price | Size |
|---|---|---|
| Ask | 0.67 |
100 |
| Ask | 0.66 |
80 |
| Bid | 0.65 |
120 |
| Bid | 0.64 |
200 |
当买卖双方价格匹配时,订单会发生撮合成交。
10. Maker 与 Taker
订单成交时,用户可能是 Maker,也可能是 Taker。
| 角色 | 含义 |
|---|---|
| Maker | 挂单进入订单簿,为市场提供流动性 |
| Taker | 主动吃掉订单簿中已有挂单,消耗流动性 |
示例:
当前最优卖价 Ask = 0.66。
| 用户操作 | 结果 | 角色 |
|---|---|---|
以 0.66 买入 |
立即成交 | Taker |
以 0.60 挂买单 |
当前无法立即成交,进入订单簿等待成交 | Maker |
订单撮合通常遵循:
| 优先级 | 说明 |
|---|---|
| 价格优先 | 更好的价格优先成交 |
| 时间优先 | 相同价格下,更早进入订单簿的订单优先成交 |
11. 持仓
未成交的挂单会占用可用余额。订单成交后,用户会获得对应 outcome 的持仓。
示例:
| 操作 | 结果 |
|---|---|
| 用户买入 10 个 YES | 成交后,用户持有 10 个 YES outcome |
持仓会随着以下操作发生变化:
| 操作 | 对持仓的影响 |
|---|---|
| 成交 | 增加或减少对应 outcome 持仓 |
| 平仓 | 减少已有 outcome 持仓 |
| Split | 生成 YES / NO outcome |
| Merge | 合并 YES / NO outcome |
| 结算 | 根据最终结果更新持仓和余额 |
12. 平仓
用户不一定要等到市场结算后才退出持仓。如果市场仍可交易,用户可以通过卖出已有 outcome 来平仓。
示例:
| 步骤 | 操作 |
|---|---|
| 第 1 步 | 用户以 0.40 买入 YES |
| 第 2 步 | 之后 YES 价格上涨到 0.70 |
| 第 3 步 | 用户卖出 YES 平仓 |
| 第 4 步 | 用户持仓和余额根据成交结果更新 |
平仓本质上是通过反向交易退出已有持仓。
13. 结算
当事件结果确定后,Market 会进入结算流程。
结算后:
| Outcome | 结算结果 |
|---|---|
| 胜出的 outcome | 结算为 1 xp |
| 未胜出的 outcome | 结算为 0 xp |
示例:
Market:Germany 会赢吗?
如果 Germany 最终赢了:
| Outcome | 结果 |
|---|---|
| YES | 胜出 |
| NO | 未胜出 |
如果 Germany 没有赢:
| Outcome | 结果 |
|---|---|
| YES | 未胜出 |
| NO | 胜出 |
市场结算后,未成交挂单会结束,用户持仓和余额会根据最终结果更新。
14. 二元市场(Binary Market)
二元市场(Binary Market)是最常见的市场结构,表示一个是 / 否问题。
示例:
| Market | Germany 会赢吗? |
|---|---|
| YES | Germany 会赢 |
| NO | Germany 不会赢 |
Binary Market 中通常只有两个 outcome:
YESNO
开发者只需要选择 YES 或 NO 对应的 assetId 进行交易。
15. 多元互斥市场(NegRisk Market)
多元互斥市场(NegRisk Market)用于多个互斥结果的场景。一个 Event 下可以包含多个相关 Market,但最终通常只有一个结果胜出。
示例:
Event:谁会赢得冠军?
| Market | 问题 |
|---|---|
| Market 1 | Germany 会夺冠吗? |
| Market 2 | France 会夺冠吗? |
| Market 3 | Brazil 会夺冠吗? |
| Market 4 | 其他球队会夺冠吗? |
如果 Germany 最终夺冠:
| Market | YES 结果 |
|---|---|
| Germany 会夺冠吗? | 胜出 |
| France 会夺冠吗? | 未胜出 |
| Brazil 会夺冠吗? | 未胜出 |
| 其他球队会夺冠吗? | 未胜出 |
对开发者来说,NegRisk Market 不改变基础交易方式。开发者仍然选择具体 Market 下的 YES 或 NO outcome,并使用对应的 assetId 交易。
| 市场类型 | 特点 | 开发者交易方式 |
|---|---|---|
| Binary Market | 单个是 / 否问题 | 选择 YES 或 NO 的 assetId |
| NegRisk Market | 多个互斥结果 | 仍然选择具体 Market 下的 YES 或 NO 的 assetId |
16. 市场状态
Market 在生命周期中会经历不同状态。
常见状态如下:
| 状态 | 含义 | 是否通常可交易 |
|---|---|---|
active |
市场正常交易中 | 是 |
paused |
市场暂停交易 | 否 |
settling |
市场结算中 | 否 |
resolved |
市场已结算 | 否 |
开发者下单前应检查 Market 状态。如果 Market 不是 active,订单可能会被拒绝。
17. 关键 ID
接入 API 时,最容易混淆的是各类 ID。
可以先记住:
| ID | 含义 | 常见用途 |
|---|---|---|
eventId |
事件 ID | 查询 Event 详情及其下属 Market |
marketId |
市场 ID | 标识具体交易问题 |
assetId |
outcome 的资产 ID | 下单、撤单、行情、订单簿、持仓等交易相关操作 |
orderId |
订单 ID | 查询订单、撤单、追踪订单状态 |
最重要的规则是:
下单、撤单、行情、订单簿相关操作通常使用 assetId。
如果接口返回的 assetId 为 null,说明该 outcome 暂时不可交易。开发者需要等待 assetId 返回有效值后,再进行交易相关操作。
18. 小结
开发者可以用下面这条链路理解 Outcomes 的核心模型:
Event → Market → Outcome → assetId → Order / Position
对应关系如下:
| 阶段 | 说明 |
|---|---|
| Event | 找到真实世界事件 |
| Market | 找到该事件下的具体问题 |
| Outcome | 选择 YES 或 NO |
| assetId | 获取可交易资产 ID |
| Order | 使用 assetId 下单 |
| Position | 成交后形成对应持仓 |
对于 API 接入方来说,最关键的是:
| 关键点 | 说明 |
|---|---|
| 不交易 Event | Event 只是组织单位 |
| 不直接交易 Market | Market 是具体问题 |
| 实际交易 Outcome | Outcome 通过 assetId 表示 |
| YES / NO 有互补关系 | YES + NO ≈ 1 |
| 镜像订单簿由系统处理 | 开发者不需要手动换算 |
| 下单前检查状态 | Market 需要处于可交易状态 |
| assetId 不能为空 | assetId = null 表示暂不可交易 |
快速开始
本指南将帮助你完成 Outcomes API 的最小接入流程,并提交第一笔订单。
前置条件
开始前,请确保你已经具备以下条件:
| 条件 | 说明 |
|---|---|
| OKX 账户 | 已开通预测市场相关能力 |
| 可用余额 | 用于提交订单 |
| OKX App | 用于创建 API Key 和授权 Agent |
| 开发环境 | 本文示例使用 Rust SDK |
| Agent 私钥 | 用于对下单、撤单等写操作进行签名 |
第 1 步:设置 API Key
在对任何请求进行签名之前,您需先通过 OKX API 管理页面 或 APP 创建 API Key。创建成功后,您将获得以下 3 项必须妥善保管的凭证:
| 凭证 | 说明 |
|---|---|
| APIKey | 公开标识 |
| SecretKey | 用于请求签名 |
| Passphrase | 您自行设置的额外安全凭证 |
其中,APIKey 与 SecretKey 由平台随机生成并提供;Passphrase 由您自行设置,用于增强 API 访问的安全性。
APIKey 权限
对于预测市场,需要选中以下所有权限:
| 权限 | 说明 |
|---|---|
| 读取(Read) | 查询账单、历史记录等只读操作 |
| 交易(Trade) | 下单、撤单、转账、调整配置等写入操作 |
创建步骤
| 步骤 1:APP 预测市场页面点选 "View" | 步骤 2:点击右上角菜单 |
|---|---|
![]() |
![]() |
| 步骤 3:进入生成 API Key 页面 | 步骤 4:生成 API Key |
|---|---|
![]() |
![]() |
| 步骤 5:生成后可查看详情 | 步骤 6:详情页面 |
|---|---|
![]() |
![]() |
所有 REST 接口都需要 OKX API 凭证。使用 with_credentials 构造客户端(详见第 5 步)。
第 2 步:设置 Agent
您的主密钥保持安全——创建一个代表您签署交易的 Agent 密钥。
- 生成一个新的以太坊密钥对(任何密钥生成工具)。保存 Agent 私钥 和 Agent 地址。
- 在 OKX App 中授权该 Agent——Agent 授权必须通过 App 完成,不能通过 API。
APP 授权 Agent 步骤
| 步骤 1:输入待授权的 Agent 地址 | 步骤 2:授权成功 |
|---|---|
![]() |
![]() |
授权完成后,后续所有写 API 调用使用 Agent 私钥,读 API 调用不需要私钥。
第 3 步:添加 SDK
安装
在 Cargo.toml 中加入本 SDK 与 Tokio 运行时:
Cargo.toml 依赖配置
[dependencies]
okx-outcomes-sdk = { git = "https://github.com/okx/outcomes-sdk.git", features = ["signing", "websocket"] }
tokio = { version = "1", features = ["full"] }
这就是完整的依赖列表。tokio-tungstenite、k256、sha3、futures-util 等都会通过 SDK 的 feature 间接引入。不要 自行添加这些依赖,否则可能与 SDK 构建时使用的版本不一致。
Feature 开关
| Feature | 启用内容 | 何时启用 |
|---|---|---|
| default | 仅 REST 客户端(事件、市场、订单、持仓、余额、成交、价格、体育数据)。 | 只读集成。 |
signing |
EIP-712 + ECDSA 动作签名辅助函数。 | 任何写操作:place_order、cancel_order、cancel_all、split、merge、redeem、heartbeat。 |
websocket |
基于 tokio-tungstenite 的 PredictionsWsClient,以及类型化的 WsMessage 解析器。 |
实时价格、成交、订单簿,以及用户订单 / 持仓 / 余额流。 |
第 4 步:配置环境变量
建议使用环境变量保存凭证:
环境变量配置
export PREDICTIONS_API_KEY="your-api-key"
export PREDICTIONS_API_SECRET="your-secret-key"
export PREDICTIONS_API_PASSPHRASE="your-passphrase"
export PREDICTIONS_AGENT_PRIVATE_KEY="0x..."
第 5 步:初始化 Client
使用 API Key、Secret Key 和 Passphrase 初始化 SDK Client:
初始化 Client 示例
use okx_outcomes_sdk::{ApiCredentials, OutcomesSdkClient};
let creds = ApiCredentials {
api_key: std::env::var("PREDICTIONS_API_KEY")?,
secret_key: std::env::var("PREDICTIONS_API_SECRET")?,
passphrase: std::env::var("PREDICTIONS_API_PASSPHRASE")?,
};
let client = OutcomesSdkClient::with_credentials(creds);
SDK 会在本地完成 REST 请求签名,并自动写入所需的鉴权请求头。
第 6 步:查询可交易市场
get_events 返回分页的事件列表。所有筛选参数都是可选的:
查询事件列表示例
let page = client
.get_events(
Some("active"), // status: "active" | "resolved"
None, // tag
None, // league_id
Some("volume_24h"), // sort: "volume" | "volume_24h" | "ending_soon" | "newest"
None, // cursor(来自上一页)
Some(20), // page_size(最大 50)
)
.await?;
for event in &page.events {
println!("{} - {} markets ({} points volume)",
event.event_title, event.total_markets_count, event.volume);
}
// 分页:把 `page.pagination.next_cursor` 作为下一次调用的 `cursor` 传回去。
其他读取方法
| 方法 | 用途 |
|---|---|
client.search(keyword, cursor, page_size) |
按关键字搜索事件与市场。 |
client.get_event(event_id) |
单个事件及其完整市场列表。 |
client.get_event_markets(event_id) |
单个事件下的全部市场(不分页)。 |
client.get_market(market_id) |
通过市场 ID 获取单个市场。 |
client.get_ticker(inst_id) |
单个市场标的的最新行情。 |
client.get_candles(...) |
K 线历史。 |
client.get_trades(...) |
最近的公开成交历史。 |
找到 status: "active" 的市场,记录:
yesOutcome.assetId(例如"100049000")——用于下单marketId(例如"1")——用于持仓查询
第 7 步:下限价单
以 0.55 xp 的价格在市场 100049000 上买入 10 个 YES 代币。需要 两层认证:API Key 请求头 + SDK 签名请求体。
下限价单完整示例
use okx_outcomes_sdk::{ApiCredentials, OutcomesSdkClient};
use okx_outcomes_sdk::models::order::{
LimitOrderType, OrderItem, PlaceOrderAction, PlaceOrderRequest, SigningOrderSide, SizeType,
};
use okx_outcomes_sdk::signing::{
action_place_order, generate_client_order_id_default, now_millis, parse_private_key,
sign_to_wrapper, LimitTif, OrderRequest, OrderType,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let creds = ApiCredentials {
api_key: std::env::var("PREDICTIONS_API_KEY")?,
secret_key: std::env::var("PREDICTIONS_API_SECRET")?,
passphrase: std::env::var("PREDICTIONS_API_PASSPHRASE")?,
};
let client = OutcomesSdkClient::with_credentials(creds);
// 1. Load the on-chain signing key (hex, with or without 0x prefix).
let key = parse_private_key(&std::env::var("PREDICTIONS_AGENT_PRIVATE_KEY")?)?;
// 2. Build the typed order. `asset_id` is the outcome asset ID from Step 6.
let order_request = OrderRequest {
asset_id: "100049000".into(),
side: SigningOrderSide::Buy,
market_type: "prediction".into(),
client_order_id: Some(generate_client_order_id_default()?),
price: "0.55".into(),
reduce_only: false,
size: "10".into(),
size_type: SizeType::Base, // omitted on the wire (default)
order_type: OrderType::Limit(LimitOrderType { tif: LimitTif::Gtc }),
};
// 3. Derive the wire-format OrderItem from the same OrderRequest so the
// JSON body cannot drift from the signed msgpack bytes.
let order_item = OrderItem::try_from(&order_request)?;
let action = action_place_order(vec![order_request]);
// 4. Sign and assemble the SignatureWrapper expected by the wire format.
let nonce = now_millis();
let signature = sign_to_wrapper(&action, nonce, None, &key)?;
// 5. Submit.
let req = PlaceOrderRequest {
action: PlaceOrderAction {
action_type: "placeOrder".into(),
grouping: "na".into(),
orders: vec![order_item],
},
nonce: nonce as i64,
signature,
};
let resp = client.place_order(&req).await?;
println!("tx_hash: {}", resp.tx_hash);
Ok(())
}
下单响应示例
{
"code": 0,
"message": "OK",
"data": {
"txHash": "0x7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b"
}
}
第 8 步:查看订单
list_orders 在 status = Some("open") 时按页返回账户上所有非终态订单。把上一次响应中的 next_cursor 通过 cursor 参数传回,直到 has_next 为 false:
查询订单列表示例
use okx_outcomes_sdk::models::order::OrderRecord;
let mut cursor: Option<String> = None;
let mut all_open: Vec<OrderRecord> = Vec::new();
loop {
let page = client
.list_orders(
None, // market_id: None = 所有市场,Some(123) = 限定某个市场
Some("open"), // "open" = PENDING_PLACE / ACTIVE / PENDING_CANCEL;"closed" = FILLED / PARTIALLY_FILLED / CANCELLED / EXPIRED / FAILED
cursor.as_deref(),
Some(50), // 每页最大条数
)
.await?;
all_open.extend(page.list);
if !page.has_next {
break;
}
cursor = page.next_cursor;
}
println!("{} open orders", all_open.len());
for o in &all_open {
println!(
"#{} [{}] {} {}/{} @ {} market={} asset={} cloid={:?}",
o.id, o.status, o.side, o.filled_size, o.size, o.price,
o.market_id, o.asset_id, o.client_order_id,
);
}
每个 OrderRecord 暴露下单参数(side、size、price、order_type、size_type、expiration)、下单元数据(market_id、asset_id、client_order_id、tx_hash),以及当前生命周期状态(status、filled_size、filled_amount)。
提示:
market_id在 SDK 中是i64,不是字符串;请用Some(123_456_789)传入。REST API 返回的marketId是字符串,SDK 内部会自动转换。client_order_id(下单时传入的 cloid)可以传给cancel_order,以使用客户端 ID 撤单而不是服务端 ID。- 记录
id(例如578840)——撤单时需要用到。
第 9 步:撤销订单
撤单流程与 place_order 一致,同样分三步:构造类型化的 CancelRequest,从中派生对应的 wire 格式 CancelItem,对动作签名,最后提交 wire 请求。可以通过订单的服务端 oid(OrderRecord.id 的返回值)或 cloid(下单时传入的 client_order_id)来定位目标订单。
下面的片段假设 client 和 key 已按下单一节构造完成。
撤销订单示例
use okx_outcomes_sdk::models::order::{CancelItem, CancelOrderAction, CancelOrderRequest};
use okx_outcomes_sdk::signing::{
action_cancel, now_millis, sign_to_wrapper, CancelRequest, CancelTarget,
};
// 1. CancelTarget 必须选其一:
// Oid = 服务端分配的订单 ID(OrderRecord.id),十进制字符串。
// Cloid = 客户端分配的订单 ID,带 0x 前缀的 hex 字符串。
let cancel_request = CancelRequest {
asset_id: "100049000".into(),
market_type: "prediction".into(),
target: CancelTarget::Oid("578840".into()),
// target: CancelTarget::Cloid("0xabc...".into()),
};
// 2. 从同一份请求派生 wire 项,确保签名字节与 JSON 请求体不会发散。
let cancel_item = CancelItem::from(&cancel_request);
let action = action_cancel(vec![cancel_request]);
// 3. 签名并提交。
let nonce = now_millis();
let signature = sign_to_wrapper(&action, nonce, None, &key)?;
let resp = client
.cancel_order(&CancelOrderRequest {
action: CancelOrderAction {
action_type: "cancel".into(),
cancels: vec![cancel_item],
},
nonce: nonce as i64,
signature,
})
.await?;
println!("cancel tx_hash: {}", resp.tx_hash);
第 10 步:查看余额、订单与持仓
查询余额与持仓示例
// 余额
let entries = client.get_balance().await?; // Vec<BalanceEntry>
for entry in &entries {
println!("[{}] available={} total={}", entry.odds_type, entry.available, entry.balance);
}
// 持仓
let positions = client.get_positions(
Some("open"), // "open" | "closed" | "settled"
None, // market_id
None, // cursor
Some(50), // limit
).await?;
println!("{} positions", positions.list.len());
订单查询方式见第 8 步。get_positions 同样通过 cursor + limit 分页。
完整流程回顾
最小接入流程如下:
- 在 App / 网页 中创建 API Key
- 在 App 中授权 Agent
- 安装 SDK
- 配置环境变量
- 初始化 Client
- 查询 Event 和 Market
- 获取 YES / NO outcome 的 assetId,使用 assetId 提交订单
- 查询订单状态
- 如有需要,撤销订单
- 成交后查询持仓和余额
注意事项
| 主题 | 说明 |
|---|---|
| 心跳 | 如果运行机器人,设置心跳以在机器人断线时自动撤销所有订单。每 < 5 分钟发送一次预签名的全部撤单,详情见 SDK 文档。 |
| 价格刻度 | 见下表。 |
| Nonce | 使用当前毫秒时间戳。每个 Nonce 只能使用一次。 |
| 时间戳偏差 | OK-ACCESS-TIMESTAMP 与服务器时间相差不能超过 30 秒,否则返回错误码 50102。 |
价格刻度规则
| 价格区间 | 最小精度 | 示例 |
|---|---|---|
| 0.04 - 0.96 | 0.01 | 0.04, 0.55, 0.96 |
| < 0.04 或 > 0.96 | 0.001 | 0.039, 0.962 |
常见问题
| 问题 | 可能原因 | 处理方式 |
|---|---|---|
assetId 为 null |
市场尚未完成注册 | 等待 assetId 返回有效值后再下单 |
| 下单失败 | 市场不可交易、余额不足、签名错误或参数不合法 | 查看错误码和订单失败原因 |
| 签名失败 | Agent 私钥不正确,或签名内容与请求体不一致 | 确认 Agent 已授权,并使用 SDK 构造签名 |
| 查询不到订单 | 订单 ID 错误,或订单已经结束 | 使用订单列表接口确认订单状态 |
| 撤单后订单仍未取消 | 撤单异步处理中 | 等待状态从 PENDING_CANCEL 更新为 CANCELLED |
REST API
REST API 用于查询事件、市场、行情、订单簿、订单、成交、持仓、余额,以及提交下单、撤单、Split、Merge、Redeem 等写操作。开发者可以通过 REST API 完成完整的交易闭环:发现市场、获取 assetId、提交订单、查询订单状态、管理持仓并处理结算相关操作。所有接口的基础地址为 https://www.okx.com。
1. 事件与市场
数据结构定义
结果选项 OutcomeResp
| 字段 | 类型 | 说明 |
|---|---|---|
| tokenId | string / null | 条件代币合约地址;未上链前为 null |
| assetId | string / null | TradeZone 资产 ID(下单时使用);未上链前为 null |
| name | string | 结果名称,如 "Yes"、"No" |
| price | string | 当前价格(0–1 之间的字符串,如 "0.82") |
| bgColor | string | 按钮背景色(Hex);体育 moneyline Yes outcome 取主队主题色;其余为 null |
市场对象 MarketResp
| 字段 | 类型 | 说明 |
|---|---|---|
| id | string | 市场中心化唯一 ID |
| marketId | string | 市场在 TradeZone 中的唯一 ID(下单时使用) |
| negRisk | boolean | 是否互斥市场(negRisk) |
| status | string | 市场状态:active / paused / settling / resolved |
| settleStage | int | 结算阶段:0=未开始 1=第一轮公示 2=第一轮 dispute 3=第二轮公示 4=第二轮 dispute 5=已结算 |
| question | string | 完整市场问题 |
| shortQuestion | string / null | 缩略市场问题 |
| description | string | 市场描述 |
| marketIcon | string / null | 市场图标 URL |
| bestBid | string / null | 最优买价(0–1);无买盘时为 null |
| bestAsk | string / null | 最优卖价(0–1);无卖盘时为 null |
| lastTradePrice | string / null | 最新成交价(0–1);从未成交时为 null |
| volume | string | 总交易量(xp) |
| probability | string / null | 市场 Yes 概率(0–1 小数);未上链时为 null |
| resolutionSources | string[] | 结算数据来源 URL 列表 |
| yesOutcome | OutcomeObject | Yes 方结果选项 |
| noOutcome | OutcomeObject | No 方结果选项 |
| startTime | string | market 预计开始时间(毫秒时间戳) |
| endTime | string | market 预计结束时间(毫秒时间戳) |
| resolveStartAt | string | 首次进入 start_resolve 状态的时间(毫秒时间戳) |
| resolveAt | string | 首次进入 resolved 状态的时间(毫秒时间戳) |
事件对象 EventResp
| 字段 | 类型 | 说明 |
|---|---|---|
| id | string | 事件中心化唯一 ID |
| eventId | string | 事件在 TradeZone 中的唯一 ID |
| negRisk | boolean | 是否负风险事件 |
| status | string | 事件状态:active / paused / resolved |
| eventTitle | string | 事件标题 |
| description | string | 事件描述 |
| eventIcon | string / null | 事件图标 URL |
| volume | string | 事件内所有市场交易量之和(xp) |
| startTime | string / null | 开始交易时间戳(ms) |
| endTime | string / null | 停止交易时间戳(ms) |
| createdAt | string | 事件创建时间戳(ms) |
| totalMarketsCount | int | 事件下市场总数 |
| finalOutcomesMarketId | string / null | 结算后胜出市场 ID;未结算时为 null |
| markets | array<MarketResp> | 市场列表(列表接口最多返回前 2 个;完整列表通过 GET /api/v5/predictions/events/{eventId}/markets 获取) |
游标分页 PaginationResp
| 字段 | 类型 | 说明 |
|---|---|---|
| nextCursor | string / null | 下一页游标;null 表示已是最后一页 |
| hasMore | boolean | 是否有更多数据 |
| pageSize | int | 本次返回数量 |
1.1 获取事件列表
获取预测市场事件列表,支持状态等多维过滤与排序。
HTTP请求
GET /api/v5/predictions/events
请求参数(Query)
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| status | string | 否 | active |
事件状态过滤:active / resolved |
| sort | string | 否 | volume_24h |
排序方式:volume / volume_24h / ending_soon / newest |
| tag | string | 否 | - | 体育标签 ID 过滤(来自 GET /api/v5/predictions/sports/tags) |
| leagueId | string | 否 | - | 联赛 ID 过滤(来自 GET /api/v5/predictions/sports/tags/{tagId}/leagues) |
| cursor | string | 否 | - | 分页游标,首次请求不传 |
| pageSize | int | 否 | 10 | 每页条数(最大 50) |
响应(data):
响应示例
{
"code": 0,
"message": "OK",
"data": {
"events": [
{
"id": "100",
"eventId": "100",
"negRisk": false,
"status": "active",
"eventTitle": "Will BTC exceed $100k by end of 2026?",
"description": "This event tracks whether Bitcoin will cross $100,000.",
"eventIcon": "https://cdn.example.com/events/100.png",
"volume": "1250000.00",
"startTime": 1700000000000,
"endTime": 1798761600000,
"createdAt": 1710000000000,
"totalMarketsCount": 1,
"finalOutcomesMarketId": null,
"markets": [
{
"id": "1",
"marketId": "1",
"oddsType": "points",
"negRisk": false,
"status": "active",
"settleStage": 0,
"question": "Will BTC exceed $100k by end of 2026?",
"shortQuestion": "BTC > $100k?",
"description": "Resolves YES if BTC price exceeds $100,000 before Jan 1, 2027.",
"marketIcon": null,
"bestBid": "0.64",
"bestAsk": "0.66",
"lastTradePrice": "0.65",
"volume": "1250000.00",
"probability": "0.65",
"resolutionSources": ["https://coinmarketcap.com"],
"yesOutcome": {
"tokenId": "0xabc...001",
"assetId": "1",
"name": "Yes",
"price": "0.65",
"finalResult": null
},
"noOutcome": {
"tokenId": "0xabc...002",
"assetId": "2",
"name": "No",
"price": "0.35",
"finalResult": null
}
}
]
}
],
"pagination": {
"nextCursor": "eyJpZCI6MTAwfQ",
"hasMore": true,
"pageSize": 10
}
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
| events | array<EventResp> | 事件列表;每个事件的 markets 最多返回前 2 个市场 |
| pagination | PaginationResp | 游标分页信息 |
1.2 获取单个事件
获取指定事件的完整信息,markets 字段返回该事件下所有市场(不截断)。
HTTP请求
GET /api/v5/predictions/events/{eventId}
路径参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| eventId | string | 是 | 事件 ID |
响应(data):
响应示例
{
"code": 0,
"message": "OK",
"data": {
"id": "100",
"eventId": "100",
"negRisk": false,
"status": "active",
"eventTitle": "NBA Finals 2026 - Lakers vs Celtics",
"description": "Who will win the 2026 NBA Championship?",
"eventIcon": "https://cdn.example.com/events/100.png",
"volume": "3200000.00",
"startTime": 1748000000000,
"endTime": 1750000000000,
"createdAt": 1710000000000,
"totalMarketsCount": 3,
"finalOutcomesMarketId": null,
"markets": [ ]
}
}
返回完整 EventResp,markets 包含该事件下的全部市场。
1.3 获取市场列表
获取指定事件下的全部市场,不分页。
HTTP请求
GET /api/v5/predictions/events/{eventId}/markets
路径参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| eventId | string | 是 | 事件 ID |
响应(data):
响应示例
{
"code": 0,
"message": "OK",
"data": {
"markets": [
{
"id": "1",
"marketId": "1",
"negRisk": false,
"status": "active",
"settleStage": 0,
"question": "Will BTC exceed $100k by end of 2026?",
"shortQuestion": "BTC > $100k?",
"description": "Resolves YES if BTC price exceeds $100,000 before Jan 1, 2027.",
"marketIcon": null,
"bestBid": "0.64",
"bestAsk": "0.66",
"lastTradePrice": "0.65",
"volume": "1250000.00",
"probability": "0.65",
"resolutionSources": ["https://coinmarketcap.com"],
"yesOutcome": {
"tokenId": "0xabc...001",
"assetId": "1",
"name": "Yes",
"price": "0.65",
"finalResult": null
},
"noOutcome": {
"tokenId": "0xabc...002",
"assetId": "2",
"name": "No",
"price": "0.35",
"finalResult": null
}
}
]
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
| markets | array<MarketResp> | 该事件下的全部市场 |
1.4 获取单个市场
获取指定市场的详细信息。
HTTP请求
GET /api/v5/predictions/markets/{marketId}
路径参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| marketId | string | 是 | 市场 TradeZone ID |
响应(data):
响应示例
{
"code": 0,
"message": "OK",
"data": {
"id": "1",
"marketId": "1",
"negRisk": false,
"status": "resolved",
"settleStage": 5,
"question": "Will BTC exceed $100k by end of 2026?",
"shortQuestion": "BTC > $100k?",
"description": "Resolves YES if BTC price exceeds $100,000 before Jan 1, 2027.",
"marketIcon": null,
"bestBid": null,
"bestAsk": null,
"lastTradePrice": "0.98",
"volume": "1250000.00",
"probability": null,
"resolutionSources": ["https://coinmarketcap.com"],
"yesOutcome": {
"tokenId": "0xabc...001",
"assetId": "1",
"name": "Yes",
"price": "1.00",
"finalResult": true
},
"noOutcome": {
"tokenId": "0xabc...002",
"assetId": "2",
"name": "No",
"price": "0.00",
"finalResult": false
}
}
}
返回完整 MarketResp。
1.5 搜索事件
按关键字全文搜索事件,返回匹配的事件列表,支持游标分页。
HTTP请求
GET /api/v5/predictions/events/search
请求参数(Query)
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| keyword | string | 是 | - | 搜索关键字,匹配事件标题和描述 |
| cursor | string | 否 | - | 分页游标,首次请求不传 |
| pageSize | int | 否 | 10 | 每页条数(最大 50) |
响应(data):
响应示例
{
"code": 0,
"message": "OK",
"data": {
"events": [
{
"id": "100",
"eventId": "100",
"negRisk": false,
"status": "active",
"eventTitle": "Will BTC exceed $100k by end of 2026?",
"description": "This event tracks whether Bitcoin will cross $100,000.",
"eventIcon": "https://cdn.example.com/events/100.png",
"volume": "1250000.00",
"startTime": 1700000000000,
"endTime": 1798761600000,
"createdAt": 1710000000000,
"totalMarketsCount": 1,
"finalOutcomesMarketId": null,
"markets": [
{
"id": "1",
"marketId": "1",
"oddsType": "points",
"negRisk": false,
"status": "active",
"settleStage": 0,
"question": "Will BTC exceed $100k by end of 2026?",
"shortQuestion": "BTC > $100k?",
"description": "Resolves YES if BTC price exceeds $100,000 before Jan 1, 2027.",
"marketIcon": null,
"bestBid": "0.64",
"bestAsk": "0.66",
"lastTradePrice": "0.65",
"volume": "1250000.00",
"probability": "0.65",
"resolutionSources": ["https://coinmarketcap.com"],
"yesOutcome": {
"tokenId": "0xabc...001",
"assetId": "1",
"name": "Yes",
"price": "0.65",
"finalResult": null
},
"noOutcome": {
"tokenId": "0xabc...002",
"assetId": "2",
"name": "No",
"price": "0.35",
"finalResult": null
}
}
]
}
],
"pagination": {
"nextCursor": "eyJpZCI6MTAwfQ",
"hasMore": true,
"pageSize": 10
}
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
| events | array<EventResp> | 事件列表 |
| pagination | PaginationResp | 游标分页信息 |
2. 价格
2.1 获取行情 Ticker
获取单个预测市场产品的最新行情信息,用于订单簿中的最新成交价展示
请求参数
| 参数 | 类型 | 必须 | 说明 |
|---|---|---|---|
instId |
String | 是 | 预测市场 yesAssetId |
HTTP请求
GET /api/v5/market/ticker?instId={yesAssetId}
返回参数
返回示例
{
"code": "0",
"msg": "",
"data": [{
"instType": "SPOT",
"instId": "{yesAssetId}",
"last": "0.65",
"lastSz": "100",
"askPx": "0.66",
"askSz": "500",
"bidPx": "0.64",
"bidSz": "300",
"open24h": "0.60",
"high24h": "0.70",
"low24h": "0.55",
"vol24h": "10000",
"volCcy24h": "6500",
"sodUtc0": "0.62",
"sodUtc8": "0.61",
"ts": "1711900800000"
}]
}
| 字段 | 类型 | 说明 |
|---|---|---|
instType |
String | 产品类型 |
instId |
String | 产品ID |
last |
String | 最新成交价 |
lastSz |
String | 最新成交数量 |
askPx |
String | 卖一价 |
askSz |
String | 卖一价对应数量 |
bidPx |
String | 买一价 |
bidSz |
String | 买一价对应数量 |
open24h |
String | 24小时开盘价 |
high24h |
String | 24小时最高价 |
low24h |
String | 24小时最低价 |
vol24h |
String | 24小时成交量(以张计) |
volCcy24h |
String | 24小时成交量(以计价货币计) |
sodUtc0 |
String | UTC 0 时开盘价 |
sodUtc8 |
String | UTC+8 时开盘价 |
ts |
String | 数据更新时间(Unix 毫秒时间戳) |
2.2 获取 K 线数据
查询预测市场产品的历史 K 线数据,K线数据按请求的粒度分组返回,K线数据每个粒度最多可获取最近1,440条
请求参数
| 参数 | 类型 | 必须 | 说明 |
|---|---|---|---|
instId |
String | 是 | 预测市场 yesAssetId |
bar |
String | 否 | K 线周期,默认 1m。可选值:1m 3m 5m 15m 30m 1H 2H 4H 6H 12H 1D 1W 1M |
after |
String | 否 | 分页游标,返回该时间戳之前的数据(Unix 毫秒) |
before |
String | 否 | 分页游标,返回该时间戳之后的数据(Unix 毫秒) |
limit |
String | 否 | 每页数量,最大 100,默认 100 |
HTTP请求
GET /api/v5/market/candles?instId={yesAssetId}&bar=1m&limit=100
返回数据
(data 为二维数组,每条记录字段顺序如下)
返回示例
{
"code": "0",
"msg": "",
"data": [
["1711900800000", "0.65", "0.67", "0.63", "0.66", "2000", "1300", "1300", "1"],
["1711900740000", "0.63", "0.66", "0.62", "0.65", "1800", "1170", "1170", "1"]
]
}
| 索引 | 字段 | 类型 | 说明 |
|---|---|---|---|
| 0 | ts |
String | K 线开始时间(Unix 毫秒时间戳) |
| 1 | o |
String | 开盘价 |
| 2 | h |
String | 最高价 |
| 3 | l |
String | 最低价 |
| 4 | c |
String | 收盘价 |
| 5 | vol |
String | 成交量(以张计) |
| 6 | volCcy |
String | 成交量(以计价货币计) |
| 7 | volCcyQuote |
String | 成交量(以报价货币计) |
| 8 | confirm |
String | K 线状态:0 未确认,1 已确认 |
2.3 获取深度数据
查询预测市场产品的买卖盘口深度快照
请求参数
| 参数 | 类型 | 必须 | 说明 |
|---|---|---|---|
instId |
String | 是 | 预测市场 yesAssetId |
sz |
String | 否 | 深度档位数量,最大值可传400,即买卖深度共800条 不填写此参数,默认返回1档深度数据 |
HTTP请求
GET /api/v5/market/pm-books?instId={yesAssetId}&sz=400
返回参数
返回示例
{
"code": "0",
"msg": "",
"data": [
{
"asks": [
["67364.1","0.45478048","5"]
],
"bids": [
["67364","1.72315936", "17"]
],
"ts": "1774943488756",
"seqId": 74487243135
}
]
}
| 字段 | 类型 | 说明 |
|---|---|---|
asks |
Array | 卖盘数据,按价格从低到高排列 |
bids |
Array | 买盘数据,按价格从高到低排列 |
ts |
String | 深度快照时间(Unix 毫秒时间戳) |
seqId |
Number | 订单簿的版本号,预测市场无需关心 |
asks / bids 数组中每条记录格式:[价格, 数量, 订单数量]
3. 订单
3.1 下单
提交签名后的下单请求。开发者自行构造 calldata 并用自己的钱包私钥(ECDSA)签名,服务端校验后写入数据库并直接转发至 TradeZone(不经过 AA Wallet)。
HTTP请求
POST /api/v5/predictions/orders
请求体
请求示例
{
"action": {
"type": "placeOrder",
"grouping": "na",
"orders": [{
"assetId": "1",
"marketType": "prediction",
"side": "buy",
"price": "0.65",
"size": "100",
"reduceOnly": false,
"clientOrderId": "0x0197a98c91312671ca83f15ccbd5186f",
"orderType": { "limit": { "tif": "gtc" } }
}]
},
"nonce": 1708929600000,
"signature": {
"Ecdsa": {
"r": "0x...",
"s": "0x...",
"v": 1
}
}
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| action | object | 是 | 下单动作 |
| action.type | string | 是 | 固定 "placeOrder" |
| action.grouping | string | 是 | 固定 "na" |
| action.orders | array | 是 | 订单列表(每次一笔) |
| action.orders[].assetId | string | 是 | TradeZone 资产 ID(如 "1") |
| action.orders[].marketType | string | 是 | 固定 "prediction" |
| action.orders[].side | string | 是 | "buy" / "sell" |
| action.orders[].price | string | 是 | 挂单价格(如 "0.65") |
| action.orders[].size | string | 是 | 下单数量(如 "100") |
| action.orders[].clientOrderId | string | 是 | 下单clientOrderId 区分地区 |
| action.orders[].reduceOnly | boolean | 否 | 是否只减仓,默认 false |
| action.orders[].sizeType | string | 否 | "base"(默认)/ "quote" |
| action.orders[].orderType | object | 是 | 订单类型 |
| action.orders[].orderType.limit.tif | string | 是 | "gtc" / "gtd" / "ioc" / "fok" / "alo" |
| nonce | long | 是 | 请求时间戳(毫秒),防重放 |
| signature | object | 是 | 签名对象 |
| signature.Ecdsa | object | 是 | ECDSA 签名分量 |
| signature.Ecdsa.r | string | 是 | 签名 r 值("0x...") |
| signature.Ecdsa.s | string | 是 | 签名 s 值("0x...") |
| signature.Ecdsa.v | int | 是 | 签名 v 值(0 或 1) |
TIF(Time In Force)
| JSON 值 | 交易结构 | 说明 |
|---|---|---|
"gtc" |
"tif": "gtc" | Good-Til-Cancel,挂单直到成交或取消 |
"gtd" |
"tif": { "gtd": { "expiresAfter": 1700000005000 } }, | Good-Til-Date,挂单直到 expiresAfter 指定的时间过期或成交/取消 |
"ioc" |
"tif": "ioc" | Immediate-Or-Cancel(即 FAK),立即尽可能成交后取消剩余。仅用于模拟市价单,需配合价格保护上/下限(price) |
"fok" |
"tif": "fok" | Fill-Or-Kill,全部成交或全部取消。仅用于模拟市价单,需配合价格保护上/下限(price) |
"alo" |
"tif": "alo" | Add-Liquidity-Only (Post-Only),只挂 maker 单,若会立即成交则拒绝 |
响应(data):
响应示例
{
"code": 0,
"message": "OK",
"data": {
"txHash": "0xdef...789"
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
| txHash | string | TradeZone 交易哈希 |
3.2 撤销单个订单
撤销一个活跃订单。开发者自行构造撤单 calldata 并签名,服务端校验订单归属和状态后提交至 TradeZone。
HTTP请求
POST /api/v5/predictions/orders/cancel
请求体
请求示例
{
"action": {
"type": "cancel",
"cancels": [{
"assetId": "1",
"marketType": "prediction",
"oid": "12345",
"clientOrderId":"0x"
}]
},
"nonce": 1708929600000,
"signature": {
"Ecdsa": {
"r": "0x...",
"s": "0x...",
"v": 1
}
}
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| action | object | 是 | 撤单动作 |
| action.type | string | 是 | 固定 "cancel" |
| action.cancels | array | 是 | 撤单列表(每次一笔) |
| action.cancels[].assetId | string | 是 | TradeZone 资产 ID(如 "1") |
| action.cancels[].marketType | string | 是 | 固定 "prediction" |
| action.cancels[].oid | string | 否 | 订单 ID与客户端ID 两个中必传一个 |
| action.cancels[].clientOrderId | string | 否 | 订单 ID与客户端ID 两个中必传一个 |
| nonce | long | 是 | 请求时间戳(毫秒) |
| signature | object | 是 | 签名对象 |
| signature.Ecdsa | object | 是 | ECDSA 签名分量 |
| signature.Ecdsa.r | string | 是 | 签名 r 值("0x...") |
| signature.Ecdsa.s | string | 是 | 签名 s 值("0x...") |
| signature.Ecdsa.v | int | 是 | 签名 v 值(0 或 1) |
响应(data):
响应示例
{
"code": 0,
"message": "OK",
"data": {
"txHash": "0xdef...abc"
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
| txHash | string | TradeZone 交易哈希 |
3.3 查询单个订单
查询指定订单的完整详情(活跃或历史订单均可)
HTTP请求
GET /api/v5/predictions/orders/{orderId}
路径参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| orderId | string | 是 | 订单 ID |
响应(data):
响应示例
{
"code": 0,
"message": "OK",
"data": {
"id": "1",
"oid": "1",
"clientOrderId": "1",
"marketId": "1",
"assetId": "1",
"side": "BUY",
"orderType": "GTC",
"sizeType": "BASE",
"size": "100",
"price": "0.65",
"expiration": null,
"txHash": "0xdef...789",
"status": "ACTIVE",
"filledSize": "40",
"filledAmount": "26",
"failReason": null,
"cancelReason": null,
"oddsType": "points",
"createdAt": "1710000000000",
"updatedAt": "1710000000000"
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
| id | string | 订单 ID |
| oid | string | 订单 oid |
| clientOrderId | string | 订单 clientOrderId |
| marketId | string | 市场 ID |
| tokenId | string | YES/NO 代币地址 |
| assetId | string | TradeZone 资产 ID |
| side | string | BUY / SELL |
| orderType | string | GTC / GTD / FOK / IOC / POST_ONLY |
| sizeType | string | BASE / QUOTE |
| size | string | 原始下单数量 |
| price | string | 挂单价格 |
| expiration | string | GTD 过期时间戳(毫秒);非 GTD 为 null |
| txHash | string | 当前阶段的交易哈希 |
| status | string | 订单状态(见下表) |
| filledSize | string | 已成交代币数量 |
| filledAmount | string | 已成交 xp 金额 |
| failReason | string | 失败原因(仅 status = FAILED 时有值) |
| cancelReason | string | 取消原因(系统触发的取消) |
| oddsType | string | 盘口类型:points=积分盘 |
| createdAt | string | 订单创建时间戳(毫秒) |
| updatedAt | string | 最后更新时间戳(毫秒) |
订单状态
| Status | Description |
|---|---|
| PENDING_PLACE | Submitted, waiting for on-chain confirmation |
| ACTIVE | Active open order |
| PENDING_CANCEL | Cancellation submitted, waiting for on-chain confirmation |
| FILLED | Fully filled |
| PARTIALLY_FILLED | Partially filled then cancelled or expired |
| FAILED | On-chain transaction failed |
| CANCELLED | Cancelled by user or system |
| EXPIRED | GTD order expired |
3.4 查询用户订单列表
查询当前认证用户的订单列表,支持过滤和分页。
HTTP请求
GET /api/v5/predictions/orders
请求参数(Query)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| marketId | string | 否 | 按市场 ID 过滤 |
| status | string | 否 | open(PENDING_PLACE / ACTIVE / PENDING_CANCEL)、closed(FILLED / PARTIALLY_FILLED / CANCELLED / EXPIRED / FAILED),不传默认open |
| cursor | string | 否 | 分页游标 |
| limit | int | 否 | 每页条数(默认 20,最大 50) |
响应(data):
响应示例
{
"code": 0,
"message": "OK",
"data": {
"list": [
{
"id": "1",
"oid": "1",
"clientOrderId": "1",
"marketId": "1",
"assetId": "1",
"side": "BUY",
"orderType": "GTC",
"sizeType": "BASE",
"size": "100",
"price": "0.65",
"expiration": null,
"txHash": "0xdef...789",
"status": "ACTIVE",
"filledSize": "40",
"filledAmount": "26",
"failReason": null,
"cancelReason": null,
"oddsType": "points",
"createdAt": "1710000000000",
"updatedAt": "1710000000000"
}
],
"nextCursor": "eyJpZCI6MTIzfQ",
"hasNext": false
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
| list[].id | string | 订单 ID |
| list[].marketId | string | 市场 ID |
| list[].oid | string | 订单 oid |
| list[].clientOrderId | string | 订单 clientOrderId |
| list[].tokenId | string | YES/NO 代币地址 |
| list[].assetId | string | TradeZone 资产 ID |
| list[].side | string | BUY / SELL |
| list[].orderType | string | GTC / GTD / FOK / IOC / POST_ONLY |
| list[].sizeType | string | BASE / QUOTE |
| list[].size | string | 原始下单数量 |
| list[].price | string | 挂单价格 |
| list[].expiration | string | GTD 过期时间戳(毫秒);非 GTD 为 null |
| list[].txHash | string | 当前阶段的交易哈希 |
| list[].status | string | 订单状态:PENDING_PLACE / ACTIVE / PENDING_CANCEL / FILLED / PARTIALLY_FILLED / FAILED / CANCELLED / EXPIRED |
| list[].filledSize | string | 已成交代币数量 |
| list[].filledAmount | string | 已成交 pts金额 |
| list[].failReason | string | 失败原因(仅 status = FAILED 时有值) |
| list[].cancelReason | string | 取消原因(系统触发的取消) |
| list[].oddsType | string | 盘口类型:points=积分盘 |
| list[].createdAt | String | 订单创建时间戳(毫秒) |
| list[].updatedAt | string | 最后更新时间戳(毫秒) |
| nextCursor | string | 下一页游标 |
| hasNext | boolean | 是否有更多数据 |
3.5 撤销全部 / 指定市场订单
撤销当前认证用户的活跃订单。assetIds 为空列表时撤销全部市场订单;传入具体值时仅撤销对应资产所在市场的订单。
HTTP请求
POST /api/v5/predictions/orders/cancel-all
请求体
请求示例(按市场撤单)
{
"action": {
"type": "cancelAll",
"assetIds": [1, 2, 5],
"marketType": "prediction"
},
"nonce": 1708929600000,
"expiresAfter": 1708929660000,
"signature": {
"Ecdsa": {
"r": "0x...",
"s": "0x...",
"v": 1
}
}
}
请求示例(撤销全部)
{
"action": {
"type": "cancelAll",
"assetIds": [],
"marketType": "prediction"
},
"nonce": 1708929600000,
"expiresAfter": 1708929660000,
"signature": {
"Ecdsa": {
"r": "0x...",
"s": "0x...",
"v": 1
}
}
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| action | object | 是 | 撤单动作 |
| action.type | string | 是 | 固定 "cancelAll" |
| action.assetIds | array | 是 | 资产 ID 列表;传空列表撤销全部订单,传具体值按对应市场撤单 |
| action.marketType | string | 是 | 市场类型,固定 "prediction" |
| nonce | long | 是 | 请求时间戳(毫秒) |
| expiresAfter | long | 是 | 过期时间戳(毫秒) |
| signature | object | 是 | 签名对象 |
| signature.Ecdsa | object | 是 | ECDSA 签名分量 |
| signature.Ecdsa.r | string | 是 | 签名 r 值("0x...") |
| signature.Ecdsa.s | string | 是 | 签名 s 值("0x...") |
| signature.Ecdsa.v | int | 是 | 签名 v 值(0 或 1) |
响应(data):
响应示例
{
"code": 0,
"message": "OK",
"data": {
"txHash": "0xdef...abc"
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
| txHash | string | TradeZone 交易哈希 |
3.6 发送心跳
心跳机制,用于保护开发者的活跃订单。请求体携带一个预签名的 cancelAll action,nonce 设置为当前时间 + 5 分钟。服务端暂存该签名;若开发者在 5 分钟内未续期心跳,系统将自动使用该签名执行全部撤单。
开发者需持续调用此接口(建议间隔 < 5 分钟),每次用新的 nonce(当前时间 + 5 分钟)覆盖上一次的预签名。
HTTP请求
POST /api/v5/predictions/heartbeat
请求体
请求示例
{
"action": {
"type": "cancelAll",
"assetIds": [],
"marketType": "prediction"
},
"nonce": 1708929900000,
"expiresAfter": 1708929960000,
"signature": {
"Ecdsa": {
"r": "0x...",
"s": "0x...",
"v": 1
}
}
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| action | object | 是 | 撤单动作,结构与 POST /api/v5/predictions/orders/cancel-all 一致 |
| action.type | string | 是 | 固定 "cancelAll" |
| action.assetIds | array | 是 | 固定传空列表 [],心跳触发时撤销全部订单 |
| action.marketType | string | 是 | 市场类型,固定 "prediction" |
| nonce | long | 是 | 当前时间 + 5 分钟的时间戳(毫秒) |
| expiresAfter | long | 是 | 过期时间戳(毫秒) |
| signature | object | 是 | 签名对象 |
| signature.Ecdsa | object | 是 | ECDSA 签名分量 |
| signature.Ecdsa.r | string | 是 | 签名 r 值("0x...") |
| signature.Ecdsa.s | string | 是 | 签名 s 值("0x...") |
| signature.Ecdsa.v | int | 是 | 签名 v 值(0 或 1) |
响应(data):
响应示例
{
"code": 0,
"message": "OK",
"data": {
"serverTimestamp": 1708929600000,
"expireAt": 1708929900000
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
| serverTimestamp | long | 服务端当前时间戳(毫秒) |
| expireAt | long | 本次心跳过期时间戳(毫秒) |
订单语义说明
订单模式支持矩阵
支持矩阵
| 业务场景 | sizeType | tif | size | price 语义 | 支持 |
|---|---|---|---|---|---|
| 限价买入 | base |
gtc / gtd / ioc / fok / alo |
用户输入 shares | 用户输入限价 | ✅ |
| 限价卖出 | base |
gtc / gtd / ioc / fok / alo |
用户输入 shares | 用户输入限价 | ✅ |
| 市价买入(按数量) | base |
ioc / fok |
用户输入 shares | 系统按盘口模拟生成的最差成交价(保护价) | ✅ |
| 市价买入(按金额) | quote |
ioc |
用户输入名义金额(pts) | 系统按盘口模拟生成的最差成交价(保护价) | ✅ |
| 市价卖出(按数量) | base |
ioc / fok |
用户输入 shares | 系统按盘口模拟生成的最差成交价(保护价) | ✅ |
| 市价买入(按金额)+ FOK | quote |
fok |
— | — | ❌ 不支持 |
关键规则
ioc在预测市场中等价于业界常用的fak:能成交多少成交多少,剩余取消,不会留单。fok仅支持按 shares 下单:sizeType=quote与tif=fok组合会被服务端拒绝。alo(Post Only)仅适用于限价单:与sizeType=quote或市价单组合会被拒绝。市价单不是"无保护价单":在下单时根据盘口模拟出
worstPrice,并以此作为price提交至 TradeZone。实际成交价格不会差于worstPrice。tif=gtd必须传expiration:字段路径为action.orders[].orderType.limit.expiration,类型为String(Unix 毫秒时间戳,13 位),绝对时间。未传或非法时当前抛10001 PARAM_ERROR。
不支持组合的错误返回
| 触发场景 | 当前错误码 |
|---|---|
sizeType=quote 且 tif=fok |
10001 PARAM_ERROR |
sizeType=quote 且 tif ∈ {gtc, gtd, alo} |
10001 PARAM_ERROR |
tif=gtd 但未传 expiration |
10001 PARAM_ERROR |
最小下单金额与清仓例外
最小下单金额规则
- 预测市场对单笔名义金额有最小值约束,1pts
清仓例外(Sell-All Exception)
用户卖出某方向持仓后,该 assetId 剩余总持仓(= 可用 + 冻结)为 0,视为清仓卖出
清仓卖出场景下,即使名义成交金额低于 1 pts,也允许提交订单。
价格单位与精度规则
合法范围
| 条件 | 是否允许 |
|---|---|
price ≤ 0 |
拒绝 |
price ≥ 1 |
拒绝 |
0 < price < 1 |
允许,进入精度校验 |
分段精度规则
精度按 [0.04, 0.96] 与区间外分两段:
| 区间 | 最大小数位数 | 等价 cent 区间 | 示例(允许) | 示例(拒绝) |
|---|---|---|---|---|
0 < price < 0.04 |
3 位 | (0, 4) cent |
0.001 / 0.025 / 0.039 |
0.0125(4 位) |
0.04 ≤ price ≤ 0.96 |
2 位(tick=0.01) | [4, 96] cent |
0.04 / 0.25 / 0.96 |
0.045 / 0.123 / 0.961 |
0.96 < price < 1 |
3 位 | (96, 100) cent |
0.962 / 0.9875 / 0.999 |
0.99875(4 位) |
ClientOrderId 生成规范
clientOrderId(简称 cloid)是订单在链上的唯一标识。链上事件全球广播,各区域消费方通过 cloid 中的 region/env 前缀筛选出属于自己的订单。任何接入方生成的 cloid 都必须遵循本规范,否则订单会被误判归属。
格式
格式为 0x{region}{env}{random}:
| 段 | 长度(字符) | 内容 | 说明 |
|---|---|---|---|
| 前缀 | 2 | 0x |
固定字面量 |
| region | 1 | 1 个 hex 字符 | 区域编码 |
| env | 1 | 1 个 hex 字符 | 环境编码 |
| random | 30 | 30 个 hex 字符 | 随机数(小写) |
总长固定 34 字符,全部为小写 hex(0-9, a-f)。
region / env 编码表
| region | 含义 |
|---|---|
0 |
HK |
1 |
US |
2 |
EU |
| env | 含义 |
|---|---|
1 |
线上 |
取值都是 1 个 hex 字符,目前用到 0/1/2,后续扩展不超过 f(15)。
random 生成要求
- 熵:≥ 120 bit(30 个 hex = 120 bit)
- 来源:必须使用密码学安全或等效强度的随机源(如 UUIDv4、
crypto.randomBytes、secrets等) - 编码:小写 hex,不足位高位补 0
参考实现
Java(UUID 派生)
UUID u = UUID.randomUUID();
long hi = u.getMostSignificantBits();
long lo = u.getLeastSignificantBits();
StringBuilder sb = new StringBuilder("0x");
sb.append(Integer.toHexString(region & 0xF));
sb.append(Integer.toHexString(env & 0xF));
for (int i = 14; i >= 0; i--) {
sb.append(Character.forDigit((int)((hi >>> (i * 4)) & 0xF), 16));
}
for (int i = 14; i >= 0; i--) {
sb.append(Character.forDigit((int)((lo >>> (i * 4)) & 0xF), 16));
}
return sb.toString();
Node.js
const crypto = require('crypto');
function generate(region, env) {
const rand = crypto.randomBytes(15).toString('hex'); // 30 hex
return `0x${region.toString(16)}${env.toString(16)}${rand}`;
}
import secrets
def generate(region: int, env: int) -> str:
return f"0x{region:x}{env:x}{secrets.token_hex(15)}"
Go
import (
"crypto/rand"
"encoding/hex"
"fmt"
)
func Generate(region, env int) (string, error) {
b := make([]byte, 15)
if _, err := rand.Read(b); err != nil {
return "", err
}
return fmt.Sprintf("0x%x%x%s", region, env, hex.EncodeToString(b)), nil
}
服务端解析与归属判断
接收方按以下规则判断 cloid 是否属于当前环境:
if cloid is null/empty
or length < 4
or 不以 "0x" 开头
or cloid[2], cloid[3] 不是合法 hex 字符:
视为 HK-预发(region=0, env=0) ← 兜底规则
else:
region = parse_hex(cloid[2])
env = parse_hex(cloid[3])
判断是否等于当前环境的 (region, env)
常见问题
心跳检测: 如果运行机器人,设置心跳以在机器人断线时自动撤销所有订单。每 < 5 分钟发送一次预签名的全部撤单,详情见api文档
Nonce: 使用当前毫秒时间戳。每个 Nonce 只能使用一次。
时间戳偏差: OK-ACCESS-TIMESTAMP 与服务器时间相差不能超过 30 秒,否则返回错误码 50102。
4. 仓位操作
4.1 Split(xp → YES + NO)
将 xp 分割为等量的 YES 和 NO 条件代币对。开发者自行构造 calldata 并签名,服务端校验后直接提交至 TradeZone。
HTTP请求
POST /api/v5/predictions/positions/split
请求体
请求示例
{
"action": {
"type": "predictionSplit",
"marketId": "1",
"size": "100000000"
},
"nonce": 1708929600000,
"signature": {
"Ecdsa": {
"r": "0x...",
"s": "0x...",
"v": 1
}
}
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| action | object | 是 | Split 动作 |
| action.type | string | 是 | 固定 "predictionSplit" |
| action.marketId | string | 是 | 市场 ID(如 "1") |
| action.size | string | 是 | pts 数量(最小单位字符串,如 "100000000") |
| nonce | long | 是 | 请求时间戳(毫秒),防重放 |
| signature | object | 是 | 签名对象 |
| signature.Ecdsa | object | 是 | ECDSA 签名分量 |
| signature.Ecdsa.r | string | 是 | 签名 r 值("0x...") |
| signature.Ecdsa.s | string | 是 | 签名 s 值("0x...") |
| signature.Ecdsa.v | int | 是 | 签名 v 值(0 或 1) |
响应(data):
响应示例
{
"code": 0,
"message": "OK",
"data": {
"txHash": "0xdef...abc"
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
| txHash | string | TradeZone 交易哈希 |
4.2 Merge(YES + NO → xp)
将等量的 YES 和 NO 条件代币合并回 xp(Split 的逆操作)。开发者自行构造 calldata 并签名,服务端校验后直接提交至 TradeZone。
HTTP请求
POST /api/v5/predictions/positions/merge
请求体
请求示例
{
"action": {
"type": "predictionMerge",
"marketId": "1",
"size": "100000000"
},
"nonce": 1708929600000,
"signature": {
"Ecdsa": {
"r": "0x...",
"s": "0x...",
"v": 1
}
}
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| action | object | 是 | Merge 动作 |
| action.type | string | 是 | 固定 "predictionMerge" |
| action.marketId | string | 是 | 市场 ID(如 "1") |
| action.size | string | 是 | 合并数量(最小单位字符串,如 "100000000") |
| nonce | long | 是 | 请求时间戳(毫秒),防重放 |
| signature | object | 是 | 签名对象 |
| signature.Ecdsa | object | 是 | ECDSA 签名分量 |
| signature.Ecdsa.r | string | 是 | 签名 r 值("0x...") |
| signature.Ecdsa.s | string | 是 | 签名 s 值("0x...") |
| signature.Ecdsa.v | int | 是 | 签名 v 值(0 或 1) |
响应(data):
响应示例
{
"code": 0,
"message": "OK",
"data": {
"txHash": "0xdef...abc"
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
| txHash | string | TradeZone 交易哈希 |
4.3 Redeem(结算后兑换 xp)
市场结算后,用胜出的条件代币按 1:1 兑换 xp。兑换数量无需传入,默认兑换用户持有的全部胜出代币。
HTTP请求
POST /api/v5/predictions/positions/redeem
请求体
请求示例
{
"action": {
"type": "predictionRedeem",
"marketId": "1"
},
"nonce": 1708929600000,
"signature": {
"Ecdsa": {
"r": "0x...",
"s": "0x...",
"v": 1
}
}
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| action | object | 是 | Redeem 动作 |
| action.type | string | 是 | 固定 "predictionRedeem" |
| action.marketId | string | 是 | 市场 ID(如 "1") |
| nonce | long | 是 | 请求时间戳(毫秒),防重放 |
| signature | object | 是 | 签名对象 |
| signature.Ecdsa | object | 是 | ECDSA 签名分量 |
| signature.Ecdsa.r | string | 是 | 签名 r 值("0x...") |
| signature.Ecdsa.s | string | 是 | 签名 s 值("0x...") |
| signature.Ecdsa.v | int | 是 | 签名 v 值(0 或 1) |
响应(data):
响应示例
{
"code": 0,
"message": "OK",
"data": {
"txHash": "0xdef...abc"
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
| txHash | string | TradeZone 交易哈希 |
5. 成交历史
5.1 查询成交记录
查询当前认证用户的成交(Fill)记录。服务端从登录态解析 userId 并内部转换为 address 查询。
HTTP请求
GET /api/v5/predictions/trades
请求参数(Query)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| marketId | string | 否 | 按市场 ID 过滤 |
| side | string | 否 | 按方向过滤:BUY / SELL |
| startTime | long | 否 | 起始时间戳(毫秒),包含 |
| endTime | long | 否 | 结束时间戳(毫秒),不包含 |
| cursor | string | 否 | 分页游标 |
| limit | int | 否 | 每页条数(默认 20,最大 100) |
响应(data):
响应示例
{
"code": 0,
"message": "OK",
"data": {
"list": [
{
"tradeId": "1",
"orderId": "1",
"marketId": "1",
"tokenId": "0xabc...001",
"side": "BUY",
"size": "50",
"amount": "32.5",
"price": "0.65",
"fee": "0.065",
"role": "TAKER",
"txHash": "0xdef...789",
"createdAt": 1710000030000
}
],
"nextCursor": "eyJpZCI6MTAwMDF9",
"hasNext": false
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
| list[].tradeId | string | 成交记录 ID |
| list[].orderId | string | 关联订单 ID |
| list[].marketId | string | 市场 ID |
| list[].tokenId | string | 代币地址(YES 或 NO) |
| list[].side | string | BUY / SELL |
| list[].size | string | 成交代币数量 |
| list[].amount | string | 成交 xp 金额 |
| list[].price | string | 成交价格 |
| list[].fee | string | 手续费(xp) |
| list[].role | string | MAKER / TAKER |
| list[].txHash | string | 链上交易哈希 |
| list[].createdAt | string | 成交时间戳(毫秒) |
| nextCursor | string | 下一页游标 |
| hasNext | boolean | 是否有更多数据 |
6. 仓位查询
6.1 查询当前持仓
查询当前认证用户的活跃持仓。
HTTP请求
GET /api/v5/predictions/positions?status=open
6.2 查询已平仓仓位
查询当前认证用户已退出或已结算的仓位。
HTTP请求
GET /api/v5/predictions/positions?status=closed
6.3 查询指定市场仓位
查询当前认证用户在指定市场的仓位。
HTTP请求
GET /api/v5/predictions/positions?marketId={marketId}
6.4 统一端点
以上三种场景均通过同一端点 + 查询参数组合实现:
HTTP请求
GET /api/v5/predictions/positions
请求参数(Query)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| status | string | 否 | open(活跃持仓)、closed(已清仓),不传返回全部 |
| marketId | long | 否 | 按市场 ID 过滤 |
| cursor | string | 否 | 分页游标 |
| limit | int | 否 | 每页条数(默认 20,最大 100) |
响应(data):
响应示例
{
"code": 0,
"message": "OK",
"data": {
"list": [
{
"id": "100001",
"tokenId": "0xabc...001",
"marketId": "1",
"tokenIndex": "1",
"tokenName": "Yes",
"size": "500",
"availableSize": "480",
"value": "325",
"avgPrice": "0.62",
"unRealizedPnl": "15",
"unRealizedPnlPercentage": "0.048",
"title": "Will BTC exceed $100k by end of 2026?",
"icon": "https://cdn.example.com/markets/1.png",
"eventId": "100",
"winningToken": null,
"positionStatus": 1,
"oddsType": "points",
"curPrice": "0.65",
"realizedPnl": "12.5",
"realizedPnlPercentage": "0.04"
}
],
"nextCursor": "eyJpZCI6MTAwMDAxfQ",
"hasNext": false
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
| list[].id | string | 仓位 ID |
| list[].tokenId | string | 代币 ID |
| list[].marketId | string | 市场 ID |
| list[].tokenIndex | string | 代币方向("1" = YES,"2" = NO) |
| list[].tokenName | string | 代币名称("Yes" / "No") |
| list[].size | string | 持仓数量(= remain) |
| list[].availableSize | string | 可用持仓量(= remain − frozen,未被 SELL 挂单锁定的数量) |
| list[].value | string | 当前市值(curPrice × size) |
| list[].avgPrice | string | 加权平均持仓成本 |
| list[].unRealizedPnl | string | 未实现盈亏 |
| list[].unRealizedPnlPercentage | string | 未实现盈亏百分比 |
| list[].title | string | 市场问题文本 |
| list[].icon | string | 市场图标 URL |
| list[].eventId | string | 父事件 ID |
| list[].winningToken | string | 结算后获胜方代币 ID;未结算时为 null |
| list[].positionStatus | integer | 仓位状态码(见 PositionStatusEnum) |
| list[].oddsType | string | 盘口类型:points=积分盘 |
| list[].curPrice | string | 当前代币实时价格 |
| list[].realizedPnl | string | 已实现盈亏 |
| list[].realizedPnlPercentage | string | 已实现盈亏百分比 |
| nextCursor | string | 下一页游标 |
| hasNext | boolean | 是否有更多数据 |
7. 账户余额
7.1 查询账户余额
查询当前认证用户的积分余额。
HTTP请求
GET /api/v5/predictions/balance
请求参数:无
响应(data):
响应示例
{
"code": 0,
"message": "OK",
"data": [
{ "oddsType": "points", "balance": "2.2", "available": "2.2" }
]
}
data 为数组,每个元素对应一种盘口类型的余额:
| 字段 | 类型 | 说明 |
|---|---|---|
| oddsType | string | 盘口类型:points=积分盘 |
| balance | string | 总余额 |
| available | string | 可用余额(总余额 - 冻结中的金额) |
8. 限流(Rate Limits)
事件与市场 API
| 端点 | 限额 |
|---|---|
GET /api/v5/predictions/events |
20 次 / 1s |
GET /api/v5/predictions/events/{eventId} |
20 次 / 1s |
GET /api/v5/predictions/events/{eventId}/markets |
20 次 / 1s |
GET /api/v5/predictions/markets/{marketId} |
20 次 / 1s |
GET /api/v5/predictions/events/search |
10 次 / 1s |
价格 API(行情数据)
| 端点 | 限额 |
|---|---|
GET /api/v5/market/ticker |
10 次 / 1s |
GET /api/v5/market/candles |
20 次 / 1s |
GET /api/v5/market/pm-books |
20 次 / 1s |
订单 API
查询类
| 端点 | 限额 |
|---|---|
GET /api/v5/predictions/orders/{orderId} |
20 次 / 1s |
GET /api/v5/predictions/orders |
20 次 / 1s |
写操作类
| 端点 | 限额 |
|---|---|
POST /api/v5/predictions/orders |
50 次 / 1s |
POST /api/v5/predictions/orders/cancel |
20 次 / 1s |
POST /api/v5/predictions/orders/cancel-all |
1 次 / 1h |
POST /api/v5/predictions/heartbeat |
1 次 / 1s |
仓位操作 API
| 端点 | 限额 |
|---|---|
POST /api/v5/predictions/positions/split |
5 次 / 1s |
POST /api/v5/predictions/positions/merge |
5 次 / 1s |
POST /api/v5/predictions/positions/redeem |
5 次 / 1s |
成交历史 / 仓位查询 / 余额
| 端点 | 限额 |
|---|---|
GET /api/v5/predictions/trades |
10 次 / 1s |
GET /api/v5/predictions/positions |
10 次 / 1s |
GET /api/v5/predictions/balance |
10 次 / 1s |
触发限流的响应
- HTTP Status:
429 Too Many Requests - 业务错误码:
50011 RATE_LIMIT_EXCEEDED - 建议客户端按指数退避策略重试:首次 1s,后续每次翻倍,最大 30s;连续重试超过 5 次仍 429 时应停止并告警
SDK API 参考
okx-outcomes-sdk Rust crate 公开的每一个公共方法、请求体、响应结构、错误变体和 WebSocket 频道的完整参考。README 是快速入门;本文档是详细版本。
通篇约定:
- 所有 Rust 类型都是从
okx_outcomes_sdk::*重新公开导出的pub类型(每个章节顶部给出具体模块路径)。 - 每一个 REST 调用的"认证"指的是通过
ApiCredentials用 HMAC-SHA256 在本地签名后写入的 OKX RESTOK-ACCESS-*请求头。SDK 在本地签名;密钥永远不会离开进程。 - "写"操作(下单 / 撤单 / 拆分 / 合并 / 赎回 / 心跳)还额外要求对类型化的 action 进行 EIP-712 ECDSA 签名。参见下面的 签名 章节。
- 所有十进制数值(价格、数量、余额、盈亏)都以十进制字符串形式交换,避免浮点精度损失。
- 所有时间戳都是 Unix 毫秒。
- 通信中的字段名使用 camelCase;Rust struct 字段使用 snake_case,通过
serde(rename_all = "camelCase")转换。
1. 安装
在 Cargo.toml 中通过 Git 添加依赖:
Cargo.toml 依赖配置
[dependencies]
okx-outcomes-sdk = { git = "https://github.com/okx/outcomes-sdk.git", features = ["signing", "websocket"] }
2. 客户端构造
模块: okx_outcomes_sdk::{OutcomesSdkClient, ApiCredentials}。
pub struct ApiCredentials {
pub api_key: String, // OK-ACCESS-KEY 请求头的值
pub secret_key: String, // HMAC-SHA256 签名密钥;永不传输
pub passphrase: String, // OK-ACCESS-PASSPHRASE 请求头的值
}
impl OutcomesSdkClient {
pub fn with_credentials(creds: ApiCredentials) -> Self;
pub fn with_credentials_and_url(creds: ApiCredentials, base_url: impl Into<String>) -> Self;
}
Base URL 解析顺序:传给 with_credentials_and_url 的显式参数 > PREDICTIONS_API_BASE 环境变量 > https://www.okx.com。Endpoint 常量是完整的绝对路径(/api/v5/predictions/...、/api/v5/market/...),与 base URL 拼接,因此一个主机配置同时覆盖预测与市场数据两类调用。
3. 错误
模块: okx_outcomes_sdk::SdkError。
每一个可失败的调用都返回 Result<T, SdkError>。该枚举有七个变体:
pub enum SdkError {
/// 网络故障:连接被拒绝、DNS、超时、TLS 握手失败。
/// 传输层;通常重试是安全的。
Http(reqwest::Error),
/// 服务端在响应信封中返回了非零业务错误码。
/// `code` 是上游 OKX 的业务码;根据它决定重试 / 退避 / 中止。
Api { code: i64, message: String },
/// 响应体无法按预期 schema 反序列化。
/// 通常意味着 SDK 与服务端版本不匹配。
Deserialize(serde_json::Error),
/// WS 连接、发送、登录或关闭失败。
/// 包括登录被拒(`60xxx` 错误码)和登录过程中的超时。
WebSocket { message: String },
/// 保留给绕过公共构造函数、最终没有附带凭据的调用方。
/// 通过 `with_credentials` / `with_credentials_and_url` 构造的客户端不会
/// 返回此变体——它们始终携带凭据。
NotAuthenticated { hint: String },
/// URL 不合法、状态缺失或意料之外的内部前置条件。
/// 几乎总是调用方的编程错误。
Internal { message: String },
/// 序列化请求体或 WS 载荷失败。
/// 实际很少见(仅在出现非有限浮点数等情况下才触发)。
Serialization { message: String },
}
各变体通过 thiserror 实现了 Display,因此 format!("{e}") 会生成可读的一行日志。
4. 事件与市场
模块: okx_outcomes_sdk::models::event::*。API 实现位于 okx_outcomes_sdk::api::events。
4.1 获取事件列表 get_events
获取预测市场事件的分页列表。
- 端点:
GET /api/v5/predictions/events - 认证: 必需
pub async fn get_events(
&self,
status: Option<&str>, // "active"(默认)| "resolved"
tag: Option<&str>, // 体育标签 ID
league_id: Option<&str>, // 体育联赛 ID
sort: Option<&str>, // "volume" | "volume_24h"(默认) | "ending_soon" | "newest"
cursor: Option<&str>, // 上一次 `EventsResponse.pagination.next_cursor` 返回的分页游标
page_size: Option<i32>, // 每页条目数,最大 50(默认 10)
) -> Result<EventsResponse, SdkError>;
pub struct EventsResponse {
events: Vec<EventObject>,
pagination: Pagination, // 见通用类型
}
pub struct EventObject {
id: String, // 全局唯一事件 ID
event_id: String, // 事件 ID
neg_risk: bool, // 互斥(negRisk)事件
status: EventStatus, // Active / Paused / Resolved / Unknown
event_title: String, // 展示标题
description: String, // 长描述
event_icon: Option<String>, // 图标 URL
volume: String, // 所有市场的总交易量
start_time: Option<i64>, // 开始交易时间(ms)
end_time: Option<i64>, // 结束交易时间(ms)
created_at: i64, // 创建时间戳(ms)
total_markets_count: i32, // 该事件下的市场数量
final_outcomes_market_id: Option<String>, // 结算后的胜出市场 ID
markets: Vec<MarketObject>, // 列表端点最多返回 2 个市场;
// 需要完整列表请调用 `get_event_markets`
}
4.2 搜索事件 search
通过关键字搜索事件和市场。
- 端点:
GET /api/v5/predictions/events/search - 认证: 必需
pub async fn search(
&self,
keyword: &str, // 自由文本查询(必需)
cursor: Option<&str>, // 分页游标
page_size: Option<i32>, // 默认 10
) -> Result<EventsResponse, SdkError>;
响应: EventsResponse(与 get_events 形状相同)。
4.3 获取单个事件 get_event
获取单个事件,并内联其完整市场列表。
- 端点:
GET /api/v5/predictions/events/{eventId} - 认证: 必需
pub async fn get_event(&self, event_id: &str) -> Result<EventObject, SdkError>;
事件 ID 不存在时返回 Api { code: 40404, ... }。
4.4 获取事件下市场 get_event_markets
获取事件的全部市场(无分页、无列表上限)。
- 端点:
GET /api/v5/predictions/events/{eventId}/markets - 认证: 必需
pub async fn get_event_markets(&self, event_id: &str) -> Result<MarketsResponse, SdkError>;
pub struct MarketsResponse {
markets: Vec<MarketObject>,
}
pub struct MarketObject {
id: String, // 全局唯一市场 ID
market_id: String, // 市场 ID
neg_risk: bool, // negRisk 市场标志
status: MarketStatus, // Active / Paused / Settling / Resolved / Unknown
settle_stage: i32, // 0 = 未开始,5 = 已结算
question: String, // 完整市场问题
short_question: Option<String>, // 简短问题
description: String, // 长描述
market_icon: Option<String>, // 图标 URL
start_time: String, // 交易开始时间(ms,字符串)
end_time: String, // 交易结束时间(ms,字符串)
resolve_start_at: String, // 结算窗口开始时间(ms,字符串)
resolve_at: String, // 结算时间(ms,字符串)
best_bid: Option<String>, // [0, 1] 区间内的十进制;没有买单时为 None
best_ask: Option<String>, // [0, 1] 区间内的十进制;没有卖单时为 None
last_trade_price: Option<String>, // 第一笔成交前为 None
volume: String, // 市场交易量
probability: Option<String>, // [0, 1] 区间内的 YES 结果概率
resolution_sources: Vec<String>, // 用于裁定的来源 URL
yes_outcome: OutcomeObject,
no_outcome: OutcomeObject,
}
pub struct OutcomeObject {
token_id: Option<String>, // 条件代币地址;部署前为 None
asset_id: Option<String>, // 资产 ID;在下单 / 行情中作为 `inst_id` 使用
name: String, // "Yes" 或 "No"
price: String, // [0, 1] 区间内的十进制
final_result: Option<bool>, // Some(true) = 胜出, Some(false) = 落败, None = 未结算
}
4.5 获取单个市场 get_market
获取单个市场。
- 端点:
GET /api/v5/predictions/markets/{marketId} - 认证: 必需
pub async fn get_market(&self, market_id: &str) -> Result<MarketObject, SdkError>;
5. 账户:余额
模块: okx_outcomes_sdk::models::balance::*。API 位于 okx_outcomes_sdk::api::balance。
5.1 查询余额 get_balance
返回已认证用户按赔率类型分组的可用余额。
- 端点:
GET /api/v5/predictions/balance - 认证: 必需
pub async fn get_balance(&self) -> Result<BalanceResponse, SdkError>;
pub type BalanceResponse = Vec<BalanceEntry>;
pub struct BalanceEntry {
odds_type: OddsType, // Spots / Points / Unknown(spots = 实盘,points = 积分盘)
balance: String, // 总余额(单位由 odds_type 决定)
available: String, // 可用余额(总余额 - 被未成交订单冻结的金额)
}
6. 账户:订单
模块: okx_outcomes_sdk::models::order::*。API 位于 okx_outcomes_sdk::api::orders。
6.1 下单 place_order
提交一个已签名的限价(或触发)订单。
- 端点:
POST /api/v5/predictions/orders - 认证: 必需(REST 凭据)+ EIP-712 签名
pub async fn place_order(&self, req: &PlaceOrderRequest) -> Result<TxHashResponse, SdkError>;
struct PlaceOrderRequest {
action: PlaceOrderAction,
nonce: i64, // 毫秒时间戳,防重放
signature: SignatureWrapper, // { Ecdsa: { r, s, v } }
}
struct PlaceOrderAction {
action_type: String, // 始终为 "placeOrder"
grouping: String, // 始终为 "na"
orders: Vec<OrderItem>,
}
struct OrderItem {
asset_id: String, // 结果 assetId
side: SigningOrderSide, // Buy / Sell(下单侧小写 wire,字节用于 EIP-712 哈希)
market_type: String, // 始终为 "prediction"
client_order_id: Option<String>, // 34 字符客户端订单 ID;见签名 > 客户端订单 ID
price: String, // [0, 1] 区间内的十进制
reduce_only: bool,
size: String, // 十进制
size_type: SizeType, // Base(默认,wire 上省略)/ Quote
order_type: OrderTypeSpec, // { limit: { tif } }
}
struct OrderTypeSpec { limit: LimitOrderType }
struct LimitOrderType { tif: LimitTif /* Gtc | Gtd { expires_after } | Ioc | Fok | Alo */ }
响应: TxHashResponse { tx_hash: String }。
构造类型化的 signing::types::OrderRequest,用 signing::sign_to_wrapper 签名,再通过 OrderItem::from(&OrderRequest) 推导出通信侧的 OrderItem,以保证签名字节和 JSON 请求体不会发生漂移。参见签名。
6.2 撤单 cancel_order
撤销一个活跃订单(按服务端 ID 或客户端订单 ID)。
- 端点:
POST /api/v5/predictions/orders/cancel - 认证: 必需 + EIP-712 签名
pub async fn cancel_order(&self, req: &CancelOrderRequest) -> Result<TxHashResponse, SdkError>;
struct CancelOrderRequest {
action: CancelOrderAction,
nonce: i64,
signature: SignatureWrapper,
}
struct CancelOrderAction {
action_type: String, // 始终为 "cancel"
cancels: Vec<CancelItem>,
}
struct CancelItem {
asset_id: String,
market_type: String, // "prediction"
// 二者必选其一:
by: CancelBy, // 扁平化序列化为 { "oid": ... } 或 { "clientOrderId": ... }
}
enum CancelBy {
Oid { oid: String }, // 服务端分配,十进制字符串
ClientOrderId { client_order_id: String }, // 34 字符客户端订单 ID,带 0x 前缀的十六进制
}
响应: TxHashResponse { tx_hash: String }。
6.3 全部撤单 cancel_all
撤销所有活跃订单,或者撤销指定 asset ID 集合下的所有活跃订单。
- 端点:
POST /api/v5/predictions/orders/cancel-all - 认证: 必需 + EIP-712 签名
pub async fn cancel_all(&self, req: &CancelAllRequest) -> Result<TxHashResponse, SdkError>;
struct CancelAllRequest {
action: CancelAllAction,
nonce: i64,
expires_after: i64, // 过期时间戳(ms),必填
signature: SignatureWrapper,
}
struct CancelAllAction {
action_type: String, // 始终为 "cancelAll"
asset_ids: Vec<String>, // 空 = 所有市场;非空 = 过滤
market_type: String, // "prediction"
}
响应: TxHashResponse。
6.4 心跳 heartbeat
刷新用于保护活跃订单的断线自动撤单开关(dead-man's switch)。
- 端点:
POST /api/v5/predictions/heartbeat - 认证: 必需 + EIP-712 签名
pub async fn heartbeat(&self, req: &CancelAllRequest) -> Result<HeartbeatResponse, SdkError>;
struct HeartbeatResponse {
server_timestamp: i64, // 服务端当前时间(ms)
expire_at: i64, // 本次心跳过期的时间(ms)
}
请求体使用同样的 CancelAllRequest 形状:签名好的载荷就是预授权的 cancel-all——心跳超时后服务端会代为执行它。将 nonce 设为 now_ms,expires_after 设为 now_ms + 300_000(5 分钟)。心跳调用频率应高于每 5 分钟一次。
6.5 查询单个订单 get_order
通过服务端分配的 ID 查询单个订单。
- 端点:
GET /api/v5/predictions/orders/{orderId} - 认证: 必需
pub async fn get_order(&self, order_id: &str) -> Result<OrderRecord, SdkError>;
pub struct OrderRecord {
id: String, // 服务端分配的订单 ID
oid: String, // 订单 oid(与 `id` 不同)
market_id: String,
token_id: String, // YES/NO 代币合约地址
asset_id: String, // YES 或 NO 结果资产 ID
client_order_id: Option<String>, // 下单时提供的客户端订单 ID(如果有)
side: OrderSide, // Buy / Sell / Unknown
order_type: TimeInForce, // Gtc / Gtd / Ioc / Fok / PostOnly / Unknown
size_type: OrderSizeType, // Base / Quote / Unknown
size: String, // 十进制
price: String, // 十进制
expiration: Option<String>, // GTD 过期时间(ms,字符串);非 GTD 时为 None
tx_hash: String, // 提交交易哈希
status: RestOrderStatus, // PendingPlace / Active / PendingCancel / Filled /
// PartiallyFilled / Failed / Cancelled / Expired / Unknown
filled_size: String, // 十进制
filled_amount: String, // 十进制
fail_reason: Option<String>, // 仅当 status == RestOrderStatus::Failed 时出现
cancel_reason: Option<String>, // 服务端发起撤单时设置(心跳超时、市场结算等)
odds_type: OddsType, // Spots / Points / Unknown
created_at: String, // Unix ms(字符串)
updated_at: String, // Unix ms(字符串)
}
6.6 查询订单列表 list_orders
列出已认证用户的订单。
- 端点:
GET /api/v5/predictions/orders - 认证: 必需
pub async fn list_orders(
&self,
market_id: Option<&str>, // 按市场 ID 过滤
status: Option<&str>, // "open"(待处理 + 活跃)| "closed"(成交 / 已撤 / 已过期 / 失败)
cursor: Option<&str>, // 分页游标
limit: Option<i32>, // 最大 50,默认 20
) -> Result<OrdersResponse, SdkError>;
// 共享分页响应外壳的类型别名。
pub type OrdersResponse = PagedListResponse<OrderRecord>;
// pub struct PagedListResponse<T> { list: Vec<T>, next_cursor: Option<String>, has_next: bool }
7. 账户:持仓
模块: okx_outcomes_sdk::models::position::*。API 位于 okx_outcomes_sdk::api::positions。
7.1 查询持仓 get_positions
查询已认证用户的持仓。
- 端点:
GET /api/v5/predictions/positions - 认证: 必需
pub async fn get_positions(
&self,
status: Option<&str>, // "open" | "closed";不传则全部
market_id: Option<&str>,
cursor: Option<&str>, // 分页游标
limit: Option<i32>, // 最大 100,默认 20
) -> Result<PositionsResponse, SdkError>;
pub type PositionsResponse = PagedListResponse<PositionRecord>;
pub struct PositionRecord {
id: String, // 标识符
token_id: String,
market_id: String,
token_index: String, // "1" = YES, "2" = NO
token_name: String, // "Yes" 或 "No"
size: String, // 当前剩余数量
available_size: String, // 可用数量(= size − 被卖单冻结的部分)
value: String, // cur_price * size
avg_price: String, // 加权平均建仓成本
un_realized_pnl: String, // 未实现盈亏
un_realized_pnl_percentage: String,
title: String, // 展示字符串
icon: String, // 展示字符串
event_id: String,
winning_token: Option<String>, // 结算后的胜出代币 ID;结算前为 None
position_status: i32, // 持仓状态码(完整枚举见 API 参考)
cur_price: String, // 当前代币价格
realized_pnl: String, // 已实现盈亏
realized_pnl_percentage: String,
odds_type: OddsType, // Spots / Points / Unknown
//(已经线上验证:wire 值为 "points" 或 "spots",不是 "real")
}
8. 账户:成交
模块: okx_outcomes_sdk::models::trade::*。API 位于 okx_outcomes_sdk::api::trades。
8.1 查询成交 get_trades
查询已认证用户的成交历史。
- 端点:
GET /api/v5/predictions/trades - 认证: 必需
pub async fn get_trades(
&self,
market_id: Option<&str>, // 按市场 ID 过滤
side: Option<&str>, // "BUY" | "SELL"
start_time: Option<i64>, // 开始时间(含,ms)
end_time: Option<i64>, // 结束时间(不含,ms)
cursor: Option<&str>, // 分页游标
limit: Option<i32>, // 最大 100,默认 20
) -> Result<TradesResponse, SdkError>;
type TradesResponse = PagedListResponse<TradeRecord>;
struct TradeRecord {
trade_id: String, // TAKER 行和链上分配前的 MAKER 行返回空串
order_id: String,
market_id: String,
token_id: String,
side: OrderSide, // Buy / Sell / Unknown
size: String, // 成交代币数
amount: String, // 成交金额
price: String,
fee: String,
role: Role, // Maker / Taker / Unknown
tx_hash: String,
created_at: String, // Unix ms(字符串)
}
trade_id 在 TAKER 行,以及在链上 trade-id 分配机制之前的历史 MAKER 行中为 None。
9. 条件代币
模块: okx_outcomes_sdk::models::position::*(与 positions 共用)。API 位于 okx_outcomes_sdk::api::positions。
三者均为需要 EIP-712 签名的写操作。每个请求体的外层形状一致: { action, nonce, signature },只是 action 不同。每个均返回 TxHashResponse { tx_hash: String }。
9.1 拆分 split
将一个市场的等额 xp 拆分为等量的 YES + NO 代币(merge 的逆操作)。
- 端点:
POST /api/v5/predictions/positions/split - 认证: 必需 + EIP-712 签名
pub async fn split(&self, req: &SplitRequest) -> Result<TxHashResponse, SdkError>;
struct SplitRequest { action: SplitAction, nonce: i64, signature: SignatureWrapper }
struct SplitAction {
action_type: String, // "predictionSplit"
market_id: String,
size: String, // 最小单位数量
}
9.2 合并 merge
将等量的 YES + NO 代币合并(回到 xp)。
- 端点:
POST /api/v5/predictions/positions/merge - 认证: 必需 + EIP-712 签名
pub async fn merge(&self, req: &MergeRequest) -> Result<TxHashResponse, SdkError>;
struct MergeRequest { action: MergeAction, nonce: i64, signature: SignatureWrapper }
struct MergeAction {
action_type: String, // "predictionMerge"
market_id: String,
size: String,
}
9.3 赎回 redeem
在市场结算后,赎回调用方的全部胜出代币余额。没有 size 字段;服务端会赎回调用方持有的全部数量。
- 端点:
POST /api/v5/predictions/positions/redeem - 认证: 必需 + EIP-712 签名
pub async fn redeem(&self, req: &RedeemRequest) -> Result<TxHashResponse, SdkError>;
struct RedeemRequest { action: RedeemAction, nonce: i64, signature: SignatureWrapper }
struct RedeemAction {
action_type: String, // "predictionRedeem"
market_id: String,
}
市场尚未裁定时返回 Api { code: 51020, ... }。
10. 市场数据
模块: okx_outcomes_sdk::models::price::*。API 位于 okx_outcomes_sdk::api::prices。
这些调用访问 OKX 的市场数据 API https://www.okx.com/api/v5/market/* —— 与预测市场 API 同主机但路径前缀和响应信封都不同。市场数据信封把 code 包成 JSON 字符串,因此 SdkError::Api { code } 中的 code 是解析后的整数值。
10.1 获取行情 get_ticker
单个 instrument 的最新报价。inst_id 是市场的 yes_outcome.asset_id。
- 端点:
GET /api/v5/market/ticker - 认证: 必需
pub async fn get_ticker(&self, inst_id: &str) -> Result<Ticker, SdkError>;
pub struct Ticker {
inst_type: String,
inst_id: String,
last: String, // 最新成交价
last_sz: String, // 最新成交数量
ask_px: String, // 盘口最优卖价
ask_sz: String, // 盘口最优卖单数量
bid_px: String, // 盘口最优买价
bid_sz: String, // 盘口最优买单数量
open24h: String, // 24 小时开盘价
high24h: String, // 24 小时最高价
low24h: String, // 24 小时最低价
vol24h: String, // 24 小时成交量(基础币)
vol_ccy24h: String, // 24 小时成交量(报价币)
sod_utc0: String, // UTC 0 开盘价
sod_utc8: String, // UTC+8 开盘价
ts: String, // 更新时间戳(Unix ms 十进制字符串)
}
服务端返回 1 元素数组;SDK 会拆包。当 inst ID 未知时返回 Api { code: -1, message: "ticker not found" }。
10.2 获取K线 get_candles
K 线历史。
- 端点:
GET /api/v5/market/candles - 认证: 必需
pub async fn get_candles(
&self,
inst_id: &str,
bar: Option<&str>, // "1m" / "5m" / "15m" / "30m" / "1H" / "4H" / "1D" / ... ;默认 "1m"
after: Option<&str>, // 返回时间戳在该值**之前**的 K 线(ms)
before: Option<&str>, // 返回时间戳在该值**之后**的 K 线(ms)
limit: Option<i32>, // 最大 300,默认 100
) -> Result<Vec<Candle>, SdkError>;
pub struct Candle(pub Vec<String>);
impl Candle {
pub fn ts(&self) -> &str; // 索引 0:开盘时间(Unix ms 字符串)
pub fn open(&self) -> &str; // 索引 1:开盘价
pub fn high(&self) -> &str; // 索引 2:最高价
pub fn low(&self) -> &str; // 索引 3:最低价
pub fn close(&self) -> &str; // 索引 4:收盘价
pub fn vol(&self) -> &str; // 索引 5:成交量(合约张数)
// 索引 6:以计价币计的成交量(无 helper)
// 索引 7:以报价币计的成交量(无 helper)
pub fn confirmed(&self) -> bool; // 索引 8:为 "1" 时返回 true(K 线已收盘)
}
10.3 获取深度 get_pm_books
预测市场盘口深度快照。
- 端点:
GET /api/v5/market/pm-books - 认证: 必需
- 限频: 40 次 / 2 秒
pub async fn get_pm_books(
&self,
inst_id: &str, // YES 结果资产 ID
sz: Option<i32>, // 单边深度档位数;最大 400(双边合计最多 800)。
// 不传时默认 1(仅 BBO)。
) -> Result<PmBookDepth, SdkError>;
pub struct PmBookDepth {
asks: Vec<Vec<String>>, // 卖盘档位,按价格升序。每条为 [price, size, order_count]。
bids: Vec<Vec<String>>, // 买盘档位,按价格降序。每条为 [price, size, order_count]。
ts: String, // 快照时间戳(Unix ms 十进制字符串)
seq_id: i64, // 盘口版本序列;对多数调用方不透明,
// 仅为了与 API 响应对齐而暴露
}
服务端返回 1 元素的 data 数组;SDK 会拆包。响应为空时返回 Api { code: -1, message: "pm-books snapshot not found" }。
11. WebSocket
模块: okx_outcomes_sdk::ws::*。需要 websocket Cargo feature。
连接模型
Open API 对公共频道和私有频道使用同一个端点: wss://<host>/ws/v5/business。公共频道可匿名使用。私有频道在 WS 握手之后需要一次性的 op: "login"。
主机:
pub mod ws::endpoints {
pub const DEFAULT_WS_HOST: &str = "wss://ws.okx.com:8443";
pub const EU_WS_HOST: &str = "wss://wseea.okx.com";
pub const US_WS_HOST: &str = "wss://wsus.okx.com";
pub const BUSINESS_PATH: &str = "/ws/v5/business";
}
PredictionsWsClient 默认使用 DEFAULT_WS_HOST;可通过 PredictionsWsClient::with_host(...) 或 PREDICTIONS_WS_HOST 环境变量覆盖。
生命周期与韧性:
- 25 秒心跳保活(OKX 要求 < 30 秒)。
- 自动重连,指数退避(3 秒 -> 6 秒 -> 12 秒 -> 最高 30 秒封顶)。
- 重连时,若已存储凭据,客户端会重放登录,并重新订阅断线时仍处于活跃状态的每一个频道。
- 每次状态切换都会触发
connection_state_callback("public" | "private", connected: bool)。
公共 API
pub struct PredictionsWsClient { /* ... */ }
impl PredictionsWsClient {
pub fn new() -> Self;
pub fn with_host(host: &str) -> Self;
pub async fn connect(&self, path: &str) -> Result<(), SdkError>;
pub async fn login(&self, creds: &ApiCredentials) -> Result<(), SdkError>;
pub async fn subscribe(&self, channel: &str, params: Vec<HashMap<String, String>>) -> Result<(), SdkError>;
pub async fn unsubscribe(&self, channel: &str, params: Vec<HashMap<String, String>>) -> Result<(), SdkError>;
pub async fn disconnect(&self);
pub fn set_on_data(&self, callback: WsDataCallback);
pub fn set_on_connection_state(&self, callback: WsConnectionStateCallback);
}
pub type WsDataCallback = Arc<dyn Fn(&WsMessage) + Send + Sync>;
pub type WsConnectionStateCallback = Arc<dyn Fn(&str, bool) + Send + Sync>;
subscribe 是幂等的:使用相同的 (channel, params) 对调用两次不会重复订阅,也不会在重放列表中产生重复条目。
登录签名(由 login 内部处理):SDK 计算 sign = Base64(HMAC-SHA256(secret_key, timestamp + "GET" + "/users/self/verify")),并发送:
{"op": "login", "args": [{"apiKey": "...", "passphrase": "...", "timestamp": "...", "sign": "..."}]}
login 返回的 future 只有在服务端响应后才会 resolve:
{"event":"login","code":"0"}->Ok(())。{"event":"error","code":"600xx",...}->Err(SdkError::WebSocket { message: "Login rejected: [60xxx] ..." })。- 30 秒内无响应 ->
Err(SdkError::WebSocket { message: "Login timed out (30s)" })。
消息分发
每个传入的 JSON 帧都会被解析一次成 WsMessage 枚举,然后交给 on_data。消费方永远看不到原始 JSON。
pub enum WsMessage {
Event {
event: String,
channel: Option<String>,
inst_id: Option<String>,
msg: Option<String>,
},
Prices(Vec<WsPriceTick>),
Books { data: Vec<WsPmBookData>, action: String }, // action = "snapshot" | "update"
Trades(Vec<WsPmTrade>),
Tickers(Vec<WsPmTicker>),
Game(Vec<WsGameStatus>),
EventStatus(Vec<WsEventStatus>),
Candle(Vec<Candle>), // 类型化包装,9 列 OHLCV 数组
Orders(Vec<WsOrder>),
Positions(Vec<WsPosition>),
UserTrades(Vec<WsUserTrade>),
Balance(Vec<WsBalance>),
Pnl(Vec<WsPnl>),
Unknown { channel: String, raw: serde_json::Value },
}
WsMessage::Event 承载 event:"subscribe"|"unsubscribe"|"login"|"error" 等确认消息,与任何数据频道无关。
公共频道
11.1 prediction-market-prices
按市场的价格 tick。
- 订阅参数:
[{"instId": "<asset_id>"}](每个市场一条)。 - 消息变体:
WsMessage::Prices(Vec<WsPriceTick>)。
struct WsPriceTick {
yes_asset_id: String,
last_trade_price: Option<String>, // 首次成交前为 None
best_bid: Option<String>, // 没有买单时为 None
best_ask: Option<String>, // 没有卖单时为 None
timestamp: String, // Unix ms 十进制字符串
probability: String, // 基点 * 100,例如 "6500" = 65.00%
market_volume: String,
event_volume: String,
event_id: String,
}
11.2 pm-books
盘口快照和增量更新。
- 订阅参数:
[{"instId": "<asset_id>"}]。 - 消息变体:
WsMessage::Books { data, action },其中action为"snapshot"(订阅 / 重连时的全量快照)或"update"(增量 delta)。
struct WsPmBookData {
asks: Vec<Vec<String>>, // [[price, size, ...], ...]
bids: Vec<Vec<String>>, // [[price, size, ...], ...]
ts: String,
checksum: Option<i64>, // 规范化盘口的 CRC32 完整性校验
seq_id: Option<i64>, // 单调序列;出现间隙 = 丢失,应重置
prev_seq_id: Option<i64>, // 首个快照为 -1
}
当 prev_seq_id 与上一帧的 seq_id 不匹配时,丢弃本地盘口,等待下一个 snapshot。
11.3 pm-trades
公开成交回报。
- 订阅参数:
[{"instId": "<asset_id>"}]。 - 消息变体:
WsMessage::Trades(Vec<WsPmTrade>)。
struct WsPmTrade {
inst_id: String,
trade_id: Option<String>, // 单笔推送:Some;聚合推送:None
f_id: Option<String>, // 聚合推送:首个 trade id;单笔:None
l_id: Option<String>, // 聚合推送:最后一个 trade id;单笔:None
px: String, // 价格
sz: String, // 数量
side: String, // "buy" / "sell"(taker 方)
ts: String,
}
通过 trade_id 与 (f_id,l_id) 中哪一对为 Some 来区分单笔推送和窗口聚合推送。
11.4 pm-tickers
OKX 风格的按 instrument ticker 推送。
- 订阅参数:
[{"instId": "<asset_id>"}]。 - 消息变体:
WsMessage::Tickers(Vec<WsPmTicker>)。
struct WsPmTicker {
inst_type: String, inst_id: String,
last: String, last_sz: String,
ask_px: String, ask_sz: String,
bid_px: String, bid_sz: String,
open24h: String, high24h: String, low24h: String,
vol24h: String, vol_ccy24h: String,
sod_utc0: String, sod_utc8: String,
ts: String,
}
11.5 pm-event-status
事件结算推送。
- 订阅参数:
[{"instId": "event-<event_id>"}]。SDK 不会自动在 WS 路径上加event-前缀;请显式传入。 - 消息变体:
WsMessage::EventStatus(Vec<WsEventStatus>)。
struct WsEventStatus {
event_id: String,
status: String, // 例如 "resolved"
market_id: String, // 胜出市场 ID
outcome_option: String, // "yes" / "no" / "others" / 球队名 / "draw"
timestamp: String,
}
11.6 pm-candle*
K 线流。频道名编码了 bar: pm-candle1m、pm-candle5m、pm-candle1H、pm-candle1D 等等。
- 订阅参数:
[{"instId": "<asset_id>"}]。 - 消息变体:
WsMessage::Candle(Vec<Candle>),每个Candle是 9 列 OHLCV 数组的类型化包装。访问方法:ts(),open(),high(),low(),close(),vol(),vol_ccy(),vol_ccy_quote(),confirmed()。
私有频道(需要 login)
订阅时传空参数(服务端将订阅范围限定到已登录账户)。
11.7 pm-order
订单状态变更。
- 订阅参数:
[]。 - 消息变体:
WsMessage::Orders(Vec<WsOrder>)。
struct WsOrder {
order_id: String,
market_id: String,
status: OrderStatus, // Active / Filled / PartiallyFilled / PlaceFailed /
// CancelFailed / Cancelled / Expired / Unknown
side: OrderSide, // Buy / Sell / Unknown
// 以下字段都依赖 status —— 见 spec 的 status → 必备字段表。
// 用 Option 建模,缺失键反序列化为 None。
client_order_id: Option<String>,
asset_id: Option<String>, // YES 或 NO 的 assetId
direction: Option<Direction>, // Yes / No / Unknown —— 该订单走的结果方向
filled_size: Option<String>,
order_size: Option<String>, // serde alias = "size"
avg_price: Option<String>,
amount: Option<String>, // BUY = 花费, SELL = 收到(xp)
limit_price: Option<String>, // serde alias = "price"
fail_message: Option<String>, // 仅 PLACE_FAILED / CANCEL_FAILED 出现
odds_type: Option<OddsType>,
tx_hash: Option<String>, // serde rename = "txHash"
trade_id: Option<String>,
}
11.8 pm-position
持仓更新。
- 订阅参数:
[]。 - 消息变体:
WsMessage::Positions(Vec<WsPosition>)。
该频道有两种 payload 变体;WsPosition 用单一扁平 struct 表示,
变体相关字段都放在 Option 里。基于 status 分支判断(可用
PositionStatus::is_position_snapshot() / is_failed())以决定
哪些字段是有意义的。
struct WsPosition {
// 两个变体共有
market_id: String,
status: PositionStatus, // Fill / FillFailed / Redeem / RedeemFailed /
// Split / SplitFailed / Merge / MergeFailed /
// Deposit / DepositFailed / Withdraw / WithdrawFailed / Unknown
amount: String, // 变体 1: 持仓 `remain`(REDEEM 时为 "0")
// 变体 2: split/merge/deposit/withdraw 数量
odds_type: Option<OddsType>,
// 变体 1(FILL / REDEEM / *_FAILED)—— 完整持仓快照
id: Option<String>,
token_id: Option<String>,
asset_id: Option<String>,
timestamp: Option<String>,
un_realized_pnl: Option<String>,
un_realized_pnl_percentage: Option<String>,
value: Option<String>,
avg_price: Option<String>,
trade_id: Option<String>,
// 变体 2(SPLIT / MERGE / DEPOSIT / WITHDRAW / *_FAILED)
tx_hash: Option<String>, // serde rename = "txHash"
ext: Option<WsPositionExt>, // 仅 DEPOSIT 时填充
}
struct WsPositionExt {
to_tx_hash: String, // serde rename = "toTxHash"
}
11.9 pm-user-trade
用户自身的成交流。
- 订阅参数:
[]。 - 消息变体:
WsMessage::UserTrades(Vec<WsUserTrade>)。
struct WsUserTrade {
order_id: String,
client_order_id: String, // 容错默认空串;通常都有值
market_id: String,
token_id: String,
asset_id: String, // yesAssetId 或 noAssetId
side: OrderSide, // Buy / Sell / Unknown
size: String,
price: String,
txhash: String,
timestamp: String,
trade_id: String, // 交易 ID
}
11.10 pm-balance
余额变更。
- 订阅参数:
[]。 - 消息变体:
WsMessage::Balance(Vec<WsBalance>)。
struct WsBalance {
wallet_address: String,
available: String,
total: String,
frozen: String,
token_id: String, // 链上 Point token id
change_type: BalanceChangeType, // Place / Cancel / Fill / Split / Merge /
// Redeem / Deposit / Withdraw / Unknown
change_amount: Option<String>, // spec:可能为 null
update_time: String,
odds_type: Option<OddsType>,
}
11.11 pm-pnl
浮动盈亏流 —— 推送两种 payload;用 serde untagged enum 建模,
根据存在的字段自动选择正确变体。
- 订阅参数:
[]。 - 消息变体:
WsMessage::Pnl(Vec<WsPnl>)。
enum WsPnl {
Overview(WsPnlOverview), // portfolioValue + 各周期汇总
Timeseries(WsPnlTimeseries), // 包含 high/low/current 的图表点
}
struct WsPnlOverview {
portfolio_value: String, // xp 余额 + 持仓市值
periods: Vec<WsPnlPeriodSummary>,
}
struct WsPnlPeriodSummary {
period: String, // "1D" / "1W" / "1M" / "6M" / "1Y"
period_pnl: String,
pnl_percent: String,
}
struct WsPnlTimeseries {
period: String, // "0"=1D / "1"=1W / "2"=1M / "3"=6M / "4"=1Y
interval: String, // ms: 600000 / 1800000 / 3600000 / 86400000
points: Vec<WsPnlPoint>,
current_pnl: String,
high: String,
low: String,
}
WS 错误码
WS 层错误通过 WsMessage::Event { event: "error", msg, .. } 暴露;登录相关的错误则通过 login() 返回的 Err 暴露。常见错误码:
| 错误码 | 含义 |
|---|---|
60004 |
登录时间戳无效(时钟漂移、已过期)。 |
60005 |
API key 无效。 |
60006 |
时间戳已过期(30 秒窗口)。 |
60007 |
签名无效。 |
60009 |
登录失败(通用)。 |
60011 |
该私有频道需要登录。 |
60012 |
无效的 op 值。 |
60018 |
订阅失败(频道名或参数错误)。 |
12. 签名
模块: okx_outcomes_sdk::signing::*。需要 signing Cargo feature。
任何写操作的完整流水线:构造一个类型化的 Action,用你的 k256::ecdsa::SigningKey 通过 sign_to_wrapper 签名,然后把得到的 SignatureWrapper 放进请求体。
pub fn parse_private_key(hex_key: &str) -> Result<SigningKey, String>;
pub fn now_millis() -> u64;
pub fn sign_to_wrapper(
action: &Action,
nonce: u64,
expires_after: Option<u64>,
key: &SigningKey,
) -> Result<SignatureWrapper, String>;
Action 构造函数:
pub fn action_place_order(orders: Vec<OrderRequest>) -> Action;
pub fn action_cancel(cancels: Vec<CancelRequest>) -> Action;
pub fn action_cancel_all(asset_ids: Vec<String>, market_type: &str) -> Action;
pub fn action_prediction_split (market_id: &str, size: &str) -> Action;
pub fn action_prediction_merge (market_id: &str, size: &str) -> Action;
pub fn action_prediction_redeem(market_id: &str) -> Action;
类型化输入:
struct OrderRequest {
asset_id: String,
side: SigningOrderSide, // Buy / Sell(下单侧小写 wire)
market_type: String, // "prediction"
client_order_id: Option<String>, // 34 字符客户端订单 ID
price: String,
reduce_only: bool,
size: String,
size_type: SizeType, // Base(默认)/ Quote
order_type: OrderType,
}
enum OrderType {
Limit(LimitTif),
}
enum LimitTif {
Plain(String), // "gtc" | "ioc" | "fok" | "alo"
Gtd { expiry_time: u64 }, // good-til-date,ms
}
struct CancelRequest {
asset_id: String,
market_type: String, // "prediction"
target: CancelTarget,
}
enum CancelTarget { Oid(String), ClientOrderId(String) }
通信侧的对应类型(OrderItem、CancelItem)分别实现 TryFrom<&OrderRequest> 和 From<&CancelRequest>,以保证 JSON 请求体和签名字节都由同一份源 struct 构造。
客户端订单 ID:
pub fn generate_client_order_id_default() -> Result<String, String>;
pub fn generate_client_order_id(region: Region, env: Env) -> Result<String, String>;
pub fn validate_client_order_id(s: &str) -> bool;
pub fn parse_client_order_id_prefix(client_order_id: Option<&str>) -> ClientOrderIdPrefix;
pub fn register_client_order_id_context(region: Region, env: Env);
客户端订单 ID 是 34 字符的十六进制字符串,形如 0x{region}{env}{30 位十六进制随机数}。generate_client_order_id_default() 读取已注册的全局上下文(默认是 HK / PROD)。若需要不同的上下文,在启动时注册一次即可覆盖。
低阶 helper:
pub fn signer_address(key: &SigningKey) -> String;
pub fn ecrecover(signing_hash: &str, signature: &str) -> Result<String, String>;
pub fn sign_action(...) -> Result<String, String>; // 返回 "0x..." 十六进制
pub fn sign_action_full(...) -> Result<(String, String, String, u8), String>; // (txhash, r, s, v)
pub fn sign_action_debug(...) -> Result<SigningDebug, String>; // 返回所有中间哈希值
正常流程请使用 sign_to_wrapper。低阶函数仅用于调试,或用于需要访问 txhash 的调用方(例如显示"在浏览器中查看"链接)。
13. 通用类型
模块: okx_outcomes_sdk::models::common::*。
struct Pagination {
next_cursor: Option<String>, // 最后一页时为 None
has_more: bool,
page_size: i32, // 当前页条目数
}
struct EcdsaSignature {
r: String, // 十六进制,带 0x 前缀
s: String, // 十六进制,带 0x 前缀
v: u8, // recovery id:0 或 1
}
struct SignatureWrapper {
// 序列化为 { "Ecdsa": { r, s, v } }
ecdsa: EcdsaSignature,
}
SDK 透明地包装了两种 API 信封:
- 预测市场 REST:
{ "code": <int>, "message": "...", "data": <T> },其中code == 0表示成功。 - OKX 市场数据:
{ "code": "<int>", "msg": "...", "data": <T> }(注意 code 是字符串)。code == "0"表示成功。
WebSocket
1. WebSocket 登录认证
仅订阅私有频道前需先进行 WebSocket 登录认证。公共频道无需登录。
详见 WebSocket 登录认证文档。
通用订阅格式
{
"op": "subscribe",
"args": [
{ "channel": "<频道名1>", "instId": "<yesAssetId1>" },
{ "channel": "<频道名2>", "instId": "<yesAssetId2>" }
]
}
通用取消订阅格式
{
"op": "unsubscribe",
"args": [
{ "channel": "<频道名1>", "instId": "<yesAssetId1>" },
{ "channel": "<频道名2>", "instId": "<yesAssetId2>" }
]
}
2. WebSocket 私有频道
| 频道名称 | 订阅参数 | 是否需要授权 | 说明 |
|---|---|---|---|
| pm-order | channelName | 是 | 用户订单数据推送 |
| pm-position | channelName | 是 | 仓位变更 |
| pm-user-trade | channelName | 是 | 用户交易历史推送 |
| pm-balance | channelName | 是 | 余额变更 |
| pm-pnl | channelName | 是 | 当前仓位的浮盈/浮亏数值 |
2.1 订单状态推送
推送频率: 事件触发,订单状态变更时推送
订阅示例
{
"op": "subscribe",
"args": [{ "channel": "pm-order" }]
}
推送数据格式
{
"arg": { "channel": "pm-order", "uid": "{cexUserId}" },
"data": [{
"orderId": "307173036051017730",
"clientOrderId": "cli-abc-123",
"marketId": "100001",
"status": "FILLED",
"assetId": "71",
"side": "BUY",
"direction": "YES",
"filledSize": "10",
"orderSize": "10",
"avgPrice": "0.57",
"amount": "5.7",
"limitPrice": "0.45",
"failMessage": null,
"oddsType": "points",
"txHash": "0xdef...",
"tradeId": "9876543210"
}]
}
推送字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
orderId |
string | 订单 ID |
clientOrderId |
string / null | 客户端订单 ID(cloid);客户端未传时为 null |
marketId |
string | 市场 ID |
status |
string | 推送事件类型(见下表枚举) |
assetId |
string | 币对资产 ID(yesAssetId 或 noAssetId) |
side |
string | 交易方向:BUY / SELL |
direction |
string | 持仓方向:YES / NO |
filledSize |
string / null | 本次累计成交份额;无成交时为 null |
orderSize |
string | 下单份额 |
avgPrice |
string / null | 累计成交均价(= amount / filledSize);无成交时为 null |
amount |
string / null | 累计成交金额(xp),BUY=花费 / SELL=收入;无成交时为 null |
limitPrice |
string / null | 限价单挂单价格(仅限价场景);市价单为 null |
failMessage |
string / null | 失败提示文案;仅 PLACE_FAILED / CANCEL_FAILED 场景填值 |
oddsType |
string | 盘口类型:points |
txHash |
string / null | 链上交易哈希;未上链事件为 null(如 PLACE_FAILED) |
tradeId |
string / null | TradeZone 成交 ID;仅限价单部分成交(status=ACTIVE)时填值 |
status 枚举
| code | 说明 | 必填字段 |
|---|---|---|
ACTIVE |
活跃(限价单上链 / 部分成交且剩余有效) | orderSize, limitPrice(部分成交时另含 filledSize, avgPrice, amount, tradeId) |
FILLED |
完全成交 | filledSize, avgPrice, amount, txHash |
PARTIALLY_FILLED |
部分成交后撤单 | filledSize, orderSize, avgPrice, amount, txHash |
PLACE_FAILED |
下单失败 | orderSize, failMessage |
CANCEL_FAILED |
撤单失败(非预期) | orderId, failMessage |
CANCELLED |
用户主动撤单 / 系统批量撤 | orderSize, limitPrice |
EXPIRED |
订单到期 | orderSize, limitPrice |
2.2 仓位变更推送
推送频率: 事件触发
订阅示例
{
"op": "subscribe",
"args": [{ "channel": "pm-position" }]
}
2.3 仓位
(status = FILL / REDEEM/FILL_FAILED/REDEEM_FAILED)
推送数据格式
{
"arg": { "channel": "pm-position", "uid": "{cexUserId}" },
"data": [{
"id": "307173036051017730",
"marketId": "100001",
"tokenId": "20000",
"assetId": "71",
"amount": "10",
"timestamp": "1712736000000",
"unRealizedPnl": "5.20",
"unRealizedPnlPercentage": "1.05",
"value": "100.00",
"avgPrice": "0.57",
"status": "FILL",
"tradeId": "9876543210",
"oddsType": "points"
}]
}
字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
id |
String | 仓位 ID |
marketId |
String | 市场 ID |
tokenId |
String | YES/NO token 链上 ID |
assetId |
String | 币对资产 ID(yesAssetId 或 noAssetId) |
amount |
String | 当前持仓数量(remain 快照),REDEEM 场景为 "0" |
timestamp |
String | 事件时间戳(毫秒) |
unRealizedPnl |
String | 未实现盈亏 = (currentPrice − avgCost) × remain;REDEEM 为 "0" |
unRealizedPnlPercentage |
String | 未实现盈亏百分比(8 位精度);REDEEM 为 "0" |
value |
String | 仓位市值 = currentPrice × remain;REDEEM 为 "0" |
avgPrice |
String | 加权平均持仓成本;REDEEM 为 "0" |
status |
String | FILL / REDEEM |
tradeId |
String / null | TradeZone 成交 ID;仅 FILL 场景填值(REDEEM 为 null) |
oddsType |
String | 盘口类型:points |
2.4 status 适用场景
| code | 说明 | 单次推送条数 |
|---|---|---|
FILL |
订单成交导致仓位变更 | 1(单 tokenId 仓位) |
REDEEM |
结算赎回,该市场仓位清零 | N(该市场原有的每个 tokenId 各一条,amount=0) |
2.5 仓位
(status = SPLIT / MERGE / DEPOSIT / WITHDRAW/SPLIT_FAILED/MERGE_FAILED/DEPOSIT_FAILED/WITHDRAW_FAILED)
推送数据格式
{
"arg": { "channel": "pm-position", "uid": "{cexUserId}" },
"data": [{
"marketId": "100001",
"status": "DEPOSIT",
"amount": "100",
"txHash": "0xdef...",
"oddsType": "points",
"ext": {
"toTxHash": "0xabc..."
}
}]
}
字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
marketId |
String | 市场 ID |
status |
String | SPLIT / MERGE / DEPOSIT / WITHDRAW |
amount |
String | 金额 |
txHash |
String | 链上交易哈希。DEPOSIT 场景填 XLayer 原始交易哈希;其余场景填该业务对应链上哈希 |
oddsType |
String | 盘口类型:points |
ext |
Object / null | 扩展信息;仅 DEPOSIT 场景填值,其余场景为 null |
ext.toTxHash |
String / null | TZ 侧入账交易哈希;仅 DEPOSIT 场景填值 |
status 枚举
| code | 说明 |
|---|---|
FILL |
订单成交导致仓位变更 |
SPLIT |
Split 拆分成功 |
MERGE |
Merge 合并成功 |
REDEEM |
结算赎回成功 |
DEPOSIT |
充值成功 |
WITHDRAW |
提现成功 |
FILL_FAILED |
成交失败 |
SPLIT_FAILED |
Split 失败 |
MERGE_FAILED |
Merge 失败 |
REDEEM_FAILED |
赎回失败 |
DEPOSIT_FAILED |
充值失败 |
WITHDRAW_FAILED |
提现失败 |
2.6 撮合成交推送
每笔 FILL 事件推送一条成交流水,前端用于实时展示成交记录列表。
推送频率: 事件触发,每笔成交一条
订阅示例
{
"op": "subscribe",
"args": [{ "channel": "pm-user-trade" }]
}
推送数据格式
{
"arg": { "channel": "pm-user-trade", "uid": "{cexUserId}" },
"data": [{
"orderId": "307173036051017730",
"clientOrderId": "cli-abc-123",
"marketId": "100001",
"tokenId": "20000",
"assetId": "71",
"side": "BUY",
"size": "10",
"price": "0.57",
"txhash": "0xdef...",
"timestamp": "1712736000000",
"tradeId": "9876543210"
}]
}
推送字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
orderId |
string | TradeZone 订单 ID |
clientOrderId |
string / null | 客户端订单 ID;客户端未传时为 null |
marketId |
string | 市场 ID |
tokenId |
string | YES/NO token 链上 ID |
assetId |
string | 币对资产 ID(yesAssetId 或 noAssetId) |
side |
string | 成交方向:BUY / SELL |
size |
string | 本次成交数量 |
price |
string | 本次成交价格 |
txhash |
string | 链上交易哈希 |
timestamp |
string | 事件时间戳(毫秒) |
tradeId |
string | TradeZone 成交 ID |
2.7 余额变更推送
余额同步成功后推送,前端用于实时更新余额显示。
推送频率: 事件触发,余额变更(挂单 / 撤单 / 成交 / Split / Merge / 赎回 / 充值 / 提现)时推送
订阅示例
{
"op": "subscribe",
"args": [{ "channel": "pm-balance" }]
}
推送数据格式
{
"arg": { "channel": "pm-balance", "uid": "{cexUserId}" },
"data": [{
"walletAddress": "0x1234abcd5678ef901234abcd5678ef901234abcd",
"available": "950.5",
"total": "1000",
"frozen": "49.5",
"tokenId": "0",
"changeType": "FILL",
"changeAmount": "100",
"updateTime": "1712736000000",
"oddsType": "points"
}]
}
推送字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
walletAddress |
String | 用户 AA 钱包地址 |
available |
String | 可用余额 |
total |
String | 总余额(含冻结) |
frozen |
String | 冻结金额(= total − available) |
tokenId |
String | xp token 链上 ID(与 oddsType 对应) |
changeType |
String | 触发原因(见下表枚举) |
changeAmount |
String / null | 本次变动金额;不适用场景为 null |
updateTime |
String | 事件时间戳(毫秒) |
oddsType |
String | 盘口类型:points |
2.8 changeType 枚举
| code | 触发场景 |
|---|---|
PLACE |
挂单冻结 |
CANCEL |
撤单解冻 |
FILL |
成交导致余额变更 |
SPLIT |
Split 扣减 xp |
MERGE |
Merge 增加 xp |
REDEEM |
结算赎回 |
DEPOSIT |
充值 |
WITHDRAW |
提现 |
2.9 Pnl变更推送
同时推送Pnl曲线图及概览
推送数据格式
{
"arg": { "channel": "pm-pnl", "uid": "{cexUserId}" },
"data": [{
"portfolioValue": "1234.56",
"periods": [
{ "period": "1D", "periodPnl": "12.34", "pnlPercent": "1.01" },
{ "period": "1W", "periodPnl": "56.78", "pnlPercent": "4.83" },
{ "period": "1M", "periodPnl": "120.45", "pnlPercent": "10.81" },
{ "period": "6M", "periodPnl": "320.10", "pnlPercent": "35.02" },
{ "period": "1Y", "periodPnl": "500.00", "pnlPercent": "67.93" }
]
}]
}
字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
portfolioValue |
String | 当前组合总价值(xp 可用余额 + 仓位市值) |
periods |
Array | 多周期 PnL 汇总数组 |
periods[].period |
String | 周期:1D / 1W / 1M / 6M / 1Y |
periods[].periodPnl |
String | 该周期内 PnL 绝对值 |
periods[].pnlPercent |
String | 该周期内 PnL 百分比 |
推送数据格式
{
"arg": { "channel": "pm-pnl", "uid": "{cexUserId}" },
"data": [{
"period": "0",
"interval": "600000",
"points": [
{ "time": "1712707200000", "pnl": "1000.00" },
{ "time": "1712728800000", "pnl": "1010.50" },
{ "time": "1712750400000", "pnl": "1023.75" }
],
"currentPnl": "1023.75",
"high": "1030.00",
"low": "995.00"
}]
}
字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
period |
String | 周期编码:0=1D / 1=1W / 2=1M / 3=6M / 4=1Y |
interval |
String | 数据点间隔(毫秒),与周期对应(见下表) |
points |
Array | 时间序列数据点 |
points[].time |
String | 数据点时间戳(毫秒) |
points[].pnl |
String | 该时刻总资产(= xp 余额 + 仓位市值) |
currentPnl |
String | 当前总资产(兼容字段) |
high |
String | 周期内最高总资产 |
low |
String | 周期内最低总资产 |
2.10 周期编码与 interval 对照
| period 编码 | 周期 | interval(毫秒) | 含义 |
|---|---|---|---|
0 |
1D | 600000 |
10 分钟 |
1 |
1W | 1800000 |
30 分钟 |
2 |
1M | 3600000 |
1 小时 |
3 |
6M | 86400000 |
1 天 |
4 |
1Y | 86400000 |
1 天 |
3. WebSocket 公共频道
3.1 深度推送
3.2 推送频率
最小推送间隔 100ms — 两次增量推送之间最短间隔
最大推送间隔 60,000ms (60s) — 订单簿无变化时最长推送间隔
订阅示例
{
"op": "subscribe",
"args": [{
"channel": "pm-books",
"instId": "{yesAssetId}"
}]
}
订阅成功响应
{
"event": "subscribe",
"arg": { "channel": "pm-books", "instId": "{yesAssetId}" }
}
推送数据格式
{
"arg": {"channel": "pm-books", "instId": "{yesAssetId}"},
"action": "snapshot",
"data": [
{
"asks": [
["67300.1", "201.18", "25"],
["67300.2", "421.45", "5"]
],
"bids": [
["67300", "525.41", "34"],
["67299.9", "0.17", "7"]
],
"ts": "1774944028506",
"checksum": -702280706,
"seqId": 308306650401,
"prevSeqId": -1
}
]
}
3.3 成交推送
实时推送每笔成交记录
推送频率:推送间隔 200ms,每次推送最多 20 条成交,编码类型为聚合成交
订阅示例
{
"op": "subscribe",
"args": [{
"channel": "pm-trades",
"instId": "{yesAssetId}"
}]
}
推送数据格式
{
"arg": { "channel": "pm-trades", "instId": "{yesAssetId}" },
"data": [{
"instId": "{yesAssetId}",
"tradeId": "123456789",
"px": "0.65",
"sz": "100",
"side": "buy",
"ts": "1711900800123"
}]
}
推送字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
instId |
String | yesAssetId |
fId |
String | 聚合周期内首笔成交 ID |
lId |
String | 聚合周期内末笔成交 ID |
px |
String | 成交价格 |
sz |
String | 成交数量 |
side |
String | 成交方向:buy 买入,sell 卖出 |
ts |
String | 成交时间(Unix 毫秒时间戳) |
3.4 K 线推送
实时推送 K 线数据,当前 K 线未收盘时会持续更新推送。端上可基于K线的收盘价绘制价格趋势图,具体订阅的颗粒度由端上自行评估
频道命名规则: pm-candle + 周期,例如 pm-candle1m、pm-candle15m
订阅示例(以 1 分钟和 5 分钟为例)
{
"op": "subscribe",
"args": [
{ "channel": "pm-candle1m", "instId": "{yesAssetId}" },
{ "channel": "pm-candle15m", "instId": "{yesAssetId}" }
]
}
推送数据格式
{
"arg": { "channel": "pm-candle15m", "instId": "{yesAssetId}" },
"data": [
["1711900800000", "0.65", "0.67", "0.63", "0.66", "2000", "1300", "1300", "0"]
]
}
3.5 推送字段说明
(data 数组中每条记录按索引顺序)
| 索引 | 字段 | 说明 |
|---|---|---|
| 0 | ts |
K 线开始时间(Unix 毫秒时间戳) |
| 1 | o |
开盘价 |
| 2 | h |
最高价 |
| 3 | l |
最低价 |
| 4 | c |
收盘价 |
| 5 | vol |
成交量(以张计) |
| 6 | volCcy |
成交量(以计价货币计) |
| 7 | volCcyQuote |
成交量(以报价货币计) |
| 8 | confirm |
K 线状态:0 未确认(当前 K 线),1 已确认(已收盘) |
3.6 行情 Ticker 推送
实时推送最新成交价、买一价、卖一价和24小时交易量等信息。
推送频率:最快100ms推送一次,触发推送的事件有:成交、买一卖一发生变动,没有触发事件时不推送
订阅示例
{
"op": "subscribe",
"args": [{ "channel": "pm-tickers", "instId": "{yesAssetId}" }]
}
订阅成功响应
{
"event": "subscribe",
"arg": { "channel": "pm-tickers", "instId": "{yesAssetId}" },
"connId": "accb8e21"
}
推送数据格式
{
"arg": { "channel": "pm-tickers", "instId": "{yesAssetId}" },
"data": [{
"instType": "PREDICTIONS",
"instId": "{yesAssetId}",
"last": "0.4999",
"lastSz": "1",
"askPx": "0.6",
"askSz": "30",
"bidPx": "0.4",
"bidSz": "30",
"open24h": "0.5001",
"high24h": "0.5001",
"low24h": "0.4999",
"vol24h": "372",
"volCcy24h": "186",
"sodUtc0": "0.4999",
"sodUtc8": "0.4999",
"ts": "1774948808119"
}]
}
推送字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
| instType | String | 产品类型 |
| instId | String | yesAssetId |
| last | String | 最新成交价 |
| lastSz | String | 最新成交数量 |
| askPx | String | 卖一价 |
| askSz | String | 卖一价对应数量 |
| bidPx | String | 买一价 |
| bidSz | String | 买一价对应数量 |
| open24h | String | 24小时开盘价 |
| high24h | String | 24小时最高价 |
| low24h | String | 24小时最低价 |
| vol24h | String | 24小时成交量(以张计) |
| volCcy24h | String | 24小时成交量(以计价货币计) |
| sodUtc0 | String | UTC 0 时开盘价 |
| sodUtc8 | String | UTC+8 时开盘价 |
| ts | String | 数据推送时间(Unix 毫秒时间戳) |
3.7 概率价格推送
预测市场概率价格推送,包含市场概率、累计成交额、买一卖一、最新成交价等信息
推送频率:定时3s推送一次
订阅示例
{
"op": "subscribe",
"args": [{
"channel": "prediction-market-prices",
"instId": "{yesAssetId}"
}]
}
推送数据格式
{
"arg": {
"channel": "prediction-market-prices",
"instId": "{yesAssetId}"
},
"data": [
{
"yesAssetId": "71",
"eventId": "1774875348987717503",
"bestBid": "0.4896",
"bestAsk": "0.6134",
"lastTradePrice": "0.5848",
"probability": "5515",
"marketVolume": "17.1463",
"eventVolume": "773.6524",
"timestamp": "1775036647300"
}
]
}
推送字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
| yesAssetId | String | 预测市场 yesAssetId |
| eventId | String | 市场所属的event |
| bestBid | String | 买一价(最优买入价) |
| bestAsk | String | 卖一价(最优卖出价) |
| lastTradePrice | String | 最新成交价 |
| probability | String | Yes 方向的市场概率,推送万分比整数(如 0.6500 → 6500) |
| marketVolume | String | 当前市场累计成交额 |
| eventVolume | String | 当前事件累计成交额 |
| timestamp | String | 事件时间戳(毫秒) |
3.8 事件状态推送
事件状态变更,用于展示事件最终结算结果
推送频率:事件触发,事件得到最终结果时推送,世界杯期间一天2~3场比赛;多元互斥、单一二元事件拿到最终结果时进行推送
订阅示例
注意: 订阅此频道时,
instId取值为 event-{eventId}
{
"op": "subscribe",
"args": [{
"channel": "pm-event-status",
"instId": "event-{eventId}"
}]
}
推送数据格式
{
"arg": {
"channel": "pm-event-status",
"instId": "event-{eventId}"
},
"data": [{
"eventId": "{eventId}",
"status": "resolved",
"marketId":"marketId",
"outcomeOption":"yes | no | others | 球队名称 | draw",
"timestamp": "1672290687"
}]
}
推送字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
| eventId | string | 比赛ID |
| status | string | 事件状态 |
| marketId | string | 胜出市场的marketId |
| outcomeOption | string | 最终胜出的展示 |
| timestamp | string | 事件时间戳(毫秒) |
常见错误码
1. 响应格式
所有异常统一返回以下 JSON 结构:
{
"code": 100015,
"msg": "calldata 无效或字段不合法"
}
1.1 通用
所有 OpenAPI v5 接口都可能产生。 更多通用错误码详见:OKX 公共错误码文档
| Code | Meaning |
|---|---|
| 10000 | User not logged in |
| 10001 | Parameter validation failed |
| 10002 | Authentication failed |
| 206004 | Request oddsType does not match the account |
1.2 事件/市场类
| Code | Meaning |
|---|---|
| 201001 | Event does not exist |
| 201002 | Market does not exist |
适用接口:
GET /api/v5/predictions/events
GET /api/v5/predictions/events/{eventId}
GET /api/v5/predictions/events/{eventId}/markets
GET /api/v5/predictions/markets/{marketId}
GET /api/v5/predictions/events/search
2. 下单 / 写操作类
涵盖:下单、撤单、全部撤单、心跳、Split、Merge、Redeem。
| Code | Meaning |
|---|---|
| 100001 | Market is not tradable |
| 100002 | Insufficient balance |
| 100006 | Account is frozen |
| 100010 | Insufficient token balance |
| 100011 | Order does not exist |
| 100012 | Order status does not allow cancellation |
| 100013 | Request address does not match the user address |
| 100015 | Invalid calldata or malformed fields |
| 100016 | Nonce already used |
| 100017 | Nonce expired |
| 100018 | Order amount is below the minimum notional |
| 100101 | TradeZone SDK signature exception |
| 120007 | User does not exist |
| 120022 | Account is in escape (exit) process |
| 201002 | Market does not exist |
| 213003 | Signature verified but TradeZone submission failed |
适用接口:
POST /api/v5/predictions/orders下单POST /api/v5/predictions/orders/cancel撤销单订单POST /api/v5/predictions/orders/cancel-all撤销全部 / 指定市场订单POST /api/v5/predictions/heartbeat心跳POST /api/v5/predictions/positions/splitSplitPOST /api/v5/predictions/positions/mergeMergePOST /api/v5/predictions/positions/redeemRedeem
3. 查询订单 / 仓位类
涵盖:查询单订单、订单列表、成交历史、仓位查询。
| Code | Meaning |
|---|---|
| 100011 | Order does not exist |
| 400 | Path parameter parsing failed |
适用接口:
GET /api/v5/predictions/orders/{orderId}查询单订单GET /api/v5/predictions/orders查询订单列表GET /api/v5/predictions/trades查询成交记录GET /api/v5/predictions/positions查询仓位







