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的核心逻辑,说白了就三个角色:策略引擎、订单管理器、状态机。我习惯把它们拆成三个独立的类,各管各的事。
先看整体结构,我画了一张图帮你理解:
看到这张图了吧?三个类各司其职: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框架的核心就三件事——算好时间片、管好订单、盯住状态。代码量不大,但每个细节都关系到实盘能不能跑。我个人建议,先把这三个类写稳了,再往上加风控、加日志、加监控。