ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OKX Web API Python调用指南:签名、下单与历史数据拉取避坑

OKX Web API Python调用指南:签名、下单与历史数据拉取避坑 简介一份面向加密货币开发者的OKEx交易所Web API调用示例集围绕杠杆交易、现货买卖、历史记录与历史数据获取等常见场景提供可直接参考的Python脚本涵盖常用业务模块。代码按MVC思路组织将工具函数、常量配置、各业务接口封装及WebSocket客户端进行模块化拆分便于理解请求签名、参数构造和数据解析流程。压缩包共12个文件全部为py源码大小仅11KB结构紧凑、无冗余依赖适合快速阅读与二次开发便于按需修改。目前已有188人学习使用尤其适合刚接触真实交易所接口的量化交易初学者。借助这些代码可快速掌握下单、撤单、查持仓、拉取K线及订阅实时行情的方法同时留意杠杆交易的风险控制为构建自动化交易或数据分析工具提供基础支撑。1. 打开python.rar之前先想清楚OKX的Web API能替你干掉哪些手工活拿到一个“ok交易所的web api调用应用”的python.rar我第一反应不是急着解压看代码而是先确认这里面的思路是不是能直接跑通。OKX的Web API把行情、现货交易、杠杆交易、历史订单和历史K线全部暴露成REST接口用Python封装成脚本之后批量对账、定时收数据、自动下单这些操作就能从网页手工点按里解放出来。对想自己搭量化回测、跑交易机器人、批量导出历史流水的人这个方向就是最省事的入口对刚配好Python环境的新手它也算一份能反复演练的API实战教材。下面按一条完整落地的顺序展开环境、签名、现货与杠杆下单、历史数据拉全以及最容易让脚本翻车的那几个细节。2. 从零搭Python调用环境安装、密钥和签名机制2.1 干净的环境省一半DEBUG时间很多朋友拿到这类压缩包的第一步是直接python main.py然后被缺包、版本冲突、密钥硬编码一堆事劝退。我一般在项目里单独建一个虚拟环境Python版本选3.8以上就行。之前帮人排一个老脚本的错最后定位到是系统Python里装了pandas 0.x的老版本DataFrame的用法全变了。虚拟环境可以避免这种“我这能跑你那不行”的玄学问题。python -m venv okx_api source okx_api/bin/activate # Windows 用 okx_api\Scripts\activate pip install requests pandas python-dotenv openpyxl aiohttp这里每个依赖都有明确用途requests负责REST请求pandas负责整理K线和订单数据python-dotenv把密钥放进.env文件而不是写死进代码openpyxl是pandas写Excel时的底层引擎aiohttp留给后续并发拉数据用。装了虚拟环境后用PyCharm或VSCode打开项目时把解释器指到venv目录下之后终端里pip安装的包就能被IDE正确识别不会出现“终端能跑IDE报ModuleNotFoundError”的经典翻车。2.2 API Key、Passphrase和签名最容易写错的一步在OKX网页端创建V5 API Key时会同时生成API Key、Secret Key和Passphrase三样东西。Passphrase是你自己设置的不是随机生成的丢了只能重新创建。这三样放进项目根目录的.env文件里再通过python-dotenv读进来。OKX的REST接口要求每个请求带上OK-ACCESS-SIGN签名签名规则是把时间戳、请求方法、请求路径、请求体按顺序拼成一个字符串用HMAC SHA256加密再做Base64编码。这个时间戳必须是UTC时区、ISO8601格式、精确到毫秒。import base64 import hashlib import hmac import os from datetime import datetime, timezone import requests from dotenv import load_dotenv load_dotenv() API_KEY os.getenv(OKX_API_KEY) SECRET_KEY os.getenv(OKX_SECRET_KEY) PASSPHRASE os.getenv(OKX_PASSPHRASE) BASE_URL https://www.okx.com FLAG 0 # 0 实盘 1 模拟盘 def get_timestamp(): # OKX要求UTC时间精确到毫秒格式如 2025-01-01T12:00:00.123Z return datetime.now(timezone.utc).strftime(%Y-%m-%dT%H:%M:%S.%f)[:-3] Z def sign(message): mac hmac.new( SECRET_KEY.encode(utf-8), message.encode(utf-8), hashlib.sha256, ) return base64.b64encode(mac.digest()).decode(utf-8) def auth_header(method, request_path, body): timestamp get_timestamp() # 签名原文时间戳 方法 请求路径(含查询参数) 请求体 message timestamp method request_path body return { OK-ACCESS-KEY: API_KEY, OK-ACCESS-SIGN: sign(message), OK-ACCESS-TIMESTAMP: timestamp, OK-ACCESS-PASSPHRASE: PASSPHRASE, Content-Type: application/json, x-simulated-trading: FLAG, } def get_balance(): method GET path /api/v5/account/balance headers auth_header(method, path) resp requests.get(BASE_URL path, headersheaders, timeout10) return resp.json()这段代码里我把签名拆成三个函数方便后面所有接口复用。需要注意的细节有三个一是get_timestamp里的毫秒取法先把微秒转成字符串再切掉后三位末尾的Z由我们自己补上二是签名原文里的request_path必须和实际请求的路径完全一致如果带了查询参数查询串也要一并拼进去三是所有Header字段名称都不能拼错OK-ACCESS-SIGN不是SIGNATUREOK-ACCESS-TIMESTAMP不是TIMESTAMP。提示模拟盘的判断看x-simulated-trading这个Header传1表示请求发送到模拟盘传0或不传走实盘。这个字段和你的API Key是否具有模拟盘权限是两回事。2.3 第一次跑通账户余额接口写一个最小调用验证整个链路通不通data get_balance() if data[code] ! 0: print(请求失败:, data[code], data[msg]) else: details data[data][0][details] for item in details: if item[ccy] USDT: print(USDT可用:, item[availBal], 冻结:, item[frozenBal])OKX的REST返回统一是{code, msg, data}结构code为0表示成功。账户余额接口返回的data是一个数组数组里第一项是总览details里按币种列出可用余额、冻结余额和总权益。这里只取USDT做验证是因为后面现货下单、杠杆下单我们都要用USDT计价。能打印出数字说明接口连通、签名正确、密钥权限足够这一步通过后再进下一章的订单接口就只剩参数层面的问题了。3. 现货与杠杆下单订单参数和保证金模式是两座山3.1 现货限价单和市价单先看懂SZ和PX下单接口是POST /api/v5/trade/order。它不接受数字类型参数sz、px这些字段要求字符串这一点和很多人的直觉不一样尤其从WebSocket或数据库里取数字出来直接传会被OKX拒绝或产生精度问题。import json def place_order(inst_id, side, ord_type, sz, pxNone, td_modecash): method POST path /api/v5/trade/order body_payload { instId: inst_id, # 如 BTC-USDT 或 BTC-USDT-SWAP tdMode: td_mode, # 现货填 cash side: side, # buy / sell ordType: ord_type, # market / limit / post_only sz: str(sz), # 数量市价买单是金额 } if ord_type limit: if px is None: raise ValueError(限价单必须带px) body_payload[px] str(px) body_str json.dumps(body_payload) headers auth_header(method, path, body_str) resp requests.post(BASE_URL path, headersheaders, databody_str, timeout10) return resp.json()现货订单里最容易踩的是sz的含义。市价买单的sz是计价币金额比如买BTC-USDTsz填50表示买50 USDT等值的BTC市价卖单的sz是交易币数量sz填0.01表示卖出0.01个BTC。限价单无论买卖sz都是交易币数量px是委托价格。这个差异在OKX文档里写得很清楚但实际写代码的人十有八九会在市价单上翻一次车。如果想让脚本更安全下单前加一层判断市价买单sz语义是计价币其余情况sz语义是交易币。3.2 杠杆交易先设杠杆倍率再下单杠杆撮合和现货不同要先调用set-leverage接口把合约或保证金交易对的杠杆倍数设置好再下订单。直接下杠杆单而没设置倍率会用默认倍率这个默认值往往不是你要的。def set_leverage(inst_id, lever, mgn_modecross): method POST path /api/v5/account/set-leverage body_payload { instId: inst_id, lever: str(lever), mgnMode: mgn_mode, # cross 全仓 / isolated 逐仓 posSide: net, # 单向持仓模式 } body_str json.dumps(body_payload) headers auth_header(method, path, body_str) resp requests.post(BASE_URL path, headersheaders, databody_str, timeout10) return resp.json()全仓cross模式下账户里的全部余额都作为保证金强平风险分摊到整个账户逐仓isolated模式下只有该仓位占用的保证金参与风险计算。对新手来说逐仓更容易理解损失边界全仓更容易被一根大针插掉全部本金。杠杆单的下单方式和现货类似但要额外注意tdMode和posSide这两个字段字段现货杠杆/合约tdModecashcross 或 isolatedposSide不填net 或 long/shortsz币数量或金额合约张数或币数量合约的sz按张数还是币数量取决于instId对应的合约规格。BTC-USDT-SWAP通常1张等于0.01 BTCETH-USDT-SWAP 1张等于0.1 ETH。想知道具体数值可以调一次获取合约信息的接口拿到ctVal字段不要在代码里硬编码。我之前一个脚本就栽在这里同一段下单逻辑换了个合约品种sz从“张”变成了“币”成交数量直接放大了十倍。3.3 杠杆单为什么容易翻车算一笔强平账强平不是交易所随机触发的它由维持保证金率决定。逐仓多单的强平价可以做一个简化估算def est_liquidation_price(entry_price, leverage, maint_rate0.005): # 逐仓多单简化模型不含手续费和资金费 # 开10倍杠杆maint_rate按0.005估算 return entry_price * (1 - 1 / leverage maint_rate) price est_liquidation_price(68000, 10) print(f10倍多单强平估算价: {price:.0f})这个函数按68000开仓价算10倍杠杆的估算强平价大约在61800附近也就是价格反向波动不到10%就触发强平。真实强平价还会受手续费、资金费率、标记价格与最新价偏差的影响所以代码里至少留5%的安全垫。这也是为什么量化策略里杠杆单必须挂止损单的原因强平是交易所替你砍仓止损是你自己砍仓两者的价格差可能高达几个百分点而滑点在极端行情下还会再放大。4. 历史订单与历史K线分页方向和数据完整性是重点4.1 用orders-history拉历史成交单历史订单接口返回的是用户自己名下已经结束的订单包括成交、取消、部分成交等状态。拉取时用after做游标翻页after取当前页最旧一条订单的orderId就能拿到更早的数据。这个方向容易搞反after在字面上像“之后”实际语义是“该id之前的更早数据”。from urllib.parse import urlencode def get_order_history(inst_id, statefilled, afterNone, limit100): method GET path /api/v5/trade/orders-history params { instId: inst_id, state: state, # filled / canceled / partially_filled limit: str(limit), # 最大100 } if after: params[after] after full_path path ? urlencode(params) headers auth_header(method, full_path) # 签名用的full_path和实际请求URL完全一致 resp requests.get(BASE_URL full_path, headersheaders, timeout10) return resp.json()重点说一下签名这里为什么不用requests自带的params参数requests会把params里True渲染成True、把中文做编码转换和签名时urlencode的字符串可能不一致一旦有一个字符不同签名就失败。先把查询串拼好签名和请求都用同一个字符串能少踩很多隐蔽坑。orders-history只能查最近3个月的订单更早的历史要走orders-history-archived接口参数结构基本一样。资金流水和手续费明细则用bills-history接口按type筛选充提、交易、资金费。4.2 K线历史数据candles接口的周期和翻页限制行情K线接口是公开接口理论上不需要签名但如果放在带签名逻辑的统一代码里也无妨。单次最多返回300根K线默认倒序排列——最新的一根在最前面。def get_candles(inst_id, bar1H, limit300): method GET path /api/v5/market/candles params { instId: inst_id, bar: bar, # 1m/5m/15m/30m/1H/4H/1D/1W等 limit: str(limit), # 最大300 } full_path path ? urlencode(params) headers auth_header(method, full_path) resp requests.get(BASE_URL full_path, headersheaders, timeout10) return resp.json()返回的数据是一个二维数组每个元素依次是时间戳(毫秒)、开盘价、最高价、最低价、收盘价、成交量、币种成交量、计价币成交量、K线是否已确认。倒序意味着第一行是最新的K线想按时间正序处理必须反转数组。如果需要一天以上的历史用after参数翻页after取当前页最后一根K线的时间戳下一轮请求拿到的是该时间戳之前的数据。这样做历史回测数据补全比用before正着翻要稳定得多。4.3 把K线数据落盘成Excel两个数据处理坑拿到的原始数据全是字符串直接写进Excel会让后续计算变成噩梦。一般用pandas统一处理import pandas as pd def candles_to_df(raw): # raw是接口返回的二维数组 df pd.DataFrame( raw, columns[ts, open, high, low, close, vol, volCcy, volCcyQuote, confirm], ) df[ts] pd.to_datetime(df[ts].astype(int64), unitms) df df.sort_values(ts).reset_index(dropTrue) # 倒序转正序 for col in [open, high, low, close, vol]: df[col] df[col].astype(float) return df df candles_to_df(get_candles(BTC-USDT, 4H, 300)[data]) df.to_excel(btc_usdt_4H.xlsx, indexFalse)这里有两个容易被忽略的细节。第一接口返回的时间戳字段在JSON里是字符串直接做pd.to_datetime会把它当普通字符串解析必须先astype(int64)再指定unitms否则时间会错乱。第二列顺序必须严格按文档写open、high、low、close这个顺序错一位整列数据就串了。如果想在回测时只看最近一段可以用数组切片df.iloc[-300:]取最后300根或者等价地把sort_values之后的尾巴留下来避免每次重复拉全部数据。5. 避坑调OKX接口时最容易翻车的五个细节以下几条全是血泪经验每一条都按现象、原因、解决来写调接口遇到问题时优先查这几处。5.1 签名一直报50101现象第一次调get_balance就返回错误码50101提示签名无效。 原因最常见的是时间戳不对。用本地时间而非UTC时间或者时间戳精度只到秒都会让服务端验签失败。另一种情况是GET请求带查询参数时签名里的requestPath没带查询串导致服务端按完整路径验签不通过。 解决统一用2.2里的get_timestamp生成UTC毫秒时间戳签名用的request_path必须和实际请求URL完全一致带查询串就拼全再签。排查时先打印签名原文肉眼对照URL和body基本能立刻定位。5.2 历史数据翻页翻出中间断层现象循环用after翻页拉历史订单拉完后发现中间某些时间段的数据缺了总数比实际少。 原因after取值不对。有的接口要求after取当前页第一条最新的id有的要求取最后一条最旧的id具体取决于该接口返回的排序方向。candles接口返回倒序K线翻页要用最后一根的时间戳orders-history返回也是按时间倒序要取最后一条订单的orderId。 解决每翻一页先打印当前页data[0]和data[-1]的时间戳确认时间在往前走再决定after取哪条。拉完整段后做一个连续性校验把相邻记录的时间差和标准周期比一下有缺口立刻补。5.3 模拟盘和实盘混用导致订单下错环境现象代码里明明设置了模拟盘FLAG1下单后却发现实盘持仓变了。 原因x-simulated-trading这个Header在某些二次封装的SDK里没有被透传或者requests底层重定向时把它丢了。我见过更隐蔽的情况同一台机器上有两套.env一套模拟盘一套实盘key加载混了。 解决每次请求前打印实际发送的headers确认x-simulated-trading的值。生产环境尽量一个进程只加载一套密钥把运行环境和.env文件绑定不要用环境变量覆盖来绕。5.4 市价单的sz填错导致成交金额差一大截现象想买0.01个BTC市价买单sz填了0.01结果只成交了0.01 USDT等值的BTC手续费都不够。 原因现货市价买单的sz是“买入金额”以计价币计市价卖单的sz才是“卖出数量”以交易币计。很多人只记了一个“市价单下单数量”没注意到买卖方向不同语义也不同。 解决在place_order函数里加方向判断ordType为market且side为buy时sz强制按金额计价代码注释写明单位。如果策略里统一用数量下单就改用限价单或者在下单前把金额转成数量用行情接口的最新价做换算。5.5 批量拉K线触发429限频现象循环拉几十个交易对的K线跑到一半开始报429 Too Many Requests。 原因OKX对REST接口按路径分组限频相同路径的请求共享配额瞬时并发超过阈值就直接拒掉。单开一个requests.get不复用连接又会放大这个问题。import time def fetch_with_retry(inst_id, bar1H, max_retry3): for attempt in range(max_retry): try: resp get_candles(inst_id, bar, 300) if resp[code] 0: return resp if resp[code] 429: time.sleep(2 ** attempt) except requests.exceptions.HTTPError as e: if e.response.status_code 429: time.sleep(2 ** attempt) raise RuntimeError(f{inst_id} 连续重试失败)解决用requests.Session复用TCP连接每两个请求之间sleep一个固定间隔比如0.2秒遇到429时读取响应头里的重试时间做指数退避。给每个请求增加超时时间避免某个响应卡死拖慢整个循环。6. 进阶并发拉取K线、数据完整性校验和风控兜底6.1 用协程把拉K线提速一个量级上一章的429避坑说的是要控制并发但并不意味着不能用并发。requests是同步阻塞的拉30个交易对的日K线每个250毫秒总计7.5秒。换aiohttp做异步协程后总耗时基本等于最慢的那一个请求。import asyncio import aiohttp async def fetch_one(session, inst_id): url https://www.okx.com/api/v5/market/candles params {instId: inst_id, bar: 1D, limit: 300} async with session.get(url, paramsparams) as resp: payload await resp.json() return inst_id, payload[data] async def main(): insts [BTC-USDT, ETH-USDT, SOL-USDT, XRP-USDT] async with aiohttp.ClientSession() as session: tasks [fetch_one(session, inst_id) for inst_id in insts] results await asyncio.gather(*tasks) for inst_id, data in results: print(inst_id, 返回K线数:, len(data))协程并发数建议控制在5个以内不然就撞限频。asyncio.gather的好处是哪个先完成就先收哪个结果不会因为一个交易对接口抖动拖累整批请求。拉到数据后仍然走4.3的candles_to_df落盘。6.2 数据校验K线缺口比策略还重要回测数据缺一根K线结果可能完全失真。用1H数据时验证相邻两根时间戳差值是否严格等于3600秒df[gap_hours] df[ts].diff().dt.total_seconds() / 3600 missing df[df[gap_hours] ! 1] print(缺口数量:, len(missing)) if len(missing): print(missing[[ts, gap_hours]].head())这个校验放在每次拉完数据后立刻执行。有缺口就定位到缺失的具体时间点用该时间戳之前的after游标重新补拉而不是整段重来。6.3 风控参数进配置文件杠杆、单笔金额、日亏损额度这类参数我习惯放在一个config.ini里代码只读配置[risk] max_order_usdt 50 daily_loss_limit 200 max_leverage 3下单时先检查本次订单是否超过max_order_usdt当日已实现亏损超过daily_loss_limit就直接停止下单。真实账户上跑自动化时一个安静的熔断逻辑比任何花哨策略都值得优先写好。回到最开始那个python.rar解压后真正值得留下的其实就是签名函数、下单函数和这层风控配置。我个人的教训是最大的坑往往不在API文档而在你下完第一张真实杠杆单之后才意识到强平价和滑点比想象中近得多。先用模拟盘跑两周再用小额实盘跑两周确认orders-history和balance两个接口的返回结构和你的记录逻辑完全一致再谈上量。希望帮到你。本文还有配套的精品资源点击获取
返回列表