接口抽象层设计:定义统一接口规范、抽象基类设计、接口适配器模式应用

做量化交易系统,最头疼的是什么?

我个人觉得,不是策略写不出来,而是接口太多、太乱。你想想看,CTP、XTP、LTS、飞马……每家柜台都有自己的API,参数命名不一样,连接方式不一样,就连报单返回的字段名都各叫各的。我早期做交易系统时,最怕的就是换柜台——一换就是改一堆代码,改完还容易出bug。

所以,接口抽象层就派上用场了。说白了,就是给所有柜台接口套上一层“通用外壳”。上层策略只跟这层外壳打交道,底层换什么柜台,跟策略无关。今天我们就来聊聊这层外壳怎么设计。

一、统一接口规范:先定规矩

做抽象的第一步,是定规范。你得先想清楚:交易系统到底需要哪些核心操作?

我习惯把交易接口抽象成四个维度:

  • 连接管理:登录、登出、心跳、重连
  • 行情订阅:订阅合约、退订、行情回调
  • 交易执行:报单、撤单、改单
  • 数据反馈:成交回报、持仓更新、资金变动

每个维度定义一组方法签名。比如交易执行,不管底层是CTP还是XTP,上层调用的都是 place_order()cancel_order()。参数也统一:合约代码、买卖方向、价格、数量、开平标志。就这么简单。

核心原则:接口规范只定义“做什么”,不定义“怎么做”。具体怎么连、怎么发,交给底层实现。

二、抽象基类设计:画好蓝图

规范定好了,接下来用代码把它固定下来。Python里最直接的方式就是抽象基类(ABC)。

我一般会设计两个基类:BaseMarketBaseTrade。一个管行情,一个管交易。为什么分开?因为有些柜台行情和交易是分开的进程,分开设计更灵活。

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图展示了接口抽象层的整体结构:

策略层 (Strategy) 接口抽象层 BaseTrade (交易基类) | BaseMarket (行情基类) CTP适配器 CtpTradeAdapter XTP适配器 XtpTradeAdapter LTS适配器 LtsTradeAdapter CTP原生API XTP原生API LTS原生API 策略层只依赖抽象基类,不直接接触任何原生API 换柜台只需新增适配器,策略代码零改动

五、实际项目中的设计要点

光有架构图还不够,落地时还有几个细节要注意:

  1. 回调线程安全:柜台的回调通常来自不同线程,适配器里要做好锁保护。我习惯用 queue.Queue 把回调事件排队,然后统一处理。
  2. 错误码统一:每家柜台的错误码都不一样。适配器里要把错误码映射成统一的枚举,比如 ErrorCode.TIMEOUTErrorCode.INVALID_PARAM
  3. 日志埋点:每个适配器方法入口和出口都打日志。出了问题,看日志就能定位到是哪个柜台、哪个方法出的错。

小技巧:抽象基类里可以加一个 get_adapter_info() 方法,返回当前适配器的名称和版本号。排查问题时,一眼就能知道用的是哪个柜台的适配器。

六、总结

接口抽象层设计,说白了就是“隔离变化”。把不变的接口规范固定下来,把变化的柜台实现封装在适配器里。这样,你的交易系统就能轻松应对柜台切换,甚至同时对接多个柜台。

我个人觉得,这一步做扎实了,后面的性能优化、多柜台并行、灾备切换,都会变得非常顺畅。嗯,今天就聊到这里,希望对你有所帮助。

专注资料整理