前言最近用 Python ccxt 从零搭建了一个跑在 Kraken Futures永续合约上的量化交易机器人断断续续迭代了很多版本——从最早的多币种梯度分配策略到后来简化成单币种固定保证金 自适应杠杆搜索策略。这篇文章不讲如何靠量化交易赚钱那种标题党我自己都不信而是想把这个过程中真正有工程价值的设计决策和踩过的坑记录下来希望对同样在做交易系统/量化机器人的朋友有点参考价值。先说结论这套系统目前跑在真实资金小额上核心目标不是跑赢市场而是在杠杆交易这种高风险场景下把程序不能崩、不能因为异常状态卡死、不能吞掉真实亏损而不自知这几件事做扎实。下面展开讲。一、整体架构项目大致分这么几个模块main.py # 主循环轮询、开仓决策、仓位调度 trader.py # 仓位生命周期管理开仓、监控、平仓结算 config.py # 配置中心币种池、策略参数、风控阈值 exchange_client.py # 交易所连接封装Kraken Futures Kraken Spot 双账户 risk.py # 杠杆等风控参数的最终校验闸门 profit_allocator.py / profit_reserve.py # 利润分流一部分自动转出到现货账户存起来 logger_setup.py # 滚动日志最初的版本是多币种梯度分配策略BTC/ETH/若干山寨币按不同比例分配仓位、随机开多空方向、按净利润率止盈止损。后来经过多轮实盘验证和调参逐步简化成了现在这版单币种BTC、固定保证金起步、自适应杠杆搜索、固定金额止盈、不设止损、止盈后强制冷却。这个简化过程本身就是一次挺有意思的从复杂到简单的重构经历后面细讲。二、几个我觉得值得写出来的设计模式1. 双轨止盈监控交易所托管单 软件轮询谁先触发算谁单纯依赖交易所的条件单比如挂一个止盈限价单有个问题程序如果重启、断线条件单本身还在交易所那边生效不会丢单这是好事但如果止盈条件是动态计算的比如净利润扣完手续费后达到某个百分比条件单没法表达这么复杂的逻辑。所以设计成两条轨道并行轨道 A硬止盈开仓的同时在交易所挂一个固定百分比的止盈限价单比如 10%作为断线兜底——哪怕程序挂了、电脑关机这个单子在交易所那边还是有效的不会变成完全裸奔的仓位。轨道 B软止盈程序主动轮询当前价格实时计算净利润已经扣除双边手续费一旦达到目标可以是固定百分比也可以是固定美金数就主动撤掉轨道A的挂单、市价平仓。每一轮监控先查轨道A的单子是不是已经被交易所自动成交了说明断线期间触发了没有的话再走轨道B的实时判断。谁先触发就按谁的结算逻辑走审计日志。defmonitor_position(futures_exchange,spot_exchange,position,quote_currencyNone):ifnot_position_still_open_on_exchange(futures_exchange,position.symbol):_reconcile_vanished_position(futures_exchange,position)return0.0,None,Falsetry:resultcheck_hard_take_profit_fill(futures_exchange,spot_exchange,position,quote_currency)ifresultisnotNone:returnresultexceptccxt.OrderNotFound:# 见下文踩坑实录第3条log.warning(...)returncheck_soft_take_profit(futures_exchange,spot_exchange,position,quote_currency)2. 自适应杠杆搜索顶格杠杆不等于能成交的杠杆这是整个项目里最反直觉的一个发现。按理说交易所标出来这个币种最高支持100倍杠杆那用100倍杠杆开仓应该没问题吧实测发现不是这样——用理论最高杠杆去下单经常被交易所以wouldCauseLiquidation会导致爆仓为由直接拒单哪怕账户里的可用保证金远超这笔仓位需要的量、名义仓位价值也很小。用 ccxt 的create_order_request()这是个纯本地方法不发真实网络请求对比了一下相同参数、只是杠杆不同的下单请求请求体本身没有任何问题。后来实测发现用低于理论最高值一截的杠杆比如从100倍降到42倍、60倍这种同样的仓位反而能顺利成交。猜测原因是交易所在做保证金检查的时候理论最高杠杆和实际能安全成交的杠杆之间需要留一点缓冲空间可能是为了防止价格波动瞬间就击穿维持保证金线顶格杠杆几乎没有这个缓冲稍微有点行情波动就会被判定为这笔单子开出来立刻就会有爆仓风险直接在下单阶段拒绝。基于这个发现把开仓逻辑改成了自适应搜索def_open_single_position_with_leverage_search(futures_exchange,symbol,side):start_leveragemin(LEVERAGE_SEARCH_START,symbol_max_leverage)# 不从理论最高开始margin_usdSINGLE_POSITION_MARGIN_USDwhilemargin_usdmargin_ceiling:leveragestart_leveragewhileleverageLEVERAGE_SEARCH_FLOOR:try:returnopen_position(...,leverageleverage)exceptException:leverage-LEVERAGE_SEARCH_STEP# 降杠杆重试margin_usd1.0# 杠杆降到底还不行加保证金从起始杠杆重新搜索# 都试完了还不行判定资金真的不够进入纯监控模式不再无限重试这套降杠杆 → 不行就加保证金 → 重新降杠杆的双层搜索本质上是把这个组合到底能不能成交这件事从猜测变成了实测——不再假设理论上限就是实际上限。3. 绝不崩溃不是一句口号是要对每一种异常状态都有明确归宿杠杆交易的仓位可能会以很多种方式消失正常止盈平仓、用户手动在网页/App上平仓、没设止损被交易所强制平仓……如果程序只处理我自己发起的平仓这一种情况剩下的场景全部会在下一轮监控的时候因为查不到这个仓位/这个订单而抛异常。这里最开始踩了一个坑仓位已经不存在了但轨道A的止盈单查询会抛ccxt.OrderNotFound代码里判断如果确认仓位已经不在了就清理收尾如果仓位还在那就是查询抽风把异常原样往外抛交给上层重试。问题是后来发现了一种没考虑到的情况仓位确实还在但对应的止盈单因为某种原因在交易所那边永久性地查不到了不是查询抽风是真的没了。这种情况下代码会把异常一直往外抛——导致每一轮监控都在这一步就中断根本没走到后面真正在起作用的软止盈判断仓位实际上处于半失明状态止盈目标即使达到了也不会被发现。这个 bug 在生产环境里安静地跑了小几个小时通过日志里同一个订单号反复报错几百次才发现。修复思路硬止盈轨道A查询失败不应该阻断软止盈轨道B的检查。两条轨道是互相独立的兜底关系一条失效了另一条应该照常工作而不是级联失败。try:resultcheck_hard_take_profit_fill(...)ifresultisnotNone:returnresultexceptccxt.OrderNotFound:log.warning(f硬止盈单查询不到但仓位确认还在跳过轨道A继续走轨道B)# 不 re-raise让流程继续往下走returncheck_soft_take_profit(...)这个教训挺典型的发生异常时不崩溃和发生异常时系统仍然在做它该做的事是两个不同层次的目标只做到第一层问题会以更隐蔽的方式存在不报错但也不干活。4. 资产隔离让利润物理上跑到亏不到的地方因为不设止损这是有意为之的风险选择后面单独说所以专门做了一个利润留存机制每次盈利结算从合约账户往现货账户转一小部分钱转账走的是交易所内部的钱包间划转接口不是市价买卖。这样做的好处是合约账户不管后面因为没有止损亏成什么样已经转出去的这部分钱是物理上独立的合约账户的保证金计算、强平逻辑完全碰不到现货钱包里的余额。这是一种既然选择了高风险敞口就至少让部分收益跑到风险敞口之外的朴素思路不是什么高深的对冲但确实是真实生效的资产隔离。三、几个环境/工程上的小坑nohup 启动的进程 程序自己的文件日志 handler如果 stdout 重定向到同一个日志文件会导致每一行日志被写两遍——一个是程序自己往文件里写的一个是终端输出被重定向进同一个文件的。定位过程中最开始一度怀疑是随机数逻辑有问题、连续开出同方向仓位后来才发现只是日志重复了。单实例互斥锁早期一次手滑同一个策略脚本被启动了两次两个进程互相抢同一份账户保证金、互相撤销对方挂的保护单很快意识到必须加进程级别的互斥锁用fcntl.flock实现启动时抢不到锁就直接退出。交易所批量下单接口和单笔下单接口的参数 schema 不完全一样批量接口要求止损单也必须带限价limitPrice单笔接口不需要ccxt 某些版本里批量下单自动生成的order_tag是整数而不是字符串会被交易所的 schema 校验直接拒绝——这类坑基本都是靠用本地不发网络请求的create_order_request()方法打印出真实请求体肉眼比对排查出来的比纯猜测靠谱很多。四、关于风险管理说点真实的这篇文章不想回避这一点当前这版策略是明确不设止损的风险管理完全依赖人工监控、随时可能手动平仓介入。这是经过反复权衡后的一个个人风险偏好选择不是技术上做不到止损代码里实际上是有完整的止损实现能力的一开始就实现了硬止损 软止损两层后来又加了比例制软止损和止损后反向重开的机制都实测跑通过只是当前这一版权衡下来选择了不用。这里想强调的是这套系统的工程可靠性不崩溃、状态一致、异常有归宿和它的资金风险敞口是两个完全独立的维度。一个工程上做得很扎实的自动化交易系统完全可能配上一个高风险的策略参数比如不设止损、高杠杆——工程质量不能替你承担策略选择的风险这个责任始终在做决策的人身上不会因为代码写得健壮就自动转移给系统。如果你也在做类似的东西这一点值得想清楚你要解决的是程序会不会crash还是我会不会亏钱这是两个问题用一套代码质量去回答另一个问题是会出事的。五、小结这个项目对我来说价值不在用它赚了多少钱目前投入金额很小本质上是个真实资金环境下的工程与风控实验而在于逼着自己去正视几个平时写业务代码不太会认真考虑的问题异常处理写不崩溃很容易写每种异常状态都有正确的业务归宿很难交易所文档写的理论上限不代表工程上应该直接拿来当参数用很多边界值需要实测验证不能纸上谈兵涉及真实资金的自动化系统日志的完整性和可读性本身就是风控的一部分——出问题的时候你能不能从日志里准确复现当时到底发生了什么直接决定了你能不能真正吸取教训。以上欢迎交流。本文不构成任何投资建议文中涉及的杠杆倍数、止盈止损参数均为个人测试环境下的选择不建议直接照搬用于自己的实盘交易。