资讯动态

OKEx API Python封装实战:从签名鉴权到量化交易避坑指南

发布时间:2026/10/9 3:05:19 来源:尧图企业网站定制
简介面向加密货币量化开发者的OKEx Web API Python调用示例集聚焦杠杆交易、现货交易、历史记录与历史数据获取等高频场景。包内共12个Python源文件按futures_api、spot_api、swap_api、account_api、lever_api等模块划分既有各业务接口封装也有utils、client、websocket等基础层支撑整体结构清晰便于嵌入Django/Flask等MVC架构的项目中二次开发。压缩包仅11KB代码量不大但覆盖了从签名鉴权、请求构造到数据解析的完整调用链路适合有Python基础并希望快速对接交易所接口的交易者或研究者。目前已有188人学习使用。借助这些现成模块读者能直接套用现货下单、杠杆仓位管理、历史K线拉取等功能并在此基础上扩展风控策略或回测逻辑节省从零阅读API文档的时间。1. 先泼盆冷水光有 OKEx API 密钥离自动交易还差这一层 Python 封装很多人拿到这份 rar 后的第一反应是有 API key、有 requests是不是就能直接下单了真不是。我见过太多写好了下单函数、挂在策略里跑结果死在签名超时、限频 429 和订单状态判断上。这份 rar 把 OKEx Web API 按业务模块拆成了 Python 封装现货、杠杆、永续、交割、账户、历史成交、K 线、websocket 都有对应文件基本覆盖了量化交易里「取数、回测、下单、对账」要用的那一层中间代码。它不是完整 MVC 框架更像 M 层下面的服务封装你只需要在自己的 Flask 路由或者 Linux 定时任务后面接它。适合谁有基础 Python 语法、想接 OKEx 做自动交易或拉历史数据回测的开发者和研究者新手拿着它至少能少走两个星期的签名弯路。2. 拆开 rar 看门道六个 API 模块与一个 websocket 客户端的分工2.1 文件清单每一层文件到底在管什么解压之后看到的文件不是乱放的基本按「底座 品种模块」组织。底座是 consts.py、utils.py、client.py、exceptions.py业务模块是 spot_api.py、lever_api.py、swap_api.py、futures_api.py、account_api.py、ett_api.py再加上一个 websocket.py 做实时推送。先看整体分工后面用哪个找哪个。文件职责典型使用场景consts.py域名、接口路径、限频与常量配置切换环境、统一路径管理utils.py签名、时间戳、参数预处理请求前组装鉴权内容client.pyHTTP 请求统一入口封装 requests所有 api 模块向下调用它spot_api.py现货买卖、撤单、订单查询现货自动下单lever_api.py杠杆借币、杠杆下单、还币保证金交易swap_api.py永续合约相关操作永续仓位管理futures_api.py交割合约相关操作季度或次季度合约策略account_api.py账户余额、持仓、杠杆倍数查询风控前置校验ett_api.py杠杆代币交易一篮子代币操作websocket.py行情、账户、订单推送实时行情监控exceptions.py自定义异常类型区分限频、鉴权失败与业务失败这种拆法有一个直接好处写策略时不用把不同品种的接口记混找到对应文件照着同一套调用风格写就行。我第一次拿到这套代码先把 spot_api.py 从头读了一遍发现它把请求路径直接写在方法里改起来非常直观比那种把所有接口塞进一个 client.py 的库要省心。而且它天然适合再包一层 MVC 的 ControllerAPI 封装专注取数和下单视图层你可以自己换。2.2 鉴权与签名client.py 和 utils.py 是怎么把请求变成合法调用的OKEx Web API 的鉴权核心是 HMAC SHA256 签名。每个请求要带四个头OK-ACCESS-KEY、OK-ACCESS-SIGN、OK-ACCESS-TIMESTAMP、OK-ACCESS-PASSPHRASE。签名消息不是整个 URL而是timestamp method requestPath requestBody拼接。对新手来说这里是个黑匣子但对这套封装而言最核心的全部在 utils.py 里。# utils.py 签名核心逻辑OKEx Web API 常见做法 import base64 import hashlib import hmac def sign_message(secret_key, timestamp, method, request_path, body): message timestamp method.upper() request_path (body or ) mac hmac.new( secret_key.encode(utf-8), message.encode(utf-8), digestmodhashlib.sha256, ) return base64.b64encode(mac.digest()).decode()这里的timestamp必须带毫秒且是 UTC 时间格式类似2024-01-01T08:00:00.123Z。method是大写 GET/POSTrequest_path是从域名后面开始的完整路径比如/api/spot/v3/orders不能漏掉开头的斜杠也不能追加额外参数。body是请求 JSON 的原始字符串不是格式化以后再拼两个字典内容一样但键顺序不一样签名结果就完全不同这是签名这块最常见的坑。client.py 只需要把签名结果塞进 headers然后统一走 requests。下面是我在此基础上常用的一套写法# client.py 统一请求头组装 import requests from datetime import datetime from utils import sign_message class Client: def __init__(self, api_key, secret_key, passphrase, base_urlhttps://www.okex.com): self.api_key api_key self.secret_key secret_key self.passphrase passphrase self.base_url base_url def _headers(self, method, path, body): ts datetime.utcnow().strftime(%Y-%m-%dT%H:%M:%S.%f)[:-3] Z sign sign_message(self.secret_key, ts, method, path, body) return { OK-ACCESS-KEY: self.api_key, OK-ACCESS-SIGN: sign, OK-ACCESS-TIMESTAMP: ts, OK-ACCESS-PASSPHRASE: self.passphrase, Content-Type: application/json, } def _request(self, method, path, paramsNone, bodyNone): headers self._headers(method, path, body) url self.base_url path if method GET: resp requests.get(url, headersheaders, paramsparams, timeout10) else: resp requests.post(url, headersheaders, databody, timeout10) return resp.json()注意databody这里传的是字符串不能传 dict否则 requests 会帮你重新编码签名就对不上了。我一般会先把body json.dumps(payload)打印出来看一遍确认没有中文转义问题再交给_request。说到底签名校验失败时交易所返回的错误码通常很笼统真正能信的依据只有你自己拼出的 message 字符串。2.3 环境准备requests、pandas 与 consts.py 的配置项先把依赖装齐。这套代码基于 requests 和 websocket-client我习惯用 pip 一次性装好后面做数据分析和回测时还要用到 pandas。pip install requests websocket-client pandaspandas 不是所有调用路径都必须但第 4 章要转 DataFrame 做回测提前装上省得后面补。装完之后第一件事不是写交易逻辑而是打开 consts.py 看 base_url、限频和日志配置。常见做法是 consts.py 里有一组 URL 常量真实盘和模拟盘分别注释切换环境只动这一处。我习惯把环境名抽成环境变量否则哪天测试完忘了切回来一笔真实下单可能就挂在不对的环境上干等。另外账号权限也要在 OKEx 后台先配好只读 API key 不能下单交易权限没开的 key 调下单接口会直接报错这一步不是代码问题但绝大多数人第一次跑 401 都会先怀疑代码写错了。3. 现货与杠杆下单从构造参数到订单状态查询的完整链路3.1 现货下单spot_api.py 里最常调的下单接口与参数说明现货下单是所有品种里最基础的一个。拿到 spot_api.py 以后先找创建订单的方法通常它的核心就是 POST/api/spot/v3/orders参数围绕 instrument_id、方向、价格和数量展开。# spot_api.py 现货限价单示意 def create_order(self, instrument_id, side, price, size, client_oidNone): body { instrument_id: instrument_id, side: side, type: limit, price: str(price), size: str(size), ord_type: 0, } if client_oid: body[client_oid] client_oid return self._post(/api/spot/v3/orders, body)这里有几个参数容易理解偏差。side只有 buy 和 sell大小写不能错type是 limit 或 market限价单必须带 price市价单通常不需要 price 但 size 的语义会变成购买金额size在 OKEx 现货里是基础币数量比如 BTC-USDT 里 size0.01 表示买入 0.01 个 BTC不是 0.01 个 USDT。ord_type是订单类型标志0 表示普通委托部分版本里还分 1、2、3 代表不同类型先查接口文档确认。还有client_oid这个参数强烈建议每次都传。它是客户端订单号最长 32 位能用来做幂等控制。网络超时后你重试下单时如果带着同一个 client_oid交易所能识别出重复请求避免同一笔单下两次这是交易程序里最实用的后悔药之一。我一般用时间戳 策略编号拼一个唯一串。3.2 杠杆账户lever_api.py 的借币、杠杆倍数与下单逻辑杠杆交易在这套封装里独立成 lever_api.py原因是它比现货多了一层账户状态借币、还币、倍率调整。先要理解 OKEx 杠杆的基本模型用自有资产做保证金借入另一种资产来放大仓位下单时可以选择自动借币也可以先借好再手动下。# lever_api.py 杠杆下单示意 def create_lever_order(self, instrument_id, side, price, size, leverage3, borrow_coinUSDT): body { instrument_id: instrument_id, side: side, type: limit, price: str(price), size: str(size), margin_trading: 1, leverage: leverage, borrow_coin: borrow_coin, } return self._post(/api/lever/v3/orders, body)margin_trading置为 1 表示这笔订单走保证金账户置 0 则是普通现货路径。leverage是账户杠杆倍数OKEx 的杠杆账户会有梯度倍数限制不是随便填 100 就有效超过上限会报业务错误。borrow_coin是自动借币时的借入币种矿工费、利息计费方式都跟它有关。这里我不建议在代码里把杠杆倍数写成死值最好从 account_api.py 里实时查账户杠杆倍数然后与下单参数做一致性校验。杠杆单的仓位价值是size * price * leverage所以强平价离你开仓价的远近完全取决于杠杆倍数和保证金率。我在把这个模块接入策略时会专门写一个函数计算预估强平价放在下单前打印出来。这个动作多一行代码但能避免很多半夜被爆仓电话叫醒的血泪经验。3.3 我一般怎么组织下单模块一个最小可运行的买卖加查单示例把前面的能力串起来一个最小可运行的现货下单流程长这样from client import Client from spot_api import SpotApi client Client(API_KEY, SECRET_KEY, PASSPHRASE) spot SpotApi(client) order_id spot.create_order( instrument_idBTC-USDT, sidebuy, price58000, size0.01, client_oiddemo_20240101_001, ) print(订单已提交:, order_id) # 轮询订单状态最多等 10 秒 import time for _ in range(20): detail spot.get_order(BTC-USDT, order_id) state detail.get(state) if state 2: print(已全部成交成交均价:, detail.get(avg_price)) break if state in (3, 4): print(订单被撤销或部分撤销) break time.sleep(0.5)这段代码的逻辑不复杂但有两个细节值得说。第一下单接口返回的只是一个受理结果不能默认等于成交真正的成交确认必须靠订单状态轮询第二state 是字符串不是数字2全部成交、3已撤销、4部分撤销比较的时候不要漏引号否则永远匹配不上。这套「提交 → 轮询 → 确认」的节奏是后面所有策略模块的地基。4. 拉历史数据和成交记录把 K 线变成可回测的 DataFrame4.1 历史 K 线与深度数据接口路径、粒度参数与返回结构做回测第一步是拿历史 K 线。现货的 K 线路径是 GET/api/spot/v3/instruments/{instrument_id}/candles永续合约要把路径里的 spot 换成 swap交割合约换成 futures路径结构一致但返回字段在某些版本里会差一列。粒度参数常见枚举是 1m、3m、5m、15m、30m、1H、2H、4H、6H、12H、1D不同版本也支持秒级 bar用之前先看 consts.py 里有没有写死。# spot_api.py 获取 K 线示意 def get_candles(self, instrument_id, granularity1m, limit100): params { granularity: granularity, limit: limit, } return self._get( /api/spot/v3/instruments/{0}/candles.format(instrument_id), params, )返回结构是嵌套数组不是 JSON 对象。每个元素大概按时间、开、高、低、收、成交量排列时间是毫秒时间戳。第一次用的时候我直接把原始 JSON 打到了屏幕上才意识到它不像很多 REST 接口那样给你带字段名所以写解析时必须按位置取列。深度数据则是另一套接口返回买一卖一到买 N 卖 N 的价格和数量数组做盘口策略时才需要普通回测一般用不上。4.2 成交历史与账户流水order history 和 fills 的读取方式历史订单和成交明细是评估策略真实成本的关键。回测里很多人只算 K 线价格但真实交易还有手续费、滑点和部分成交这些数据只有从订单历史和 fills 接口里才能拿到。# account_api.py / spot_api.py 获取订单历史示意 def get_order_history(self, instrument_id, statusall, beforeNone, afterNone, limit50): params { instrument_id: instrument_id, status: status, limit: limit, } if before: params[before] before if after: params[after] after return self._get(/api/spot/v3/orders, params)status可以按需求过滤all 是所有历史订单open 是当前未成交filled 是已成交cancelled 是已撤销。before和after是分页游标注意它跟普通 REST 的 page 参数语义完全不同before是查询该时间之前的记录after是查询该时间之后的记录两个一起用容易绕晕。成交明细 fills 接口会返回每笔成交的价格、数量、手续费和手续费币种我拿它来统计实际成交均价和现货账户总成本。拉这些数据时建议在 Linux 上用 crontab 每天定时落盘一次不然历史太久以后翻页会很慢。4.3 数据落地分钟 K 线转 DataFrame 的常规做法拿到原始 K 线数组以后第一件事是转成规范的 DataFrame并且把时间列变成索引。下面是沿用比较多的一套处理逻辑。# 把 OKEx K 线数组转成 pandas DataFrame import pandas as pd def candles_to_dataframe(raw): df pd.DataFrame( raw, columns[ts, open, high, low, close, volume], ) df[ts] pd.to_datetime(df[ts].astype(int64), unitms, utcTrue) for col in [open, high, low, close, volume]: df[col] df[col].astype(float) df df.sort_values(ts).drop_duplicates(ts).reset_index(dropTrue) return df这里做了三个动作时间戳转 datetime全部价格转 float按时间排序并去重。最后一个动作很容易被忽略因为 OKEx 的 K 线接口在分页边界上偶尔会返回重复时间戳如果不去重后面的技术指标计算会出现诡异毛刺。成交量这一列的单位要特别留意不同接口可能返回币数量也可能返回计价货币数量最好在转换时确认单位并统一命名比如volume_coin和volume_quote否则回测资金曲线会跟真实账户对不上那种「策略收益 30% 实盘亏 10%」的翻车十有八九就是单位没对齐。5. 避坑手册签名过期、限频误伤、杠杆单位错位、websocket 断线5.1 时间戳与签名不一致现象连续几个请求返回 401 Invalid Sign重新在网页端登录后又能用一段时间本地偶尔成功偶尔失败。原因本地系统时间偏差超过 30 秒或者签名 message 里的 body 与请求实际发送的内容不一致。我遇到过把 Python dict 转成 JSON 后又做了一次ensure_ascii处理导致字符串变化的坑也有一次是请求路径末尾少了斜杠签名用了带斜杠路径、请求却发了不带斜杠的地址。还有一些时间是本地时区没转 UTCdatetime.now() 直接拼进去服务端按 UTC 解析当然对不上。解决调试时先把待签名串原样打印出来与请求实际发送的 body 逐字符比对时间戳统一用datetime.utcnow()生成 ISO8601 毫秒格式如果服务器时间不准可以在启动时请求一次公共接口用返回头里的时间戳校准本地时钟。5.2 请求频率限制误伤现象程序跑到一半开始返回 429或者业务错误码提示请求过于频繁本来正常的行情行情也一起断掉。原因交易下单和行情拉取用了同一个 API key频率加起来超过了接口限频。OKEx 不同接口有各自的限频档位公共行情接口和私有交易接口经常分开计数但你的程序不会自动区分把所有请求堆在一个线程里就会互相拖累。解决公共行情数据优先走 websocket.py不要用 REST 轮询私有请求本地加令牌桶简单实现就是把每个品种的请求间隔控制在 200 毫秒以上失败响应里带了限频重置时间就等重置后再发。重试必须带退避第一次等 1 秒第二次等 2 秒最多等 8 秒无脑循环重试只会让限频更严重。5.3 杠杆倍数与下单数量单位错位现象下单成功但账户里的仓位价值不是你预设的 3 倍杠杆有时甚至提示可用余额不足。原因杠杆交易里 size 通常是基础币数量保证金由 leverage 折算不是「现货数量 × 杠杆」直接在数量上放大。常见误用是把现货下单的 size 习惯直接套到杠杆单以为传 0.03 就是 3 倍 0.01结果仓位完全不是预期。另外各交易对还有最小下单量和步进单位size 不满足 lot size 会被拒。解决下单前从 account_api.py 查询当前杠杆倍数和该交易对的最小下单量把 size 按最小步进取整比如最小步进是 0.01就不能传 0.015。杠杆倍数不要硬编码在策略里先从账户接口读真实值再跟下单参数做交叉校验不一致直接抛异常别让程序带着错误假设继续跑。5.4 websocket 掉线后行情静默现象订阅行情后前半小时正常后面价格不动了程序不报错日志也没有异常但策略已经基于过期价格在计算。原因websocket 连接被服务端断开或者订阅频道在长时间无操作后过期。很多客户端只处理首次连接成功没监听 close 事件断线后看起来连接还在实际上数据流已经停了。这个问题隐蔽因为它不像 HTTP 请求失败会立刻抛异常而是像一个黑匣子一样悄悄吞掉错误。解决websocket.py 里加两层保护。第一层每 30 秒发一次 ping服务端不响应就主动重连第二层重连后必须先重新 login再重新订阅之前的频道顺序不能反过来因为私有频道在重新登录前订阅会直接失败。每次收到 ticker 数据就更新一个本地时间戳这个时间戳超过 15 秒没动就触发告警。5.5 历史数据返回条数不够现象明明参数里 limit 填了 300实际只返回 100 或 200 条后面数据缺失回测结果看起来前期全是空仓。原因K 线接口和订单历史接口都有单次返回上限超出部分不会报错而是直接截断。常见做法是通过 before 和 after 翻页但两个参数方向和预期容易搞反翻页后和前一页拼接出现重叠或断层。解决写一个循环拉取函数每次取 100 条用返回里最早一条时间戳作为下一次请求的 before 参数一直拉到目标时间窗口或者返回为空为止。拉完之后合并、去重、按时间排序再存 CSV 或 Parquet。每次落盘前检查行数K 线有缺口的话按时间序列 resample 成目标频率后isna().sum()能直接看到空值位置。6. 从「能下单」到「敢下单」把 API 调用包到轮询状态机和风控里6.1 订单状态轮询不要只信下单返回值下单接口返回 ok 只代表请求被受理不代表订单已经成交。我见过不少人在回测里默认订单全部立即成交实盘跑起来才发现限价单可能挂一小时都没成交策略资金占用完全失控。正确的做法是把订单状态轮询封装成一个函数统一处理成交、撤销和超时。# 订单状态轮询返回最终状态 import time def wait_order_filled(api, instrument_id, order_id, timeout30): start time.time() while time.time() - start timeout: detail api.get_order(instrument_id, order_id) state detail.get(state) if state in (2, 3): return state, detail time.sleep(0.5) return timeout, detail这里的 state 字符串含义在不同接口版本里是稳定的2 全部成交3 已撤销4 部分撤销。超时不能直接当作失败处理因为限价单可能只是还没成交先查一下未成交挂单是否需要撤掉再决定是否重下。轮询间隔 0.5 秒是比较平衡的值太密会触发限频太疏会错过及时止损。6.2 一个只有三行的风控前置检查订单状态机解决的是「订单最后怎么了」风控解决的是「这笔单到底该不该下」。我的习惯是风控检查放在调用 API 函数之前而不是成交之后补救因为订单一旦出去撤单也要时间行情剧烈时这一两秒可能就是全部代价。# 下单前置检查单笔价值不超过总权益的 10% def pre_trade_check(equity, order_value_usdt, max_ratio0.1): if order_value_usdt equity * max_ratio: raise RuntimeError( single order {} exceeds {}% of equity.format( order_value_usdt, max_ratio * 100 ) ) return True这段代码逻辑很简单但它逼着你在下单前把 order_value_usdt 算清楚。现货是price * size杠杆单还要乘杠杆倍数这个乘积才是真实风险敞口。算清楚之后策略里的每笔单都会过这一道闸口单笔超限直接抛异常程序停止而不是继续执行后面的错误逻辑。从那以后我每次接一个新的交易所 API都强制走一遍「原始 JSON 落盘 → 对照文档逐字段核实 → 再写解析封装」的顺序风控检查永远放在签名之前。早年那次把杠杆单 size 当成现货数量直接下进去的翻车让我明白一个道理能成交的代码不是跑通一次就完事而是要在连续错误发生时还能把损失控制在预设范围内。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑