
简介面向Python开发者的OKEx V5 API封装工具包适合量化交易、账户操作与行情查询等自动化场景。代码将密钥签名、HTTP请求、数据解析等基础逻辑封装为可直接调用的模块显著降低入门门槛与重复开发成本。压缩包仅6KB包含7个Python脚本按功能拆分为行情与交易接口模块、统一客户端封装以及常量定义、工具函数、异常处理等支撑模块结构轻量清晰便于按需查阅与二次改造。已有4818人学习下载。借助该包可快速掌握OKEx V5接口调用方式直接复用账户余额查询、限价/市价下单、撤单、K线及最新价格拉取等功能并在此基础上扩展自定义交易策略或数据分析流程是入门OKEx V5 API的高性价比参考。1. 先讲清楚这套 OKX V5 API Python 封装覆盖交易、账户、查询三大块很多人第一次用 Python 调 OKX V5 API会先找个现成封装试跑行情跑通后扭头就去做策略结果到了真下单环节被签名、时间戳、限频这些隐藏问题卡住半天。这套封装我完整拆过一遍覆盖了行情、交易、账户操作、订单查询这几大类 REST 接口适合做网格策略、定投提醒和量化回测信号的人。它不解决策略本身但把「发请求、签名字、拿数据、解析返回」这层脏活整理得比较干净。你拿到手能直接初始化一个客户端然后调 ticker、下单、撤单、查持仓代码量比对着文档手搓少得多。2. 鉴权与代码骨架V5 签名体系拆开看四要素一个都不能少2.1 V5 相比 V3路径统一了限频和参数反而更细OKX 从 V3 升到 V5最直观的变化是 REST 路径全部归到 /api/v5 下面不再有 /api/swap/v3、/api/futures/v3 这种按产品线拆分的路径。你查行情、查账户、下单面对的是同一套 URL 规则这给封装省了大量重复代码。代价是每个接口的参数变严格了比如下单时必须显式传 tdMode保证金模式限价单必须传 px市价单必须传 sz 或 sz 的替代字段缺一个就给你返回参数错误。V5 另一个变化是限频策略更细。公共行情接口、交易接口、账户接口各有各的频率上限同一个接口在不同产品下也可能有差异。封装里如果没有做请求节流策略一跑起来很容易触发 431。所以拿到这套封装后我建议你先别急着加策略把客户端请求频率的日志打开观察前十分钟请求分布再做批量调度。2.2 鉴权四要素Key、Secret、Passphrase 与签名算法V5 的鉴权不复杂但细节多。每个请求头里需要带四样东西OK-ACCESS-KEY、OK-ACCESS-SIGN、OK-ACCESS-TIMESTAMP、OK-ACCESS-PASSPHRASE。如果你创建的是项目级 API Key还要额外带 OK-ACCESS-PROJECT。签名算法是 Base64 编码的 HMAC SHA256参与签名的字符串是时间戳、请求方法、请求路径和请求体拼起来的一整段。这里有一个容易忽略的点GET 请求的签名串里请求体是空字符串但 POST 请求如果传了空对象 {}参与签名的字符串就是 {} 而不是 。如果你在封装里统一处理成 json.dumps(body) 再签名就不会踩到这个坑。下面这张表是每个请求头的作用调试时对照着看会快很多。Header 字段作用出错时的常见表现OK-ACCESS-KEYAPI Key 明文50111 无效 KeyOK-ACCESS-SIGN签名结果50113 签名错误OK-ACCESS-TIMESTAMP请求时间戳90010 时间戳过期OK-ACCESS-PASSPHRASE创建 Key 时设置的密码短语50114 密码短语错误OK-ACCESS-PROJECT项目级 Key 的项目 ID50122 缺少项目信息2.3 最小可用封装一个带签名的通用请求客户端我一般会把客户端封装成一个类把签名、请求头、GET/POST 分发都收敛到一个方法里。这样外部调用方只需要关心接口路径和参数不用每次重复拼签名。下面这段代码是这类封装里最常见的基础形态import base64 import hashlib import hmac import json import time import requests class OkxClient: def __init__(self, api_key, secret_key, passphrase, base_urlhttps://www.okx.com, project_idNone): self.api_key api_key self.secret_key secret_key self.passphrase passphrase self.base_url base_url self.project_id project_id def _sign(self, timestamp, method, request_path, body_str): message timestamp method request_path body_str mac hmac.new( self.secret_key.encode(utf-8), message.encode(utf-8), hashlib.sha256, ) return base64.b64encode(mac.digest()).decode(utf-8) def _request(self, method, request_path, paramsNone, bodyNone): timestamp str(time.time()) body_str if body is not None: body_str json.dumps(body, separators(,, :)) sign self._sign(timestamp, method, request_path, body_str) 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.project_id: headers[OK-ACCESS-PROJECT] self.project_id url self.base_url request_path if params: url ? .join(f{k}{v} for k, v in params.items()) if method GET: resp requests.get(url, headersheaders, timeout10) else: resp requests.post(url, headersheaders, jsonbody, timeout10) return resp.json()这段代码里有两个细节值得说。第一签名用的 body_str 和实际发送的 body 必须完全一致所以 json.dumps 里显式用了 separators(,, :)去掉默认的空格。第二时间戳用 str(time.time())保留小数位服务端对带小数的 Unix 秒是接受的我实测过 int 时间戳也没问题但带小数更稳妥。base_url 留了默认值模拟盘切换时直接改这个字段就行。调用方式很简单。比如查 BTC-USDT 的单个 Ticker先初始化 client再传 instId 进去返回的 JSON 里 code 为 0 时 data 就是结果。client OkxClient(api_keyyour_key, secret_keyyour_secret, passphraseyour_passphrase) result client._request(GET, /api/v5/market/ticker, params{instId: BTC-USDT}) print(result[data][0][last])提示client 初始化后先跑一次公共接口确认基础联通再碰带签名的接口排查范围会小很多。3. 行情查询实战Ticker、K 线与数据清洗的落地写法3.1 调用方式公开行情也走统一请求通道OKX V5 的行情接口本质是公开接口不需要签名也能访问。但在封装里我习惯统一走 _request原因有两个一是代码路径单一出问题时只需要排查一个方法二是后续如果要切换模拟盘或加请求日志改一处就行。查询单个 Ticker 用 /api/v5/market/ticker批量 Ticker 用 /api/v5/market/tickersK 线用 /api/v5/market/candles。这里有个使用习惯问题很多人只在策略下单时才初始化 client查行情时直接 requests.get。短期看没问题但等你把行情、下单、账户查询拼到同一个调度框架里会发现不同模块各写各的请求逻辑加代理、加超时、加重试全都得改多遍。我一般把所有交易所接口都收敛到同一个 client 里后续维护成本低很多。3.2 字段处理把 Ticker 原始数据整理成可用的行情面板Ticker 接口返回的 data 是一个数组里面每个元素是 instId、last、bidPx、askPx、open24h、high24h、low24h 这一组字段。直接打印能看到原始数据但放到策略里用最好先转成字典或 DataFrame再按需要的字段筛选。常见做法是建一个解析函数只留自己关心的字段顺便统一价格的数据类型。import pandas as pd def parse_ticker(raw_list): rows [] for item in raw_list: rows.append({ instId: item[instId], last: float(item[last]), bid: float(item[bidPx]), ask: float(item[askPx]), high24h: float(item[high24h]), low24h: float(item[low24h]), vol24h: float(item[vol24h]), ts: int(item[ts]), }) return pd.DataFrame(rows) raw client._request(GET, /api/v5/market/tickers) ticker_df parse_ticker(raw[data]) print(ticker_df.head())注意OKX 返回的 ts 是毫秒级 Unix 时间戳转换成 datetime 时要除以 1000。这个坑在画图和时间过滤时特别明显我第一次用 pandas 直接转画出来的横轴差了 8 小时排查了半天。3.3 换周期拉 K 线bar 参数与缓存习惯K 线接口的 bar 参数决定周期1m、5m、15m、1H、4H、1D 是常见取值。每次请求最多返回 300 根 K 线这个上限要牢记策略里如果需要更长的历史数据就得做分页或缓存。我一般会在本地维护一个按 instId 和 bar 维度拆分的缓存字典避免每次跑策略都重复拉同样的数据。kline_cache {} def get_candles(client, inst_id, bar1H, limit200): cache_key f{inst_id}_{bar} if cache_key in kline_cache: return kline_cache[cache_key] params {instId: inst_id, bar: bar, limit: min(limit, 300)} raw client._request(GET, /api/v5/market/candles, paramsparams) candles raw[data] kline_cache[cache_key] candles return candles缓存这块不要只缓存内存进程一重启就没了。如果策略是定期任务可以把 K 线落成 CSV 或 SQLite增量追加最后一根未收盘的 bar。这样每次拉取只需要请求一次把新数据拼到旧数据末尾大大减少触发限频的概率。接口返回的 K 线顺序是从新到旧拼接时要注意先把列表 reverse 再追加。4. 交易与账户操作从下单参数到撤单查询的完整闭环4.1 下单前置tdMode、ordType、instId 三个参数先对齐下单接口 /api/v5/trade/order 的参数比行情接口多得多但核心就三个instId 决定交易什么tdMode 决定保证金模式ordType 决定订单类型。tdMode 的取值是 cash现货现金、isolated逐仓、cross全仓。现货交易一般用 cash合约逐仓用 isolated全仓用 cross这个选错会直接导致报错。ordType 的取值是 market市价、limit限价、post_only只做 maker、fok全部成交或取消、ioc立即成交或取消。网格策略常用 limit 和 post_only抢反弹常用 market套利用 fok 比较多。sz 是数量px 是价格限价单必传 px市价单不用传 px。下面这张表是实际写单时的高频参数参数含义必填场景常见错误instId产品 ID所有订单大小写或后缀写错tdMode保证金模式所有订单现货传了 crossside买卖方向所有订单buy/sell 拼错ordType订单类型所有订单market 单还带 pxsz下单数量所有订单小数位超精度px下单价格限价单市价单多传了 pxreduceOnly只减仓合约平仓开仓误传 true4.2 下单和撤单带 body 的 POST 请求怎么写下单是 POST 请求参数要放进 body 里不能拼在 URL 上。签名时参与计算的也是这个 body 字符串。下面这段代码演示了限价单和市价单的写法以及对应撤单的调用方式def place_order(client, inst_id, td_mode, side, ord_type, sz, pxNone): body { instId: inst_id, tdMode: td_mode, side: side, ordType: ord_type, sz: str(sz), } if px is not None: body[px] str(px) result client._request(POST, /api/v5/trade/order, bodybody) if result[code] 0: return result[data][0][ordId] else: raise RuntimeError(f下单失败: {result[code]} {result[msg]}) def cancel_order(client, inst_id, ord_id): body {instId: inst_id, ordId: ord_id} result client._request(POST, /api/v5/trade/cancel-order, bodybody) return result[code] 0这里的 sz 和 px 我都转成了字符串。OKX 对数字精度的要求比较严格直接传 float 在某些接口上可能因为浮点精度问题被拒绝。比如数量是 0.1Python 里可能是 0.100000000000000005转成字符串后发出去就有问题。我一般用 Decimal 先做量化处理再转字符串这个习惯帮我避免了好几次下单被拒的翻车。撤单有两个标识可以用ordId 是下单后返回的订单 IDclOrdId 是你在下单时自己设置的客户端订单 ID。网格策略里我习惯用 clOrdId 做映射因为每个网格单的用途可以编码进这个 ID 里撤单时不用额外维护订单和策略的关联表。撤单接口返回的 code 为 0 只代表撤单请求被接受不代表订单一定撤销成功之后还需要查订单状态确认。4.3 余额与持仓查询账户接口的字段对应关系查余额用 /api/v5/account/balance返回的 data[0].details 是一个数组每个元素对应一个币种包含 ccy、cashBal、availBal、eqUsd 这些字段。其中 availBal 是可用余额cashBal 是总余额eqUsd 是按美元折算的等值。做仓位管理时我一般用 availBal 做下单上限判断用 eqUsd 做整体风险敞口统计。def get_balance(client): result client._request(GET, /api/v5/account/balance) if result[code] ! 0: return {} details result[data][0][details] balance_map {} for item in details: balance_map[item[ccy]] { total: float(item[cashBal]), available: float(item[availBal]), equity_usd: float(item[eqUsd]), } return balance_map def get_positions(client, inst_typeNone): params {} if inst_type: params[instType] inst_type result client._request(GET, /api/v5/account/positions, paramsparams) positions [] for item in result[data]: positions.append({ instId: item[instId], pos: float(item[pos]), avgPx: float(item[avgPx]), upl: float(item[upl]), marginMode: item[marginMode], }) return positions持仓接口要注意一点如果你的合约账户设置的是双向持仓模式返回的持仓里 posSide 会区分 long 和 short下单时也必须在 body 里传 posSide。如果是单向持仓模式posSide 默认是 net。封装里没有做模式探测我一般在初始化时手动配置一次账户设置把 posSide 的取值写死在配置里避免每次下单都要查一遍账户配置。5. 避坑与常见问题V5 API 最容易翻车的五个细节5.1 时间戳过期报错 90010 或 50111 时的排查顺序现象第一次发带签名请求返回 90010 时间戳过期或 50111 Key 无效。原因本地系统时间和 OKX 服务器时间偏差超过 30 秒。开发机时间经常因为休眠、手动改时区等原因漂移而签名里用的是本地时间戳服务端验签时会比对时间窗口。解决先调用 /api/v5/public/time 获取服务器时间再校准本地时间。脚本里可以在 client 初始化时把本地时间和服务器时间的差值算出来后续每次请求在时间戳上加上这个偏移量。我在部署策略的云服务器上遇到过系统时钟慢 40 秒的情况加了偏移量后问题消失。5.2 instId 大小写与后缀BTC-USDT 和 BTC-USDT-SWAP 不是一回事现象行情能查到 BTC-USDT但下单时报产品不存在。原因OKX 的 instId 区分大小写现货是 BTC-USDT永续合约是 BTC-USDT-SWAP交割合约是 BTC-USDT-YYMMDD。很多人把合约的 instId 写成现货格式接口直接拒绝。解决写配置时统一从一个常量表里取 instId不要手敲。我常用的做法是把所有交易对定义在 config 文件里现货和合约分开下单前先检查 instId 是否在允许列表里。另外查询产品列表用 /api/v5/public/instruments参数传 instTypeSPOT 或 SWAP能拿到完整的 instId 清单第一次对接时先拉一遍存本地。5.3 签名不一致JSON 序列化里的隐藏空格问题现象同样的参数用 Python 的 requests 发就报 50113 签名错误但用文档里的示例代码就能通。原因json.dumps 默认会在逗号后面加空格而你签名用的字符串和实际发送的 body 字符串可能不一致。比如签名时用了 json.dumps(body)但发送时 requests 又内部序列化了一次两边格式不同服务端验签失败。这个本质是参与签名的字符串和实际传输的字符串不一致。解决签名和发送用同一个序列化结果。代码里先 body_str json.dumps(body, separators(,, :))签名用 body_str发送时也把 body_str 传给 requests。requests 的 json 参数会重新序列化所以要用 data 参数传字符串同时设置 Content-Type 为 application/json。封装里把这一步收敛好后面所有接口都不用再操心。5.4 限频 431把请求批处理而不是一秒打二十个现象策略跑起来后连续请求出现 HTTP 431频率高的时候行情和下单接口一起报错。原因V5 对每个接口有独立的频率限制公共行情接口一般 20 次/2 秒部分接口只有 10 次/2 秒。策略里如果循环里直接调行情接口几秒钟就能打满限额。更隐蔽的是下单接口高频网格策略一次批量撤单可能触发交易接口的限频导致关键撤单失败。解决在封装里加一个简单的限速器按接口维度记录最近请求时间间隔不足就 sleep。更重要的是把请求批量化比如 Grid 策略的批量撤单接口 /api/v5/trade/cancel-order一次可以传多个 ordId把几十个撤单请求合并成一个。查 K 线时用上面提到过的缓存减少重复请求。限频响应头里有 OKX-LIMIT-REMAINING 这类字段可以做监控告警。5.5 模拟盘与实盘切换一个请求头的差别现象本地测试正常切到生产环境后订单行为完全不对或者反过来模拟盘正常实盘报错。原因OKX 的模拟盘不是通过不同域名切换的而是在请求头里加 OKX-SIMULATED-TRADING: 1。封装如果没有暴露这个开关切换环境时很容易漏掉导致模拟盘和实盘用的是同一套配置。解决在 client 初始化时加一个 simulated 参数为 True 时自动带上这个请求头。同时把 API Key 配置独立管理模拟盘 Key 和实盘 Key 分开绝对不能共用。我犯过最蠢的错误是模拟盘策略跑了一周赚了 30%切实盘时忘记换 Key结果实盘账号收到一堆模拟盘的垃圾订单之后我把环境名写进了日志前缀每次启动第一行打出来。6. 进阶把历史 K 线和历史订单完整拉下来的分页技巧6.1 历史 K 线翻页100 根/次的上限与 after 方向行情接口 /api/v5/market/candles 只返回最近 300 根要拉更长的历史必须用 /api/v5/market/history-candles每次最多 100 根。分页参数是 after 和 before这里的语义有点反直觉after 传 ID 表示请求该 ID 之前更早的数据before 传 ID 表示请求该 ID 之后更晚的数据。我一般用 after 往前翻循环里每次都取当前最早一根 K 线的 ts 作为下一次的 after。def fetch_history_candles(client, inst_id, bar, start_ts, end_ts): all_candles [] current start_ts while current end_ts: params { instId: inst_id, bar: bar, after: str(current), limit: 100, } result client._request(GET, /api/v5/market/history-candles, paramsparams) if result[code] ! 0 or not result[data]: break candles result[data] all_candles.extend(candles) oldest_ts int(candles[-1][0]) if oldest_ts current: break current oldest_ts time.sleep(0.1) all_candles.sort(keylambda x: int(x[0])) return all_candles这段代码里有个保护逻辑if oldest_ts current: break。正常情况下 after 应该越翻越早但如果接口返回异常或数据断层oldest_ts 可能没有变小这时候不加保护就会死循环。sleep 0.1 是为了给限频留余量拉一天的历史 K 线大概几秒到十几秒属于可接受范围。6.2 历史订单滚动查询100 条/次与状态过滤历史订单接口 /api/v5/trade/orders-history 每次最多返回 100 条同样用 after 翻页。这个接口比 K 线复杂的地方在于它要求必传 instType而且 state 参数决定你查的是已成交、已撤销还是部分成交的订单。如果不传 state只返回最近 7 天的数据传了具体状态可以查更长时间。def fetch_order_history(client, inst_typeSPOT, statefilled, inst_idNone): all_orders [] after while True: params { instType: inst_type, state: state, limit: 100, } if after: params[after] after if inst_id: params[instId] inst_id result client._request(GET, /api/v5/trade/orders-history, paramsparams) if result[code] ! 0 or not result[data]: break batch result[data] all_orders.extend(batch) if len(batch) 100: break after batch[-1][ordId] time.sleep(0.2) return all_orders订单分页里有一个和 K 线类似的坑after 取的是当前批次最后一条的 ordId但接口的排序是从新到旧所以要把整个结果集最后再按 ordId 或 fillTime 升序排一遍避免拼接时顺序错乱。另外orders-history 接口默认只覆盖最近三个月更早的订单要单独按时间段查询封装里如果只做了滚动分页遇到时间跨度大的需求会漏数据。从那以后我每次对接新的交易所接口都会强制走一遍相同流程先跑通公共接口验证网络再跑一条带签名的查询确认鉴权最后才动下单。这个习惯帮我避开了大半翻车场景。这套 OKX V5 API 封装把前面这些脏活都处理过了行情、交易、账户、查询四类接口可以直接拿来跑策略希望帮到你。本文还有配套的精品资源点击获取