接口抽象层设计:定义统一接口规范、抽象基类设计、接口适配器模式应用
做量化交易系统,最头疼的是什么?
我个人觉得,不是策略写不出来,而是接口太多、太乱。你想想看,CTP、XTP、LTS、飞马……每家柜台都有自己的API,参数命名不一样,连接方式不一样,就连报单返回的字段名都各叫各的。我早期做交易系统时,最怕的就是换柜台——一换就是改一堆代码,改完还容易出bug。
所以,接口抽象层就派上用场了。说白了,就是给所有柜台接口套上一层“通用外壳”。上层策略只跟这层外壳打交道,底层换什么柜台,跟策略无关。今天我们就来聊聊这层外壳怎么设计。
一、统一接口规范:先定规矩
做抽象的第一步,是定规范。你得先想清楚:交易系统到底需要哪些核心操作?
我习惯把交易接口抽象成四个维度:
- 连接管理:登录、登出、心跳、重连
- 行情订阅:订阅合约、退订、行情回调
- 交易执行:报单、撤单、改单
- 数据反馈:成交回报、持仓更新、资金变动
每个维度定义一组方法签名。比如交易执行,不管底层是CTP还是XTP,上层调用的都是 place_order()、cancel_order()。参数也统一:合约代码、买卖方向、价格、数量、开平标志。就这么简单。
核心原则:接口规范只定义“做什么”,不定义“怎么做”。具体怎么连、怎么发,交给底层实现。
二、抽象基类设计:画好蓝图
规范定好了,接下来用代码把它固定下来。Python里最直接的方式就是抽象基类(ABC)。
我一般会设计两个基类:BaseMarket 和 BaseTrade。一个管行情,一个管交易。为什么分开?因为有些柜台行情和交易是分开的进程,分开设计更灵活。
from abc import ABC, abstractmethod
class BaseTrade(ABC):
"""交易接口抽象基类"""
@abstractmethod
def connect(self, config: dict) -> bool:
"""连接交易服务器"""
pass
@abstractmethod
def place_order(self, symbol: str, direction: str,
price: float, volume: int, offset: str) -> str:
"""报单,返回订单ID"""
pass
@abstractmethod
def cancel_order(self, order_id: str) -> bool:
"""撤单"""
pass
@abstractmethod
def on_order(self, order_data: dict):
"""订单状态回调"""
pass
@abstractmethod
def on_trade(self, trade_data: dict):
"""成交回调"""
pass
你看,每个方法都加了 @abstractmethod,子类必须实现。这就相当于画了一张蓝图:你要接入新柜台,就得按这个模板来填。
我的经验:抽象基类里不要写任何业务逻辑。只定义方法签名和文档注释。逻辑越少,子类越不容易出错。
三、接口适配器模式:让不同柜台“说同一种话”
抽象基类有了,但CTP的API跟我们的基类长得不一样怎么办?这时候适配器模式就登场了。
适配器模式,说白了就是“翻译官”。它把柜台原生的API调用,翻译成我们基类定义的方法。举个例子:
class CtpTradeAdapter(BaseTrade):
"""CTP交易接口适配器"""
def __init__(self):
self._api = CtpApi() # 原生CTP API
def connect(self, config: dict) -> bool:
# 把我们的config参数,转成CTP需要的格式
front_addr = config['front_addr']
broker_id = config['broker_id']
user_id = config['user_id']
password = config['password']
return self._api.login(front_addr, broker_id, user_id, password)
def place_order(self, symbol: str, direction: str,
price: float, volume: int, offset: str) -> str:
# 把统一参数转成CTP的字段
ctp_direction = self._map_direction(direction)
ctp_offset = self._map_offset(offset)
order_ref = self._api.insert_order(symbol, ctp_direction,
price, volume, ctp_offset)
return str(order_ref)
def _map_direction(self, direction: str) -> int:
mapping = {'buy': 0, 'sell': 1}
return mapping[direction]
def _map_offset(self, offset: str) -> int:
mapping = {'open': 0, 'close': 1, 'close_today': 2}
return mapping[offset]
看到没?适配器里主要做两件事:参数转换和回调映射。参数转换就是把我们的统一参数,转成柜台API能认的格式。回调映射就是把柜台推过来的数据,转成我们统一的数据结构。
避坑指南:我曾经在适配器里直接修改了原生API的配置对象,结果导致多个实例共享同一份配置,出现并发问题。记住:适配器里尽量做“拷贝转换”,不要直接修改传入的参数。
四、整体架构图
说了这么多,画张图帮你理清关系。下面这张SVG图展示了接口抽象层的整体结构:
五、实际项目中的设计要点
光有架构图还不够,落地时还有几个细节要注意:
- 回调线程安全:柜台的回调通常来自不同线程,适配器里要做好锁保护。我习惯用
queue.Queue把回调事件排队,然后统一处理。 - 错误码统一:每家柜台的错误码都不一样。适配器里要把错误码映射成统一的枚举,比如
ErrorCode.TIMEOUT、ErrorCode.INVALID_PARAM。 - 日志埋点:每个适配器方法入口和出口都打日志。出了问题,看日志就能定位到是哪个柜台、哪个方法出的错。
小技巧:抽象基类里可以加一个 get_adapter_info() 方法,返回当前适配器的名称和版本号。排查问题时,一眼就能知道用的是哪个柜台的适配器。
六、总结
接口抽象层设计,说白了就是“隔离变化”。把不变的接口规范固定下来,把变化的柜台实现封装在适配器里。这样,你的交易系统就能轻松应对柜台切换,甚至同时对接多个柜台。
我个人觉得,这一步做扎实了,后面的性能优化、多柜台并行、灾备切换,都会变得非常顺畅。嗯,今天就聊到这里,希望对你有所帮助。