欧易 · 风控
OKX API 自动交易风控:仓位、订单与异常处理
你可能以为量化的风险来自行情:看错了方向就亏。但跑脚本时,能让账户瞬间归零的未必是行情,也可能是你自己的代码——一个写反的方向、一个停不下来的循环、一次断网后的重复下单。手动交易时,你按确认前还有最后一眼;脚本没有这一眼,它会忠实地、连续地把错误执行下去。
这篇我们(量化喵编辑组)专门讲量化风控,从「代码层面怎么防」切入。我们会拆开 API 交易特有的几类风险,给出可以照抄的止损、分仓、重试代码,再说账户安全的底线和「小额冷启动」的上场顺序。读完你应该能给自己的脚本装上几道刹车:单笔有上限(总量那条我们只给了骨架,缺的检查项文中列出来了,要你自己补齐)、止损不靠你盯盘、重发之前先查状态、凭证权限压到最小。刹车装齐不等于不会亏,但至少出错时你知道损失的边界在哪儿。
API 交易的特有风险
手动交易和 API 自动交易,风险类型不一样。手动交易的主要敌人是情绪和判断;API 交易在这之上,还多了一整类「机器特有」的风险。认清它们,才知道刹车该装在哪儿。
程序跑飞
这是最典型的。一段逻辑写错了——买卖方向搞反、止损条件判断写成了相反、循环少了退出条件——程序不会像人一样停下来想想「是不是哪里不对」,它会照着错误的指令一遍遍执行。人手动可能下错一单就发现了,脚本能在几秒内下错几十单。跑飞的杀伤力,在于它的「忠实」。
重复下单
网络抖动、请求超时时最容易出。你发了一个下单请求,没收到响应,脚本以为失败了,于是重发——但其实第一单已经成交了,结果你下了两单。在合约上,这意味着仓位翻倍、风险翻倍。重复下单的根子往往在于没处理好「不确定的中间状态」。
断网与掉线
脚本跑在你的电脑或服务器上,网络一断,它就和交易所失联了。糟糕的情况是:你的持仓还裸露在市场里,而本该保护它的止损逻辑跟着脚本一起掉线了。等你重连上来,可能已经面目全非。断网风险的本质,是别把所有保护都押在「脚本一直在线」这个假设上。
仓位管理与分仓
风控的第一道墙,是别让任何单笔错误能伤到你的全部资金。这靠仓位规则实现,而且这些规则要写死在代码里,不靠你临场克制。
几条值得一直遵守的原则:
- 设单笔上限。任何一单不超过总资金的某个固定比例(比如 1%–2%)。这样就算这一单方向全错,损失也有顶。
- 设总持仓上限。所有未平仓位加起来不超过一个总额,别让程序有机会把你全部资金一次压上去。
- 分仓,不要一把梭。把资金拆成几份分批进场,降低单一进场点位的判断错误带来的冲击。
- 给脚本留现金。账户里始终保留一部分不参与交易的资金,既是缓冲,也是出问题时你能手动操作的余地。
把这些写成代码里的硬约束,下单前先检查,不满足就拒绝下单。下面这段是骨架,不能直接搬去跑实盘——它只落实了「单笔上限」这一条,缺的部分我们在代码后面逐条列出来:
MAX_PER_ORDER_RATIO = 0.02 # 单笔不超过资金基数的 2%
MAX_TOTAL_RATIO = 0.30 # 总持仓不超过资金基数的 30%(本骨架尚未实现)
def check_position_limit(okx, symbol, order_value_usdt):
if not (order_value_usdt > 0):
raise ValueError('下单金额必须为正')
bal = okx.fetch_balance()
usdt_total = bal['USDT']['total'] # 注意:这只是 USDT 一个币种的余额,不等于账户总权益
# 单笔上限:拿 USDT 余额当基数,只在「现货账户、几乎全是 USDT」的简单情形下近似成立
if order_value_usdt > usdt_total * MAX_PER_ORDER_RATIO:
raise ValueError('超过单笔仓位上限,拒绝下单')
# TODO: 总持仓上限(MAX_TOTAL_RATIO)还没做,直接用这段等于没有总量约束
return True
这段之所以只能当骨架看,是因为它至少还缺四样东西,而每一样缺失都可能让「上限」形同虚设:
- 资金基数怎么算。
bal['USDT']['total']只是 USDT 这一个币种的余额。你要是还持有 BTC、ETH 或者跑的是合约账户,真正的基数应该是按统一计价口径折算出来的账户权益,而不是这一个数字。 - 现有持仓和挂单的敞口。总持仓上限要成立,就得把当前已有的持仓、以及还挂在盘口上、随时可能成交的委托一起算进去。只看「这一单多大」,连着下十单照样超限。
- 并发下单。几个策略同时调这个函数,它们可能各自查到同一份余额、各自判定「没超」,然后一起下单。要么给下单路径加锁串行化,要么在本地记一份「已占用额度」的账。
- 输入本身的合法性。金额必须为正——上面那行判断就是为这个加的。负数或 0 会让比较逻辑直接失去意义。
骨架归骨架,方向是对的:它把「我得控制仓位」这个意愿,变成了脚本下单前必须过的一道闸。意愿会动摇,代码不会。你要做的是把上面四项补齐,再让它上真金。
止损必须程序化(代码)
这篇里最要紧的一条:止损必须程序化。脚本是自动跑的,你不可能 24 小时盯盘;行情快速反向时,等你看到再手动平仓,往往已经晚了好几个价位。
程序化止损有两种思路,推荐配合使用:
思路一:交易所端的条件单 / 止损单。下主单的同时,在欧易挂一个止损委托,让交易所在触发价自动帮你平仓。它最大的好处是:就算你的脚本掉线了,这个止损仍然在交易所那边生效。这是对抗「断网时持仓裸奔」最可靠的一招。
思路二:脚本本地监控 + 触发平仓。脚本在循环里持续读取持仓的浮亏,一旦超过你设的阈值就主动发平仓单。它更灵活(能做移动止损、按时间止损等),但前提是脚本得在线。
下面是一个本地监控止损的最简骨架,演示逻辑(实盘前务必在 模拟盘 验证):
import ccxt, time
# 🔴 这段触发后会「市价卖出」,是立刻成交的真实动作。
# 先用模拟盘(Demo Trading)那把 Key 跑够,确认触发条件和数量都对,再考虑换实盘凭证。
# 模拟盘 Key 要在模拟交易环境里单独创建,和实盘不是同一套。
okx = ccxt.okx({
'apiKey': '模拟盘的_apiKey',
'secret': '模拟盘的_secret',
'password': '模拟盘的_Passphrase',
'enableRateLimit': True,
})
# 不加这行,下面的止损单就是拿真仓位在实盘市价平掉
okx.set_sandbox_mode(True)
# 自检:必须打印 1,打印 None 就别往下跑
print(okx.headers.get('x-simulated-trading'))
symbol = 'BTC/USDT'
entry_price = 60000 # 你的进场均价(示例)
amount = 0.001 # 持仓数量
stop_ratio = 0.05 # 跌 5% 止损
while True:
ticker = okx.fetch_ticker(symbol)
last = ticker['last']
drawdown = (entry_price - last) / entry_price
if drawdown >= stop_ratio:
okx.create_order(symbol, 'market', 'sell', amount)
print(f'触发止损,已市价平仓 @ {last}')
break
time.sleep(10) # 留足间隔,别打爆接口
注意两点:一是循环里务必有 sleep,否则会撞限频;二是本地止损只是补充,交易所端的止损单才是断网时的最后防线,两者都做才稳。
防重复下单与断网
重复下单的根源,是「我不确定上一单到底成没成」。防它的核心是:别盲目重发,先确认状态。
两个办法配合用:
用客户端自定义订单号(欧易文档里叫 clOrdId)。下单时带上一个你自己生成的 ID。交易所会拿它查重,但查重的范围只有你账户里当前未终结的订单——同一个 ID 撞上一笔还活着(挂单中或部分成交)的单,会被拒绝并报 51016「客户端订单号已存在」。
边界就在这里,一定要看清:按欧易文档,订单一旦进入终结状态(完全成交、已撤销),同一个 clOrdId 可以再次使用。换句话说,它挡得住「同一瞬间发了两次、第一次还挂在盘口」,挡不住「上一单其实已经成交了,你没收到响应又拿同一个 ID 发了一次」——后面这次,交易所会当成一笔全新的委托收下来。所以别把 clOrdId 当成幂等保证,它只是一个把手。
格式上还有硬要求:只能是字母和数字,最长 32 位。所以 str(uuid.uuid4()) 那种带连字符的 36 位字符串不合规,得取 .hex。
真正防重复的是下面这套流程,clOrdId 只是被这套流程握着的那个把手:
- 下单之前,先在本地把「这次的业务意图 → clOrdId → 状态」记下来并落盘,别只存在内存里——进程一崩,内存里的东西就没了。
- 请求超时或者干脆没有响应时,不要重发,先拿这个 clOrdId 去交易所查。
- 「当前挂单里没有」不等于「没成交」。它可能已经成交、进了历史,所以要用能覆盖终结状态的查法:拿 clOrdId 去查订单详情(挂单列表接口根本不认这个过滤条件),必要时再翻一遍成交明细。只查 open orders 就下结论,是这一步容易犯的错。
- 确认这个 clOrdId 确实不存在,才可以重发。
- 进程重启后,第一件事是从本地那份记录里把状态还悬着的意图捞出来,逐个对完状态,再让策略往下跑。
import ccxt, uuid
# clOrdId 只能是字母和数字、最长 32 位。
# uuid4() 转成字符串带连字符、共 36 位,不合规;取 .hex 才是 32 位纯字母数字。
def new_cid():
return 'meow' + uuid.uuid4().hex[:16] # 4 + 16 = 20 位,全是字母数字
def save_intent(cid, **fields):
"""把「意图 → clOrdId → 状态」落盘。写 SQLite、写文件都行,
关键是进程重启后还能读回来 —— 这里只示意,你自己实现。"""
...
def find_order_by_cid(okx, symbol, cid):
"""按 clOrdId 查单,走的是「订单详情」接口 GET /api/v5/trade/order,
挂着的和已终结的它都能查到。
注意:挂单列表和历史列表(orders-pending / orders-history)的
请求参数里没有 clOrdId 这一项,把它塞进 params 会被静默忽略 ——
接口照样返回一整页订单,你再在本地过滤,翻页翻不到就以为「没下成」。
所以按 cid 找单不要用那两个列表接口。"""
try:
# ordId 传 None,ccxt 就改用 params 里的 clOrdId 去查
return okx.fetch_order(None, symbol, params={'clOrdId': cid})
except ccxt.OrderNotFound:
return None
symbol = 'BTC/USDT'
cid = new_cid()
# 1) 先落盘,再发请求
save_intent(cid, symbol=symbol, side='buy', amount=0.001, price=50000, state='sending')
try:
order = okx.create_order(
symbol, 'limit', 'buy', 0.001, 50000,
params={'clientOrderId': cid}, # ccxt 会把它映射成欧易的 clOrdId
)
save_intent(cid, state='placed')
except Exception as e:
# 2) 超时或没响应:这一单到底成没成,现在是未知状态,不能直接重发
print('下单结果未知,先查:', e)
found = find_order_by_cid(okx, symbol, cid)
if found is None:
# 3) 查过挂单也查过历史,确认不存在 —— 这时才轮到重发
save_intent(cid, state='not_found_retry')
else:
# 已经进去了,记下真实状态就好,别发第二遍
save_intent(cid, state=found['status'])
超时 / 断网后,先查再补。上面那个 except 分支就是这条规则的代码形态:请求超时不代表下单失败,只代表你不知道结果。重连之后同理——先 fetch_open_orders、fetch_positions、必要时再翻一遍历史订单和成交,看清楚账户当前的真实状态,再让策略继续。把「先查状态、再动作」作为重连后的固定第一步,能挡掉大部分因失联导致的重复操作。
限频、超时与重试
风控不只是防亏钱,也包括防脚本「自己把自己搞挂」。限频和超时处理不好,会让你的策略在关键时刻失灵。
限频:初始化时打开 'enableRateLimit': True,ccxt 会自动在请求间插入间隔。高频循环里再加指数退避(撞一次等 1 秒,再撞等 2 秒、4 秒)。这部分我们在 API 常见报错 那篇讲得更细,这里只强调:任何循环里打接口,限速是标配。
超时:给请求设一个明确的超时时间,别让脚本因为某个请求卡住而整体僵死。ccxt 可以配 'timeout'(毫秒):
okx = ccxt.okx({
'apiKey': '你的_apiKey',
'secret': '你的_secret',
'password': '你的_Passphrase',
'enableRateLimit': True,
'timeout': 10000, # 单个请求 10 秒超时,避免卡死
})
重试有上限。遇到网络抖动可以重试,但要设最大次数,撞够了就报警/停下,而不是无限重试。无限重试本身就是一种「程序跑飞」——它会在你没注意时一直消耗请求、甚至重复操作。
权限与 IP 白名单:安全底线
风控的最底层,是账户安全。脚本写得再稳,凭证泄露了一切归零。这里有两条几乎没有例外的底线:
只勾「读取」和「交易」,永远别勾「提现」。跑量化根本用不到提现这个动作。只要 Key 没有提现权限,三件套哪天泄露了,对方也没法把币提到链上的地址去;一旦勾了提现,泄露就等于把保险柜钥匙交出去。🔴 但「不勾提现」不是保险,而且它兜住的范围比你可能以为的窄:按欧易文档的定义,交易权限本身就包含资金划转(资金账户↔交易账户、主账户↔子账户)和一部分账户设置,所以对方照样能在你的账户之间搬钱;至于亏损,只带交易权限的 Key 一样能让你亏光——对方可以在盘口薄的冷门交易对上自己挂单、自己接对手盘,把你的本金变成「交易亏损」流走,账单上全是正常成交。上面那些仓位和止损约束能压低伤害,但真正的第二道防线是 IP 白名单,必须和「不勾提现」一起做。
设 IP 白名单。如果你的脚本跑在固定 IP 的机器上(云服务器、固定公网 IP 的家宽),把这个 IP 加进 Key 的白名单,只接受来自它的请求,安全性再上一个台阶。欧易每把 Key 最多能绑 20 个 IP,支持 IPv4/IPv6 和网段,所以「出口偶尔会换」也不见得非得放弃白名单。出口 IP 确实不稳定时,方向是把出口固定下来(把策略挪到固定 IP 的机器上),而不是干脆不绑——还有一条容易被忽略的规则:没绑 IP、又带交易或提现权限的 Key,闲置 14 天后会被自动删除。不管绑不绑,凭证都别写死在会上传的代码里,改用环境变量或不会被同步的配置文件。
这两条加上前面的仓位和止损,构成了「就算最坏情况发生,损失也有边界」的防护。建 Key 的具体步骤在我们的 API 量化入门 里,也可以用 建 Key 前后的核对清单 开 Key 前对一遍。
小额冷启动:真金前清单
所有代码层的风控都装好了,上实盘还差最后一步:别一上来就重仓。新策略上真金,建议先走「小额冷启动」,按这个顺序:
- 先模拟盘连续跑。覆盖几轮涨跌,确认下单、撤单、止损、回调全都符合预期。这步零成本,跳过它等于拿真金当测试环境。
- 再用很小的真金冷启动。金额小到全亏了也不心疼,把它当学费。模拟盘没法复现的滑点、深度,这时候才会露出来。
- 盯着它跑头几天。别真撒手,确认实盘行为和模拟盘一致、止损真的会触发、没有重复下单,再逐步加码。
- 合约和杠杆最后考虑。现货跑稳之前,别碰杠杆。杠杆会把上面每一类风险都成倍放大,新手阶段强烈建议只做现货。
把这份清单和前面的代码约束合起来,你给脚本装的就不止是一道刹车,而是好几道:仓位有上限、止损会自动触发、重发前先查状态、凭证泄露时对方提不到链上(但仍可能划转、也可能靠恶意对敲把仓位做没,所以 IP 白名单要一起绑)、上场还先用小钱试。任何一道单独都不够,合起来才让「出错」和「爆仓」之间隔着足够的缓冲。
一个高频踩坑场景
print 刷屏 Ctrl+C 停掉即可,把方向改对、补上退出条件和 sleep(10),再加上单笔仓位上限那段检查就好。但要是这事发生在实盘、还带着杠杆,那串错单就是真金白银连续往外流。正是这种场景定下了一条铁规矩:止损逻辑必须先在模拟盘验证、循环必须有间隔和退出条件、下单前必须过仓位上限检查——三样缺一不可。
常见问题
API 自动交易最容易让人爆仓的风险是什么?
常见的元凶是程序的「忠实」:脚本不会像人一样犹豫。一个搞反的买卖方向、一个漏掉退出条件的循环、一个断网后重复发出的下单,人手动操作时可能在确认前就发现了,脚本却会连续执行几十遍。加上合约杠杆,这种错误能在极短时间内让本金归零。防它要把风控也写进代码,别指望自己盯着。
止损一定要写在代码里吗?手动盯不行吗?
强烈建议程序化。脚本是自动跑的,你不可能 24 小时盯着;一旦行情快速反向,等你看到再手动止损往往已经晚了。把止损逻辑写进策略,或者用欧易的条件单/止损单,让程序在触发线自动平仓,才跟得上自动交易的节奏。手动盯只能作为补充,不能作为唯一防线。
怎么防止脚本重复下单?
给订单带客户端自定义订单号(欧易文档里叫 clOrdId)有用,但别把它当幂等保证:按官方说明,它的查重范围只是你账户里当前未终结的订单——撞上一笔还挂着或部分成交的单会被拒(51016),而订单一旦完全成交或被撤销,同一个 ID 就可以再次使用,所以它挡不住「上一单已经成交、你又重发了一次」。真正防重的是流程:下单前先把「业务意图 → clOrdId → 状态」落盘;请求超时或没响应时先拿这个 ID 去查订单和成交,注意当前挂单里没有不等于没成交,还要翻历史订单和成交明细;确认确实不存在才重发;进程重启后先从本地记录把未决的意图恢复出来。格式上 clOrdId 只能是字母和数字、最长 32 位。
API Key 权限怎么设才安全?
跑量化的 Key 只勾「读取」和「交易」,永远别勾「提现」。这样就算三个凭证泄露,对方也没法把币提到链上的地址去。但别把它当成保险,它兜住的范围也比你可能以为的窄:按欧易文档,交易权限本身就含资金划转和一部分账户设置,对方照样能在你的资金账户、交易账户和子账户之间搬钱;而且只带交易权限的 Key 一样能让你亏光,拿到它的人可以在盘口薄的冷门交易对上自己挂单、自己接对手盘,本金以「交易亏损」的形式流走。再给 Key 绑定 IP 白名单(如果你跑在固定 IP 的机器上),只接受指定 IP 的请求,安全性再上一个台阶。自动化交易根本用不到提现这个动作。
新策略上实盘第一次该投多少?
用你就算全亏了也不心疼的金额,把它当学费而不是本金。先在模拟盘把流程跑顺,再用很小的真金做冷启动,盯着它跑头几天,确认实盘行为和模拟盘一致、滑点和成交都在预期内,再逐步加码。一上来就压重仓,等于把没验证过的代码直接赌上全部资金。
断网了正在跑的脚本会怎样?
取决于你怎么写的。糟糕的写法是断网时请求超时、脚本崩溃,或者盲目重试导致重复下单,而你的持仓还裸露在市场里没有止损。稳妥的做法是:给请求设超时和重试上限,重连后先查一遍当前持仓和挂单状态再决定动作,并且依赖交易所端的止损单(而不是只靠脚本本地判断),这样即使脚本掉线,平仓保护仍然在交易所那边生效。
风控写完代码也不算完,得跟着你的策略一起调整。建议你把这篇的清单存下来,每次上新策略都对一遍。接下来可以去看 API 常见报错 把脚本的稳定性补齐,新代码一律先丢 模拟盘 验证。