☰
OKX交易所API Python调用实战:签名鉴权、现货杠杆与历史数据
2026/10/8 3:40:28 网站建设 项目流程

简介:面向需要调用OKEx Web API的Python开发者,提供一套聚焦杠杆交易、现货交易、历史记录与历史数据获取的轻量代码包,适合量化交易、自动化下单或行情分析场景。压缩包共12个文件,全部为py脚本,整体仅11KB,按现货、合约、杠杆、账户、ETT等业务拆分模块,并包含WebSocket客户端封装,便于快速集成。目前已有188人学习/下载。通过阅读源码可掌握OKEx签名鉴权、请求构造、数据解析等关键流程,也能直接复用函数对接杠杆下单、K线历史查询等操作;结合MVC标签,可在此基础上构建结构清晰、界面友好的管理工具。对于熟悉Python但初次接触加密交易所API的开发者尤其具有参考价值。

1. ok交易所的 web api 调用应用,现货、杠杆与历史数据:一套 Python 方案能解决什么

“ok交易所的 web api 调用应用”这类 python 源码包,打开之后通常是一堆 py 文件:有下单的、有拉行情的、有查订单历史的。别急着跑,先分清它调的是哪套接口、用什么签名方式、下单参数是不是字符串——这三个问题没搞明白,代码跑通概率很低。这套方案典型价值在于:把现货交易、币币杠杆、历史订单和 K 线数据统一封装成函数,让策略脚本和复盘脚本不用重复造轮子。适合两类人:一类是想照着源码理解交易所 web api 调用链的 python 入门者,另一类是已经跑着策略、需要补历史数据或对账的。以下按 REST 接口主线,把封装、现货、杠杆、历史数据依次讲透,高频坑集中放在第 5 章。

2. 先跑通 REST 客户端:签名鉴权与最小可复用的 Python 封装

看再多免费 python 源码,交易所 API 真正卡人的永远是签名。OK交易所的 web api 签名像黑匣子:四个请求头、一个 Base64 的 HMAC SHA256 串,任何字符不对就报错。这一章先把签名规则拆开,再给一个能复用的客户端封装,最后用公开接口验证连通性。

2.1 鉴权签名规则:三个必填请求头和一个 HMAC SHA256 签名串

OK交易所 V5 接口的私有请求需要四个请求头:OK-ACCESS-KEY(API Key)、OK-ACCESS-SIGN(签名)、OK-ACCESS-TIMESTAMP(请求时间戳)、OK-ACCESS-PASSPHRASE(API 口令)。签名串的拼接顺序是固定的:

import base64 import hashlib import hmac from datetime import datetime, timezone def iso_timestamp() -> str: # V5 要求 UTC 时间,精确到毫秒,格式:2025-01-01T08:00:00.123Z now = datetime.now(timezone.utc) return now.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" def build_sign(timestamp: str, method: str, request_path: str, body: str, secret_key: str) -> str: # message = 时间戳 + 请求方法 + 请求路径(含query string) + 请求体 message = timestamp + method.upper() + request_path + body mac = hmac.new( secret_key.encode("utf-8"), message.encode("utf-8"), hashlib.sha256, ) return base64.b64encode(mac.digest()).decode("utf-8")

这段代码有三个容易翻车的地方。第一,时间戳必须是 UTC,不能用datetime.now()直接格式化,否则服务器和本地时间差超过 30 秒就会返回50118。第二,request_path要包含查询参数原样拼接,比如GET /api/v5/account/balance?ccy=BTC,这里的?ccy=BTC必须进入签名串,少一个字符签名都无效。第三,body是 POST 请求体 JSON 字符串,不能带多余空格,{}和{"a":1}这类序列化结果要固定。

2.2 最小客户端封装:requests 实现、限频与重试

签名只是第一步,真正干活的是一个统一的请求封装。用requests.Session()复用连接,把签名、请求头、超时、重试、模拟盘开关都收进去:

import json import time import requests class OkxV5Client: def __init__(self, api_key: str, secret_key: str, passphrase: str, demo: bool = False, base_url: str = "https://www.okx.com"): self.api_key = api_key self.secret_key = secret_key self.passphrase = passphrase self.demo = demo self.session = requests.Session() self.base_url = base_url def request(self, method: str, path: str, params: dict = None, body: dict = None, retries: int = 3): method = method.upper() body_str = "" if body is None else json.dumps(body) query = "" if params: query = "?" + "&".join(f"{k}={v}" for k, v in params.items()) request_path = path + query timestamp = iso_timestamp() sign = build_sign(timestamp, method, request_path, body_str, self.secret_key) headers = { "OK-ACCESS-KEY": self.api_key, "OK-ACCESS-SIGN": sign, "OK-ACCESS-TIMESTAMP": timestamp, "OK-ACCESS-PASSPHRASE": self.passphrase, "Content-Type": "application/json", } if self.demo: headers["x-simulated-trading"] = "1" # 模拟盘开关 url = self.base_url + request_path for attempt in range(retries): try: if method == "GET": resp = self.session.get(url, headers=headers, timeout=10) else: resp = self.session.post(url, headers=headers, data=body_str, timeout=10) data = resp.json() if data.get("code") == "0": return data # 50011/429 限频,500/501/503 服务端抖动,退避重试 if data.get("code") in ("50011", "50013") or resp.status_code in (429, 500, 501, 503): time.sleep(0.5 * (attempt + 1)) continue return data except requests.RequestException: time.sleep(0.5 * (attempt + 1)) return {"code": "-1", "msg": "request failed after retries", "data": []}

封装里我加了两个实用设计。一是模拟盘开关,通过x-simulated-trading: 1请求头切换,新写的下单逻辑先在这边跑,避免真金白银试错;二是针对50011和429的退避重试,注意重试只对幂等查询安全,下单接口重试前要确认上一笔是否已经提交成功。

2.3 连通性自检:先用公开行情接口验证封装

客户端写完先别急着下单,用不需要签名的公开接口验证网络和封装,再用一个私有接口验证签名链路:

curl "https://www.okx.com/api/v5/public/time" curl "https://www.okx.com/api/v5/market/ticker?instId=BTC-USDT"

第一次接入时,我习惯先跑一段 python 脚本,顺序是:调public/time看本地时间与服务端时间差,调market/ticker看行情返回结构,最后调一次account/balance私有接口确认签名没毛病。如果前面公开接口都通、私有接口报签名错误,问题基本锁定在时间戳或request_path拼法上。

3. 现货交易接口:下单、撤单与持仓查询的落地写法

现货是整个方案的基础业务,杠杆、历史数据都围绕它展开。现货下单走/api/v5/trade/order,关键参数是tdMode=cash。这一章讲清楚下单请求体的每个字段怎么填,撤单怎么做到幂等,以及查询余额、持仓、订单状态三个接口的分工。

3.1 下单请求体:订单类型、价格、数量与 tgtCcy 的边界

现货下单的请求体核心字段有六个:instId(交易对,如BTC-USDT)、tdMode(现货固定cash)、side(buy/sell)、ordType(market/limit)、sz(数量)、px(价格)。注意一个反直觉的点:所有数值字段都必须是字符串,传浮点数会在精度校验上翻车。

def place_order(self, inst_id: str, side: str, ord_type: str, sz, px=None, tgt_ccy=None, cl_ord_id=None): body = { "instId": inst_id, "tdMode": "cash", "side": side, "ordType": ord_type, "sz": str(sz), } if px is not None: body["px"] = str(px) # 限价单必填 if tgt_ccy is not None: body["tgtCcy"] = tgt_ccy # 市价单按金额买时传 quote_ccy if cl_ord_id is not None: body["clOrdId"] = cl_ord_id # 客户端订单号,用于幂等 return self.request("POST", "/api/v5/trade/order", body=body)

参数说明:限价单必须同时给px和sz,价格精度要符合交易对规则,比如BTC-USDT的价格最小变动是 0.1,传67000.05会被拒。市价买单有两种下法:按数量买,sz传币数量;按金额买,tgtCcy=quote_ccy且sz传 USDT 金额。市价卖单只能按数量。clOrdId是客户端生成的唯一单号,下单成功后用它查单、撤单,比ordId更可控。

3.2 撤单与幂等:用 clOrdId 防止重复动作

撤单是高频动作,最容易出问题的是重复撤单和撤单时订单已成交。cancel-order接口支持用ordId或clOrdId定位订单,两个至少要传一个:

def cancel_order(self, inst_id: str, ord_id: str = None, cl_ord_id: str = None): body = {"instId": inst_id} if ord_id: body["ordId"] = ord_id if cl_ord_id: body["clOrdId"] = cl_ord_id return self.request("POST", "/api/v5/trade/cancel-order", body=body)

逻辑说明:撤单前最好先查一次订单状态,只有live和partially_filled状态的单能撤。filled和canceled的单再撤会返回错误码,如果不做状态检查,就要接受这个错误码当作“已终结”处理。批量撤单用/api/v5/trade/cancel-batch-orders,数组格式传多笔订单,我一般把批量撤单放在异常清理场景里,比如策略停止时把所有活单一次性撤掉。

3.3 余额、持仓与订单状态:三个查询接口的分工

查询类接口有三个容易混淆。/api/v5/account/balance查账户资产,返回每个币种的availBal(可用)和frozenBal(冻结);/api/v5/account/positions查持仓,现货模式下基本为空,杠杆和合约才有实际内容;/api/v5/trade/order查单笔订单详情,状态字段state取值:live、partially_filled、filled、canceled。

def get_order(self, inst_id: str, ord_id: str = None, cl_ord_id: str = None): params = {"instId": inst_id} if ord_id: params["ordId"] = ord_id if cl_ord_id: params["clOrdId"] = cl_ord_id return self.request("GET", "/api/v5/trade/order", params=params) def get_balance(self): return self.request("GET", "/api/v5/account/balance")

下单后轮询订单状态是个实用的模式:每 1~2 秒查一次,连续查到filled或canceled就结束,超过 30 秒没终态就告警人工介入。注意balance返回的是各币种原始数量,BTC 就是 BTC 的数量,要折合成 USDT 得另取价格,别把数量当金额。frozenBal是挂单冻结的部分,计算可用资金时两个都要看。

4. 杠杆交易接口:逐仓、全仓与倍数调整的落地方案

杠杆交易在 OK交易所 V5 里和现货共用/api/v5/trade/order下单接口,差别只在tdMode。但这一个字段的差别背后是一整套不同的账户和风控逻辑。这一章讲杠杆倍数怎么设置、逐仓全仓有哪些接口差异、下单前怎么做保证金校验。

4.1 杠杆倍数设置:set-leverage 与全仓/逐仓的差异

杠杆倍数通过/api/v5/account/set-leverage设置,下单前必须先把倍数设好,否则接口可能按默认倍数执行或者直接报错。参数是instId、lever、mgnMode,其中mgnMode传cross(全仓)或isolated(逐仓):

def set_leverage(self, inst_id: str, lever: int, mgn_mode: str, pos_side: str = None): body = { "instId": inst_id, "lever": str(lever), "mgnMode": mgn_mode, } if pos_side: body["posSide"] = pos_side # 双向持仓模式下传 long/short return self.request("POST", "/api/v5/account/set-leverage", body=body)

参数说明:lever是整数,比如 3 表示 3 倍杠杆;mgnMode决定保证金模式。全仓模式下,整个账户的资产都作为保证金,风险共担;逐仓模式下,每笔仓位的保证金独立,亏到该仓位的保证金就强平。如果你的账户开了双向持仓模式,posSide必须传long或short;单向持仓模式(net)可以不传。设置杠杆的返回值里一般会包含当前杠杆倍数,注意回读确认。

4.2 杠杆下单与现货下单的差别:tdMode、posSide 与借币

杠杆下单走同一个下单接口,tdMode从cash换成cross或isolated。下单前先设置杠杆,再按开仓方向下单:

def place_margin_order(self, inst_id: str, side: str, ord_type: str, sz, px=None, mgn_mode="cross", pos_side=None): body = { "instId": inst_id, "tdMode": mgn_mode, # cross=全仓杠杆, isolated=逐仓杠杆 "side": side, "ordType": ord_type, "sz": str(sz), } if px is not None: body["px"] = str(px) if pos_side: body["posSide"] = pos_side return self.request("POST", "/api/v5/trade/order", body=body)

逻辑说明:杠杆买入和现货买入的表面差别是tdMode,但底层逻辑完全不同。杠杆做多是买入资产的同时借入计价币,杠杆做空是卖出自有资产的同时借入标的币。所以下单前要确认账户里有足够本金,系统才会按杠杆借币,本金不足但可借额度够时也能成交。双向持仓模式下,开多传posSide=long,平多传side=sell, posSide=long,这个方向很容易写反。

4.3 保证金与风控:可借数量、强平价与模拟盘验证

杠杆单的保证金校验比现货严格得多。我下单前固定做三步检查:第一步查/api/v5/account/balance确认可用余额;第二步查/api/v5/account/positions看现有仓位方向和已占用保证金;第三步用instId的元数据估算下单量是否在可借范围内。

positions返回里有一个我一直当成强平预警用的字段:liqPx(预估强平价)。当行情接近这个价格时,仓位会非常危险。杠杆是把双刃剑,做多时币价跌 1/杠杆倍数的幅度就爆仓。第一次跑杠杆逻辑,我的建议是坚持用模拟盘验证:OkxV5Client(..., demo=True)下几笔 1 倍和 2 倍的单,观察仓位变化和强平价计算是否符合预期,确认没有问题再切实盘。

5. OK交易所 Web API 高频排查点:现象、原因与解决路径

以下五条是调用这套接口里最常见的踩坑现场,按“现象 → 原因 → 解决”写,全是血泪经验。

5.1 签名一直报 50118:时间戳格式与 query string 没参与签名

现象:私钥、Key、Passphrase 填得都对,但接口稳定返回50118,看代码逻辑怎么都对。

原因:两个高频点。一是时间戳不是 UTC 或毫秒格式不对,datetime.now()拿到的是本地时区,服务器比对的却是 UTC,时差加签名串错位一起导致校验失败;二是 GET 请求的query string没有拼进request_path,比如查余额带了?ccy=BTC,签名时却只签了/api/v5/account/balance。

解决:统一用第 2 章的iso_timestamp()生成时间戳,先调public/time接口对时,把本地时间与服务端时间的偏移记下来。签名用的request_path必须是除域名外的完整路径,含问号和参数原样。

5.2 下单报 5xxxx:数字必须是字符串,精度要对齐 tickSz/lotSz

现象:下单接口返回5xxxx系列错误码,提示参数无效或下单失败,但看参数感觉都对。

原因:最常见的两个原因。一是sz、px用了浮点数而非字符串,JSON 序列化时0.1可能变成0.1000000000000000055之类;二是数量或价格精度不满足交易对规则,比如最小下单量是 0.01 BTC,传了 0.001 就会被拒。

解决:所有数值一律先str()再放进请求体。交易对精度用/api/v5/public/instruments?instType=SPOT查询,返回里有lotSz(下单数量步长)、minSz(最小下单量)、tickSz(价格步长),下单前对参数做一次取整和范围校验。这一条几乎能解决 80% 的下单失败问题。

5.3 历史K线总少最后一根:confirm 未确认与游标翻页边界

现象:拉历史K线时,每次拉完 100 根,最后一根的时间戳和上一批对不上,中间有缺口或重复。

原因:K线接口返回的每根K线带一个confirm字段,0表示这根K线还没走完,数据会变化;1表示已确认。拉最新K线时会把未确认的这根也返回,直接入库就会造成最后一根数据不准。另外,用after/before游标翻页时容易搞混方向,after是往更早翻,before是往更近翻,很多人正好用反。

解决:入库时对confirm == "0"的K线做特殊处理,要么跳过等下一根,要么单独标记为“未确认”并在下次拉取时覆盖更新。翻页时以上一页最早一条的ts作为after参数继续拉更早的数据,形成一个单调向前的爬取序列。

5.4 请求一快就被限频:429/50011 与退避重试

现象:脚本跑到一半,报429或50011,加日志一看是高频请求触发了接口限频。

原因:OK交易所的 web api 按 IP 维度限频,不同接口的限频档位不同,行情类接口和交易类接口是分开算的。单线程循环里不控制请求间隔,很容易在批量拉历史数据或批量下单时触发。

解决:客户端里加一个请求间隔控制器,我一般用threading.SLock配合最小间隔time.sleep(0.1)做全局节流,批量任务再把间隔放宽到 0.2~0.3 秒。遇到限频错误码就退避重试,退避时间按0.5s / 1s / 2s递增,连续重试超过 3 次就停下来人工看。

5.5 杠杆单被当成现货单拒绝:tdMode 传错与账户资金隔离

现象:用杠杆下单接口传了tdMode=isolated,结果报错说账户没有开通杠杆或者资金不足,但现货账户里明明有币。

原因:OK交易所的现货和杠杆共用“现货/杠杆”账户资金池,但杠杆下单需要账户处于可杠杆状态,且资金要在这个账户体系内。如果代码里tdMode没生效或者资金没有划转到对应账户,就会报错。另外一个类似问题:订单已经用cash模式成交了,持仓查询里当然看不到杠杆仓位。

解决:下单前先调set-leverage确认倍数设置成功,再查balance看可用资金;杠杆仓位必须用positions查询而不是看现货余额变化。建议在下单函数里强制校验tdMode,把cash和cross/isolated分流到不同函数,避免复用一个下单函数时传错。

6. 进阶技巧:历史数据的增量同步与本地校验

历史数据拉取最容易出现“全量重跑”的低效问题。我习惯把历史K线的同步拆成两步:首次全量拉取 + 增量更新。全量拉取用after游标一路往前翻,翻到data为空或到达目标时间为止;增量更新则只拉最近 200~300 根,与本地最后一根ts对比,缺口小就只补最新几根,缺口大就回补全量。

def incremental_sync_candles(client: OkxV5Client, inst_id: str, bar: str = "1m"): latest = client.request("GET", "/api/v5/market/history-candles", params={"instId": inst_id, "bar": bar, "limit": "100"}) latest_ts = int(latest["data"][0][0]) # 服务器最新K线时间戳 local_ts = get_local_latest_ts(inst_id, bar) # 本地最新K线时间戳 if local_ts is None or latest_ts - local_ts > 120_000: full_sync(client, inst_id, bar) # 缺口大于2分钟,回补全量 else: upsert_candles(latest["data"]) # 否则直接覆盖更新最近100根

本地校验有两个动作:一个是以ts为主键入库,用INSERT OR REPLACE天然去重,重复拉取不会产生脏数据;另一个是检查最新一根K线的confirm字段,未确认的K线单独标记或直接跳过,等下一轮增量同步时自然覆盖成确认数据。跑数据落库后,抽查几个时间点的high/low/close与kline图表对照,确认没有错位。我已经习惯了任何行情库都先跑一遍“最近 100 根与服务器对比”的脚本再投入使用,这一步成本很低,能挡掉大部分数据质量事故。杠杆单当年因为没看清tgtCcy参数,市价按金额买入了超预期数量的币,差点爆仓,翻车之后所有下单函数都加了参数边界校验。希望这些细节能帮到你,少交点学费。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询