使用 Python 进行事件驱动的回测

使用 Python 进行事件驱动的回测 -第一部分

我们描述了事件驱动回测器的概念。本系列文章的其余部分将集中讨论构成整个系统的每个单独的类层次结构。在本文中,我们将考虑事件以及如何使用它们在对象之间传递信息。

如前文所述,交易系统使用两个 while 循环 - 一个外部循环和一个内部循环。内部 while 循环处理从内存队列捕获事件,然后将其路由到适当的组件以执行后续操作。在此基础结构中,有四种类型的事件:

  • MarketEvent - 当外部 while 循环开始新的“心跳”时触发。当对象DataHandler收到当前正在跟踪的任何符号的市场数据的新更新时,就会发生这种情况。它用于触发Strategy对象生成新的交易信号。事件对象仅包含一个标识,表明它是一个市场事件,没有其他结构。
  • SignalEvent -Strategy对象利用市场数据创建新SignalEvent信号。信号SignalEvent包含股票代码、生成时间的时间戳和方向(多头或空头)。信号SignalEvent被对象用作Portfolio交易建议*。*
  • OrderEvent - 当Portfolio对象收到时SignalEvent,它会在投资组合的更广泛背景下评估它们,包括风险和头寸规模。这最终会导致OrderEvent将 发送给ExecutionHandler
  • FillEvent - 当ExecutionHandler收到时,OrderEvent它必须处理订单。订单处理完成后,它会生成一个FillEvent,它描述了购买或销售的成本以及交易成本,例如费用或滑点。

父类称为Event。它是一个基类,不提供任何功能或特定接口。在以后的实现中,事件对象可能会变得更加复杂,因此我们通过创建类层次结构来确保此类系统的设计面向未来。

# event.py

class Event(object):
    """
    Event is base class providing an interface for all subsequent 
    (inherited) events, that will trigger further events in the 
    trading infrastructure.   
    """
    pass

MarketEvent继承Event并提供了自我认同,即它是一个“市场”类型的事件。

# event.py

class MarketEvent(Event):
    """
    Handles the event of receiving a new market update with 
    corresponding bars.
    """

    def __init__(self):
        """
        Initialises the MarketEvent.
        """
        self.type = 'MARKET'

ASignalEvent需要一个股票代码、一个生成时间戳和一个方向来为Portfolio对象提供建议。

# event.py

class SignalEvent(Event):
    """
    Handles the event of sending a Signal from a Strategy object.
    This is received by a Portfolio object and acted upon.
    """
    
    def __init__(self, symbol, datetime, signal_type):
        """
        Initialises the SignalEvent.

        Parameters:
        symbol - The ticker symbol, e.g. 'GOOG'.
        datetime - The timestamp at which the signal was generated.
        signal_type - 'LONG' or 'SHORT'.
        """
        
        self.type = 'SIGNAL'
        self.symbol = symbol
        self.datetime = datetime
        self.signal_type = signal_type

OrderEvent稍微复杂一些,SignalEvent因为除了 的上述属性之外,它还包含一个数量字段SignalEvent。数量由Portfolio约束决定。此外, 还有OrderEvent一个print_order()方法,用于在必要时将信息输出到控制台。

# event.py

class OrderEvent(Event):
    """
    Handles the event of sending an Order to an execution system.
    The order contains a symbol (e.g. GOOG), a type (market or limit),
    quantity and a direction.
    """

    def __init__(self, symbol, order_type, quantity, direction):
        """
        Initialises the order type, setting whether it is
        a Market order ('MKT') or Limit order ('LMT'), has
        a quantity (integral) and its direction ('BUY' or
        'SELL').

        Parameters:
        symbol - The instrument to trade.
        order_type - 'MKT' or 'LMT' for Market or Limit.
        quantity - Non-negative integer for quantity.
        direction - 'BUY' or 'SELL' for long or short.
        """
        
        self.type = 'ORDER'
        self.symbol = symbol
        self.order_type = order_type
        self.quantity = quantity
        self.direction = direction

    def print_order(self):
        """
        Outputs the values within the Order.
        """
        print "Order: Symbol=%s, Type=%s, Quantity=%s, Direction=%s" % \
            (self.symbol, self.order_type, self.quantity, self.direction)

是最复杂FillEventEvent。它包含订单执行的时间戳、订单代码和执行的交易所、交易的股票数量、购买的实际价格以及产生的佣金。

佣金是使用Interactive Brokers 佣金计算的。对于美国 API 订单,此佣金为每笔订单最低 1.30 美元,固定费率为每股 0.013 美元或 0.08 美元,具体取决于交易规模是低于还是高于 500 股股票。

# event.py

class FillEvent(Event):
    """
    Encapsulates the notion of a Filled Order, as returned
    from a brokerage. Stores the quantity of an instrument
    actually filled and at what price. In addition, stores
    the commission of the trade from the brokerage.
    """

    def __init__(self, timeindex, symbol, exchange, quantity, 
                 direction, fill_cost, commission=None):
        """
        Initialises the FillEvent object. Sets the symbol, exchange,
        quantity, direction, cost of fill and an optional 
        commission.

        If commission is not provided, the Fill object will
        calculate it based on the trade size and Interactive
        Brokers fees.

        Parameters:
        timeindex - The bar-resolution when the order was filled.
        symbol - The instrument which was filled.
        exchange - The exchange where the order was filled.
        quantity - The filled quantity.
        direction - The direction of fill ('BUY' or 'SELL')
        fill_cost - The holdings value in dollars.
        commission - An optional commission sent from IB.
        """
        
        self.type = 'FILL'
        self.timeindex = timeindex
        self.symbol = symbol
        self.exchange = exchange
        self.quantity = quantity
        self.direction = direction
        self.fill_cost = fill_cost

        # Calculate commission
        if commission is None:
            self.commission = self.calculate_ib_commission()
        else:
            self.commission = commission

    def calculate_ib_commission(self):
        """
        Calculates the fees of trading based on an Interactive
        Brokers fee structure for API, in USD.

        This does not include exchange or ECN fees.

        Based on "US API Directed Orders":
        https://www.interactivebrokers.com/en/index.php?f=commission&p=stocks2
        """
        full_cost = 1.3
        if self.quantity <= 500:
            full_cost = max(1.3, 0.013 * self.quantity)
        else: # Greater than 500
            full_cost = max(1.3, 0.008 * self.quantity)
        full_cost = min(full_cost, 0.5 / 100.0 * self.quantity * self.fill_cost)
        return full_cost

在本系列的下一篇文章中,我们将考虑如何DataHandler通过相同的类界面开发一个允许历史回溯测试和实时交易的市场类层次结构。

过去几个月,我们一直在 QuantStart 上使用 Python 和pandas对各种交易策略进行回测。pandas的矢量化特性确保对大型数据集的某些操作非常快速。然而,我们迄今为止研究的矢量化回测器形式在交易执行模拟方式上存在一些缺陷。在本系列文章中,我们将讨论一种更现实的历史策略模拟方法,即使用 Python 构建事件驱动的回测环境。

事件驱动的软件

在深入研究开发此类回测程序之前,我们需要了解事件驱动系统的概念。视频游戏为事件驱动软件提供了一个自然的用例,并提供了一个简单的示例供您探索。视频游戏有多个组件,它们在高帧速率下实时交互。这是通过在称为事件循环游戏循环的“无限”循环中运行整个计算集来处理的。

在游戏循环的每次更新中,都会调用一个函数来接收最新事件,该事件将由游戏中一些相应的先前操作生成。根据事件的性质(可能包括按键或鼠标单击),将采取一些后续操作,这些操作将终止循环或生成一些其他事件。然后该过程将继续。以下是一些示例伪代码:

while True:  # Run the loop forever
    new_event = get_new_event()   # Get the latest event

    # Based on the event type, perform an action
    if new_event.type == "LEFT_MOUSE_CLICK":
        open_menu()
    elif new_event.type == "ESCAPE_KEY_PRESS":
        quit_game()
    elif new_event.type == "UP_KEY_PRESS":
        move_player_north()
    # ... and many more events

    redraw_screen()   # Update the screen to provide animation
    tick(50)   # Wait 50 milliseconds

代码不断检查新事件,然后根据这些事件执行操作。特别是,它允许实时响应处理的假象,因为代码不断循环并检查事件。很明显,这正是我们进行高频交易模拟所需要的。

为什么要使用事件驱动的回测器?

与矢量化方法相比,事件驱动系统具有许多优势:

  • 代码重用- 事件驱动回测器在设计上既可用于历史回测,也可用于实时交易,只需极少的组件切换。但矢量化回测器则不然,因为矢量化回测器必须同时提供所有数据才能进行统计分析。
  • 前瞻偏差- 使用事件驱动的回测器时,不存在前瞻偏差,因为市场数据接收被视为必须采取行动的“事件”。因此,可以向事件驱动的回测器“滴灌”市场数据,复制订单管理和投资组合系统的行为方式。
  • 真实性- 事件驱动的回测器允许对订单执行方式和交易成本进行大量定制。由于可以构建自定义交易所处理程序,因此处理基本市场和限价订单以及开盘市价 (MOO) 和收盘市价 (MOC) 非常简单。

尽管事件驱动系统具有许多优点,但与更简单的矢量化系统相比,它们有两个主要缺点。首先,它们的实现和测试要复杂得多。有更多“活动部件”,导致引入错误的可能性更大。为了缓解这种情况,可以采用适当的软件测试方法,例如测试驱动开发。

其次,与矢量化系统相比,它们的执行速度较慢。在进行数学计算时,无法利用最佳矢量化操作。我们将在后续文章中讨论如何克服这些限制。

事件驱动回测器概述

要将事件驱动方法应用于回溯测试系统,必须定义处理特定任务的组件(或对象):

  • 事件-Event是事件驱动系统的基本类单元。它包含一个类型(例如“MARKET”、“SIGNAL”、“ORDER”或“FILL”),该类型决定了如何在事件循环中处理它。
  • 事件队列- 事件队列是一个内存中的 Python 队列对象,它存储由其余软件生成的所有事件子类对象。
  • DataHandler -DataHandler是一个抽象基类(ABC),它提供了一个用于处理历史或实时市场数据的接口。这提供了很大的灵活性,因为策略和投资组合模块可以在两种方法之间重复使用。DataHandler 会在系统MarketEvent每次心跳时生成一个新的数据(见下文)。
  • 策略-Strategy也是一个 ABC,它提供了一个接口,用于获取市场数据并生成相应的信号事件,这些信号事件最终由 Portfolio 对象使用。信号事件包含股票代码、方向(LONG 或 SHORT)和时间戳。
  • 投资组合- 这是一个 ABC,用于处理与策略的当前和后续头寸相关的订单管理。它还对整个投资组合进行风险管理,包括行业风险敞口和头寸规模。在更复杂的实现中,可以将此委托给 RiskManagement 类。它Portfolio从队列中获取 SignalEvents 并生成添加到队列的 OrderEvents。
  • ExecutionHandler -ExecutionHandler模拟与经纪公司的连接。处理程序的工作是从队列中获取订单事件并执行它们,可以通过模拟方法或实际连接到实际经纪公司。订单执行后,处理程序会创建 FillEvents,描述实际交易的内容,包括费用、佣金和滑点(如果已建模)。
  • 循环- 所有这些组件都包装在一个事件循环中,该事件循环可以正确处理所有事件类型,并将它们路由到适当的组件。

这是一个相当基本的交易引擎模型。有很大的扩展空间,特别是在如何Portfolio使用方面。此外,不同的交易成本模型也可能被抽象到它们自己的类层次结构中。在这个阶段,它会在本系列文章中引入不必要的复杂性,因此我们目前不会进一步讨论它。在以后的教程中,我们可能会扩展系统以包含更多的现实性。

以下是一段 Python 代码,演示了回测程序的实际工作原理。代码中有两个循环。外层循环用于向回测程序提供心跳**。**对于实时交易,这是轮询新市场数据的频率。对于回测策略,这并不是绝对必要的,因为回测程序使用以滴灌形式提供的市场数据(参见该bars.update_bars()行)。

内部循环实际上处理来自队列对象的事件events。特定事件被委托给相应的组件,随后新事件被添加到队列中。当事件队列为空时,心跳循环继续:

# Declare the components with respective parameters
bars = DataHandler(..)
strategy = Strategy(..)
port = Portfolio(..)
broker = ExecutionHandler(..)

while True:
    # Update the bars (specific backtest code, as opposed to live trading)
    if bars.continue_backtest == True:
        bars.update_bars()
    else:
        break
    
    # Handle the events
    while True:
        try:
            event = events.get(False)
        except Queue.Empty:
            break
        else:
            if event is not None:
                if event.type == 'MARKET':
                    strategy.calculate_signals(event)
                    port.update_timeindex(event)

                elif event.type == 'SIGNAL':
                    port.update_signal(event)

                elif event.type == 'ORDER':
                    broker.execute_order(event)

                elif event.type == 'FILL':
                    port.update_fill(event)

    # 10-Minute heartbeat
    time.sleep(10*60)

这是事件驱动回测器设计的基本轮廓。在下一篇文章中,我们将讨论事件类层次结构。

猜你喜欢

转载自blog.csdn.net/m0_74840398/article/details/143040334