3、TWAP算法基础框架搭建:Python环境准备、核心类结构设计、订单管理与状态机

好,咱们直接进入正题。TWAP算法说白了,就是把一个大单子拆成很多小单子,均匀地扔到市场里去。听起来简单吧?但真正落地的时候,坑可不少。我最早做这个的时候,以为写个循环定时下单就完事了,结果被实盘数据教做人——嗯,今天咱们就把这个框架搭扎实了。

3.1 Python环境准备:别在工具上翻车

我个人习惯用Python 3.8+,太老的版本有些异步库支持不好。你想想看,TWAP是要跟交易所实时交互的,网络延迟、数据推送,这些都得靠异步来搞定。

先列一下必备的库:

  • asyncio:Python原生异步框架,咱们的定时器、订单状态轮询全靠它
  • pandas:处理历史数据、计算时间切片
  • numpy:做点简单的数学运算,比如均分数量
  • loguru:日志库,比logging好用十倍。我在项目中吃过日志没打全的亏,后来全换成这个了

安装命令很简单:

pip install asyncio pandas numpy loguru
注意: 别用全局安装!我建议用虚拟环境。曾经有一次我在生产环境里因为依赖版本冲突,整个交易进程挂了半小时——那滋味,不想再体验第二次。

3.2 核心类结构设计:把架子搭稳

TWAP的核心逻辑,说白了就三个角色:策略引擎、订单管理器、状态机。我习惯把它们拆成三个独立的类,各管各的事。

先看整体结构,我画了一张图帮你理解:

TWAP算法核心类结构 TWAPEngine - 总数量、总时间 - 时间切片列表 - 当前切片索引 + run() 启动算法 OrderManager - 订单队列 - 已成交列表 - 未成交列表 + place_order() 下单 StateMachine - 当前状态 - 状态转换规则 - 回调函数列表 + transition() 状态转换 调用 状态反馈 订单结果

看到这张图了吧?三个类各司其职:TWAPEngine负责算时间、算数量;OrderManager只管跟交易所打交道;StateMachine盯着整个流程别出乱子。

咱们先看TWAPEngine的核心代码:

class TWAPEngine:
    def __init__(self, total_qty: float, total_seconds: int, slices: int):
        self.total_qty = total_qty
        self.total_seconds = total_seconds
        self.slices = slices
        self.current_slice = 0
        self.order_mgr = OrderManager()
        self.state_machine = StateMachine()
        
        # 计算每个时间片应该下多少
        self.slice_qty = total_qty / slices
        self.slice_interval = total_seconds / slices
        
    def calculate_slice(self):
        """计算当前时间片应该下单的数量"""
        # 这里有个细节:最后一片要把剩余数量全部吃掉
        if self.current_slice == self.slices - 1:
            remaining = self.total_qty - self.current_slice * self.slice_qty
            return round(remaining, 2)
        return round(self.slice_qty, 2)

关键点: 最后一片要吃掉所有剩余数量。为什么?因为浮点数除法会有误差,累积到最后可能差个0.01。我在实盘里见过因为0.01没成交导致整个算法卡死的——交易所那边订单最小单位是0.01,你少一分钱都不行。

3.3 订单管理:别让订单丢了

订单管理这块,说白了就是三个队列:待下单、已提交、已成交。我见过有人用一个列表搞定,结果订单状态一乱,整个算法就崩了。

来看看OrderManager怎么设计:

class OrderManager:
    def __init__(self):
        self.pending_orders = []    # 待下单队列
        self.active_orders = {}     # 已提交但未成交的
        self.filled_orders = []     # 已成交的
        
    async def place_order(self, qty: float, price: float):
        """异步下单,避免阻塞主循环"""
        order_id = await exchange.submit_order(qty, price)
        self.active_orders[order_id] = {
            'qty': qty,
            'price': price,
            'status': 'submitted',
            'timestamp': time.time()
        }
        return order_id
        
    async def check_fill(self, order_id: str):
        """检查订单是否成交"""
        status = await exchange.get_order_status(order_id)
        if status == 'filled':
            order = self.active_orders.pop(order_id)
            self.filled_orders.append(order)
            return True
        return False

避坑指南: 我曾经把订单ID用整数自增,结果交易所那边返回的ID是字符串,类型对不上,查了半天bug。后来统一用字符串,再也没出过问题。

3.4 状态机:给算法装上安全阀

状态机这东西,说白了就是给算法定规矩:什么情况下能做什么事。TWAP的状态其实不多,但每个状态转换都得小心。

我常用的状态定义:

状态 含义 允许的下一个状态
INIT 初始化完成,等待启动 RUNNING
RUNNING 正在执行切片下单 PAUSED, COMPLETED, ERROR
PAUSED 手动暂停或风控触发 RUNNING, STOPPED
COMPLETED 所有切片执行完毕
ERROR 发生不可恢复错误 STOPPED
STOPPED 彻底终止

代码实现其实不复杂:

class StateMachine:
    def __init__(self):
        self.state = 'INIT'
        self.callbacks = {}
        
    def register_callback(self, state: str, callback):
        """注册状态进入时的回调函数"""
        if state not in self.callbacks:
            self.callbacks[state] = []
        self.callbacks[state].append(callback)
        
    def transition(self, new_state: str):
        """状态转换,带校验"""
        allowed = {
            'INIT': ['RUNNING'],
            'RUNNING': ['PAUSED', 'COMPLETED', 'ERROR'],
            'PAUSED': ['RUNNING', 'STOPPED'],
            'COMPLETED': [],
            'ERROR': ['STOPPED'],
            'STOPPED': []
        }
        
        if new_state not in allowed.get(self.state, []):
            raise ValueError(f"非法状态转换: {self.state} -> {new_state}")
        
        old_state = self.state
        self.state = new_state
        print(f"状态变更: {old_state} -> {new_state}")
        
        # 触发回调
        if new_state in self.callbacks:
            for cb in self.callbacks[new_state]:
                cb()

注意: 状态转换一定要做校验。我见过有人直接self.state = new_state,结果从COMPLETED又跳回RUNNING,订单重复提交,被交易所封了账号。这种低级错误,咱们不能犯。

3.5 把它们串起来:一个完整的TWAP循环

好了,三个类都写好了,怎么配合呢?核心逻辑就是一个异步循环:

async def run_twap(self):
    self.state_machine.transition('RUNNING')
    
    for i in range(self.slices):
        if self.state_machine.state != 'RUNNING':
            break  # 如果被暂停或出错,立即停止
            
        qty = self.calculate_slice()
        order_id = await self.order_mgr.place_order(qty)
        
        # 等待一个时间间隔
        await asyncio.sleep(self.slice_interval)
        
        # 检查上一笔订单是否成交
        filled = await self.order_mgr.check_fill(order_id)
        if not filled:
            # 没成交?记录一下,但别卡死
            print(f"切片{i}未成交,继续下一片")
            
    self.state_machine.transition('COMPLETED')

你想想看,这个循环其实很脆弱——如果网络断了怎么办?如果交易所返回错误怎么办?嗯,这些异常处理咱们下一节再聊。但框架搭好了,后面加东西就简单了。

总结一下: TWAP框架的核心就三件事——算好时间片、管好订单、盯住状态。代码量不大,但每个细节都关系到实盘能不能跑。我个人建议,先把这三个类写稳了,再往上加风控、加日志、加监控。


专注资料整理