CTA:交易引擎CtaEngine

交易引擎CtaEngine

CTA策略既可以用于投研、回测,也可以用于实盘交易。执行实盘交易的引擎是CtaEngine。为了保证策略在回测和实盘时接口保持统一,CtaEngineBacktestingEninge在与策略相关的接口设计上有相似之处。
而不同点在于,CtaEngine要对接实时行情接口、订单提交接口、UI界面接口,不需要绩效分析功能。

代码解读

CtaEngine的代码在vnpy_ctastrategy -> engine.py中。

初始设置

class CtaEngine(BaseEngine):
    """"""
    engine_type: EngineType = EngineType.LIVE  # live trading engine

    setting_filename: str = "cta_strategy_setting.json"
    data_filename: str = "cta_strategy_data.json"

    def __init__(self, main_engine: MainEngine, event_engine: EventEngine) -> None:
        """"""
        super().__init__(main_engine, event_engine, APP_NAME)

        self.strategy_setting: dict = {}                                # strategy_name: dict
        self.strategy_data: dict = {}                                   # strategy_name: dict

        self.classes: dict = {}                                         # class_name: stategy_class
        self.strategies: dict = {}                                      # strategy_name: strategy

        self.symbol_strategy_map: defaultdict = defaultdict(list)       # vt_symbol: strategy list
        self.orderid_strategy_map: dict = {}                            # vt_orderid: strategy
        self.strategy_orderid_map: defaultdict = defaultdict(set)       # strategy_name: orderid set

        self.stop_order_count: int = 0                                  # for generating stop_orderid
        self.stop_orders: Dict[str, StopOrder] = {}                     # stop_orderid: stop_order

        self.init_executor: ThreadPoolExecutor = ThreadPoolExecutor(max_workers=1)

        self.vt_tradeids: set = set()                                   # for filtering duplicate trade

        self.database: BaseDatabase = get_database()
        self.datafeed: BaseDatafeed = get_datafeed()
  • symbol_strategy_map:每一个标的可能有多个策略追踪,故建立一个标的与其对应的一系列策略的映射
  • orderid_strategy_mapstrategy_orderid_map:在订单编号与策略名称之间建立双向映射

处理实时行情

def call_strategy_func(
    self, strategy: CtaTemplate, func: Callable, params: Any = None
) -> None:
    """
    Call function of a strategy and catch any exception raised.
    """
    try:
        if params:
            func(params)
        else:
            func()
    except Exception:
        strategy.trading = False
        strategy.inited = False

        msg: str = f"触发异常已停止\n{traceback.format_exc()}"
        self.write_log(msg, strategy)

call_strategy_func提供了调用策略回调函数的同一接口,只需将需要调用的回调函数传入func参数,而params是传入回调函数的对象。

def register_event(self) -> None:
    """"""
    self.event_engine.register(EVENT_TICK, self.process_tick_event)
    self.event_engine.register(EVENT_ORDER, self.process_order_event)
    self.event_engine.register(EVENT_TRADE, self.process_trade_event)

此处将交易过程产生的事件及其对应的处理函数注册到事件引擎中。

  • EVENT_TICK:实时Tick行情

    • 每当实时行情接口受到一笔Tick数据,就会产生一个EVENT_TICK并加到事件引擎的事件队列中去
    • 事件引擎调用对应函数处理:
    def process_tick_event(self, event: Event) -> None:
        """"""
        tick: TickData = event.data
    
        strategies: list = self.symbol_strategy_map[tick.vt_symbol]
        if not strategies:
            return
    
        self.check_stop_order(tick)
    
        for strategy in strategies:
            if strategy.inited:
                self.call_strategy_func(strategy, strategy.on_tick, tick)
    

    其中,call_strategy_func函数调用了策略处理Tick数据的回调函数,最终实现了将Tick数据传到策略。

  • EVENT_ORDER:订单事件

    • 处理订单事件
    def process_order_event(self, event: Event) -> None:
        """"""
        order: OrderData = event.data
    
        strategy: Optional[type] = self.orderid_strategy_map.get(order.vt_orderid, None)
        if not strategy:
            return
    
        # Remove vt_orderid if order is no longer active.
        vt_orderids: set = self.strategy_orderid_map[strategy.strategy_name]
        if order.vt_orderid in vt_orderids and not order.is_active():
            vt_orderids.remove(order.vt_orderid)
    
        # For server stop order, call strategy on_stop_order function
        if order.type == OrderType.STOP:
            so: StopOrder = StopOrder(
                vt_symbol=order.vt_symbol,
                direction=order.direction,
                offset=order.offset,
                price=order.price,
                volume=order.volume,
                stop_orderid=order.vt_orderid,
                strategy_name=strategy.strategy_name,
                datetime=order.datetime,
                status=STOP_STATUS_MAP[order.status],
                vt_orderids=[order.vt_orderid],
            )
            self.call_strategy_func(strategy, strategy.on_stop_order, so)
    
        # Call strategy on_order function
        self.call_strategy_func(strategy, strategy.on_order, order)
    
  • EVENT_TRADE:成交事件

    • 处理成交事件
    def process_trade_event(self, event: Event) -> None:
        """"""
        trade: TradeData = event.data
    
        # Filter duplicate trade push
        if trade.vt_tradeid in self.vt_tradeids:
            return
        self.vt_tradeids.add(trade.vt_tradeid)
    
        strategy: Optional[type] = self.orderid_strategy_map.get(trade.vt_orderid, None)
        if not strategy:
            return
    
        # Update strategy pos before calling on_trade method
        if trade.direction == Direction.LONG:
            strategy.pos += trade.volume
        else:
            strategy.pos -= trade.volume
    
        self.call_strategy_func(strategy, strategy.on_trade, trade)
    
        # Sync strategy variables to data file
        self.sync_strategy_data(strategy)
    
        # Update GUI
        self.put_strategy_event(strategy)
    

策略的加载与运行

def add_strategy(
    self, class_name: str, strategy_name: str, vt_symbol: str, setting: dict
) -> None:
    """
    Add a new strategy.
    """
    ......

    strategy: CtaTemplate = strategy_class(self, strategy_name, vt_symbol, setting)
    self.strategies[strategy_name] = strategy

    # Add vt_symbol to strategy map.
    strategies: list = self.symbol_strategy_map[vt_symbol]
    strategies.append(strategy)

    # Update to setting file.
    ......

add_strategy将策略添加到引擎中。

  • 这里主要需要理解CtaEnginestrategies字典,它是一个策略名称到策略实例的映射,这样,通过策略名称就能找到策略
  • add_strategy中,完成了将策略实例化为strategy(注意策略实例的名称strategy_name不能重复)
  • symbol_strategy_map中根据策略所定义的标的,将策略加入其中

CtaEngine的逻辑下,一个策略实例只支持跟踪单个交易所的单个标的。

def init_strategy(self, strategy_name: str) -> Future:
    """
    Init a strategy.
    """
    return self.init_executor.submit(self._init_strategy, strategy_name)

def _init_strategy(self, strategy_name: str) -> None:
    """
    Init strategies in queue.
    """
    strategy: CtaTemplate = self.strategies[strategy_name]

    if strategy.inited:
        self.write_log(_("{}已经完成初始化,禁止重复操作").format(strategy_name))
        return

    self.write_log(_("{}开始执行初始化").format(strategy_name))

    # Call on_init function of strategy
    self.call_strategy_func(strategy, strategy.on_init)

    # Restore strategy data(variables)
    data: Optional[dict] = self.strategy_data.get(strategy_name, None)
    if data:
        for name in strategy.variables:
            value = data.get(name, None)
            if value is not None:
                setattr(strategy, name, value)

    # Subscribe market data
    contract: Optional[ContractData] = self.main_engine.get_contract(strategy.vt_symbol)
    if contract:
        req: SubscribeRequest = SubscribeRequest(
            symbol=contract.symbol, exchange=contract.exchange)
        self.main_engine.subscribe(req, contract.gateway_name)
    else:
        self.write_log(_("行情订阅失败,找不到合约{}").format(strategy.vt_symbol), strategy)

    # Put event to update init completed status.
    strategy.inited = True
    self.put_strategy_event(strategy)
    self.write_log(_("{}初始化完成").format(strategy_name))

策略的初始化:做三件事情。

  • init_executor是一个单独的初始化线程,用于执行初始化
  • 一是,调用策略的初始化函数strategy.on_init
  • 二是,设置策略的相关属性
    • strategy_data通过load_strategy_data函数,从本地json配置文件中获得
    def load_strategy_data(self) -> None:
    """
    Load strategy data from json file.
    """
        self.strategy_data = load_json(self.data_filename)
    
  • 三是,订阅合约数据
  • 最后,推送策略初始化完成的事件

在调用策略的初始化函数strategy.on_init时,回顾on_init的内容:

def on_init(self):
    """
    Callback when strategy is inited.
    """
    self.write_log("策略初始化")
    self.load_bar(10)

需要调用load_bar函数,预先向ArrayManager填充一部分数据用于计算相关指标。
在实盘时,它最终调用的是CtaEngine -> load_bar(在回测时是BacktestingEngine -> load_bar)。

def load_bar(
    self,
    vt_symbol: str,
    days: int,
    interval: Interval,
    callback: Callable[[BarData], None],
    use_database: bool
) -> List[BarData]:
    """"""
    symbol, exchange = extract_vt_symbol(vt_symbol)
    end: datetime = datetime.now(DB_TZ)
    start: datetime = end - timedelta(days)
    bars: List[BarData] = []

    # Pass gateway and datafeed if use_database set to True
    if not use_database:
        # Query bars from gateway if available
        contract: Optional[ContractData] = self.main_engine.get_contract(vt_symbol)

        if contract and contract.history_data:
            req: HistoryRequest = HistoryRequest(
                symbol=symbol,
                exchange=exchange,
                interval=interval,
                start=start,
                end=end
            )
            bars: List[BarData] = self.main_engine.query_history(req, contract.gateway_name)

        # Try to query bars from datafeed, if not found, load from database.
        else:
            bars: List[BarData] = self.query_bar_from_datafeed(symbol, exchange, interval, start, end)

    if not bars:
        bars: List[BarData] = self.database.load_bar_data(
            symbol=symbol,
            exchange=exchange,
            interval=interval,
            start=start,
            end=end,
        )

    return bars

load_bar按照先后顺序尝试获得数据:

  • 首先是网关Gateway
  • 然后是数据接口datafeed(前两步可以省略)
  • 最后是本地数据库database
def load_tick(
    self,
    vt_symbol: str,
    days: int,
    callback: Callable[[TickData], None]
) -> List[TickData]:
    """"""
    symbol, exchange = extract_vt_symbol(vt_symbol)
    end: datetime = datetime.now(DB_TZ)
    start: datetime = end - timedelta(days)

    ticks: List[TickData] = self.database.load_tick_data(
        symbol=symbol,
        exchange=exchange,
        start=start,
        end=end,
    )

    return ticks

策略初始化有时也需要加载Tick数据。

订单管理

还是回到订单发出的源头,回顾CtaTemplate -> send_order函数:

def send_order(
    self,
    direction: Direction,
    offset: Offset,
    price: float,
    volume: float,
    stop: bool = False,
    lock: bool = False,
    net: bool = False
) -> list:
    """
    Send a new order.
    """
    if self.trading:
        vt_orderids: list = self.cta_engine.send_order(
            self, direction, offset, price, volume, stop, lock, net
        )
        return vt_orderids
    else:
        return []

在实盘的情况下,这里调用的是CtaEnginesend_order函数:

def send_order(
    self,
    strategy: CtaTemplate,
    direction: Direction,
    offset: Offset,
    price: float,
    volume: float,
    stop: bool,
    lock: bool,
    net: bool
) -> list:
    """
    """
    contract: Optional[ContractData] = self.main_engine.get_contract(strategy.vt_symbol)
    if not contract:
        self.write_log(_("委托失败,找不到合约:{}").format(strategy.vt_symbol), strategy)
        return ""

    # Round order price and volume to nearest incremental value
    price: float = round_to(price, contract.pricetick)
    volume: float = round_to(volume, contract.min_volume)

    if stop:
        if contract.stop_supported:
            return self.send_server_stop_order(
                strategy, contract, direction, offset, price, volume, lock, net
            )
        else:
            return self.send_local_stop_order(
                strategy, direction, offset, price, volume, lock, net
            )
    else:
        return self.send_limit_order(
            strategy, contract, direction, offset, price, volume, lock, net
        )

订单类型分为两类:停止单和限价单

  • 对于停止单,如果该合约支持报送停止单,则调用send_server_stop_order直接调用;否则将停止单存在本地send_local_stop_order
  • 对于限价单,则直接调用send_limit_order报送

直接报送至网关的函数为send_server_order,而send_server_stop_ordersend_limit_order都使用了这个接口。

def send_server_order(
    self,
    strategy: CtaTemplate,
    contract: ContractData,
    direction: Direction,
    offset: Offset,
    price: float,
    volume: float,
    type: OrderType,
    lock: bool,
    net: bool
) -> list:
    """
    Send a new order to server.
    """
    # Create request and send order.
    original_req: OrderRequest = OrderRequest(
        symbol=contract.symbol,
        exchange=contract.exchange,
        direction=direction,
        offset=offset,
        type=type,
        price=price,
        volume=volume,
        reference=f"{APP_NAME}_{strategy.strategy_name}"
    )

    # Convert with offset converter
    req_list: List[OrderRequest] = self.main_engine.convert_order_request(
        original_req,
        contract.gateway_name,
        lock,
        net
    )

    # Send Orders
    vt_orderids: list = []

    for req in req_list:
        vt_orderid: str = self.main_engine.send_order(req, contract.gateway_name)

        # Check if sending order successful
        if not vt_orderid:
            continue

        vt_orderids.append(vt_orderid)

        self.main_engine.update_order_request(req, vt_orderid, contract.gateway_name)

        # Save relationship between orderid and strategy.
        self.orderid_strategy_map[vt_orderid] = strategy
        self.strategy_orderid_map[strategy.strategy_name].add(vt_orderid)

    return vt_orderids

此处将订单信息打包成请求,比回测中稍复杂些。OrderRequest是发送至网关的请求信息,是与网关通信的方式。

def send_limit_order(
    self,
    strategy: CtaTemplate,
    contract: ContractData,
    direction: Direction,
    offset: Offset,
    price: float,
    volume: float,
    lock: bool,
    net: bool
) -> list:
    """
    Send a limit order to server.
    """
    return self.send_server_order(
        strategy,
        contract,
        direction,
        offset,
        price,
        volume,
        OrderType.LIMIT,
        lock,
        net
    )

def send_server_stop_order(
    self,
    strategy: CtaTemplate,
    contract: ContractData,
    direction: Direction,
    offset: Offset,
    price: float,
    volume: float,
    lock: bool,
    net: bool
) -> list:
    """
    Send a stop order to server.

    Should only be used if stop order supported
    on the trading server.
    """
    return self.send_server_order(
        strategy,
        contract,
        direction,
        offset,
        price,
        volume,
        OrderType.STOP,
        lock,
        net
    )

若合约不支持停止单,则将停止单存在本地,若停止单被触发,则转为限价单报送。

def send_local_stop_order(
    self,
    strategy: CtaTemplate,
    direction: Direction,
    offset: Offset,
    price: float,
    volume: float,
    lock: bool,
    net: bool
) -> list:
    """
    Create a new local stop order.
    """
    self.stop_order_count += 1
    stop_orderid: str = f"{STOPORDER_PREFIX}.{self.stop_order_count}"

    stop_order: StopOrder = StopOrder(
        vt_symbol=strategy.vt_symbol,
        direction=direction,
        offset=offset,
        price=price,
        volume=volume,
        stop_orderid=stop_orderid,
        strategy_name=strategy.strategy_name,
        datetime=datetime.now(DB_TZ),
        lock=lock,
        net=net
    )

    self.stop_orders[stop_orderid] = stop_order

    vt_orderids: set = self.strategy_orderid_map[strategy.strategy_name]
    vt_orderids.add(stop_orderid)

    self.call_strategy_func(strategy, strategy.on_stop_order, stop_order)
    self.put_stop_order_event(stop_order)

    return [stop_orderid]

以下是检测停止单是否被触发的函数。回顾上面的process_tick_event函数,当中调用了check_stop_order,也就是说,每接收到一笔Tick数据,就会去检查停止单是否被触发。一旦被触发,则转为限价单报送。

def check_stop_order(self, tick: TickData) -> None:
    """"""
    for stop_order in list(self.stop_orders.values()):
        if stop_order.vt_symbol != tick.vt_symbol:
            continue

        long_triggered = (
            stop_order.direction == Direction.LONG and tick.last_price >= stop_order.price
        )
        short_triggered = (
            stop_order.direction == Direction.SHORT and tick.last_price <= stop_order.price
        )

        if long_triggered or short_triggered:
            strategy: CtaTemplate = self.strategies[stop_order.strategy_name]

            # To get excuted immediately after stop order is
            # triggered, use limit price if available, otherwise
            # use ask_price_5 or bid_price_5
            if stop_order.direction == Direction.LONG:
                if tick.limit_up:
                    price = tick.limit_up
                else:
                    price = tick.ask_price_5
            else:
                if tick.limit_down:
                    price = tick.limit_down
                else:
                    price = tick.bid_price_5

            contract: Optional[ContractData] = self.main_engine.get_contract(stop_order.vt_symbol)

            vt_orderids: list = self.send_limit_order(
                strategy,
                contract,
                stop_order.direction,
                stop_order.offset,
                price,
                stop_order.volume,
                stop_order.lock,
                stop_order.net
            )

            # Update stop order status if placed successfully
            if vt_orderids:
                # Remove from relation map.
                self.stop_orders.pop(stop_order.stop_orderid)

                strategy_vt_orderids: set = self.strategy_orderid_map[strategy.strategy_name]
                if stop_order.stop_orderid in strategy_vt_orderids:
                    strategy_vt_orderids.remove(stop_order.stop_orderid)

                # Change stop order status to cancelled and update to strategy.
                stop_order.status = StopOrderStatus.TRIGGERED
                stop_order.vt_orderids = vt_orderids

                self.call_strategy_func(
                    strategy, strategy.on_stop_order, stop_order
                )
                self.put_stop_order_event(stop_order)

相应地,有一系列撤销订单函数。

posted @ 2025-02-15 15:53  superzzh  阅读(10)  评论(0编辑  收藏  举报