Skip to main content

Event-driven Python backtesting engine - Events - Part 4

·782 words·4 mins
Anthony Ori
Author
Anthony Ori
~ tinkerer ~

In the previous article we covered the data layer, and in this one we will address the different type of events within the engine.

When following an event-driven architecture (EDA), the developer needs to make a decision on what events to capture and which to ignore. This is domain-specific, so there’s no hard rule about what events to consider significant. In the context of a backtesting engine, the universe of possible events should be narrowed down to those relevant to any of the core parts of the engine:

  • data handler (data.py)
  • strategy (strategy.py)
  • portfolio (portfolio.py)
  • broker (broker.py)

Thus, the events we will be focusing on are:

  • MarketEvent – to simulate data arrival from the market feed
  • SignalEvent – to generate a signal in line with the strategy logic
  • OrderEvent   – to generate an order
  • FillEvent     – to fill an order

We’ll dive deeper into each in that order.

# trading_engine/events.py

class Event:
    """
    Event is the base class providing and interface for all subsequent inherited events.
    """
    pass


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

    def __init__(self):
        """
        Initialises the Market event.

        :param trade: market event type, either: 'LONG'; 'SHORT'; or 'EXIT' -> str
        """
        self.type = "MARKET"

Not much happening here, so we can carry on to the SignalEvent

# trading_engine/events.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 Signal event.

        :param symbol: the ticker symbol, i.e. 'IBM' etc.
        :param datetime: the timestamp at which the signal was generated
        :param signal_type: "LONG" or "SHORT"
        """

        self.type = "SIGNAL"
        self.symbol = symbol
        self.datetime = datetime
        self.signal_type = signal_type

This is also fairly straightforward, pass in a bunch of params and assign them to instance variables. It will be clearer what those do when we get to the strategy component.

Moving on to the OrderEvent,

# trading_engine/events.py

# [...]

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

    def __init__(self, symbol, order_type, quantity, direction):
        """
        Initialises the order type (MKT or LMT), quantity, and direction 'BUY' or 'SELL').

        :param symbol: symbol of the instrument to trade
        :param order_type: market 'MKT' or limit 'LMT'
        :param quantity: non-negative integer for quantity
        :param direction: 'BUY' or 'SELL'
        """

        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 (for monitoring purposes).
        """
        print(f"Order: Symbol={self.symbol}, Type={self.order_type}, "
              f"Quantity={self.quantity}, Direction={self.direction}")

The print_order() method simply prints the order details to the stdout in the console. I find it helpful when replaying a strategy to see what happens in the terminal.

Finally, the FillEvent,

# trading_engine/events.py

# [...]

class FillEvent(Event):
    """
    Encapsulates the notion of a filled Order, as returned from a brokerage. Stores the
    actual quantity filled (which can differ from the ordered amount) and at what price.
    It also keeps information of the commission cost.
    """

    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 commission calculation will default to Interactive
        Brokers fees.

        :param timeindex: the bar-resolution when the order was filled
        :param symbol: the instrument that was filled
        :param exchange: the exchange where the order was filled
        :param quantity: filled quantity
        :param direction: direction of fill ('BUY' or 'SELL')
        :param fill_cost: the holdings value in US dollars
        :param commission: an optional commission if given
        """

        self.type = 'FILL'
        self.timeindex = timeindex
        self.symbol = symbol
        self.exchange = exchange
        # for now filled and ordered quantity are the same, but in production they might diverge
        self.filled_quantity = quantity
        self.ordered_quantity = quantity
        self.direction = direction
        self.fill_cost = fill_cost

        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 Interactive Brokers fee structure, in USD.

        This does not include exchange or ECN fees.

        Reference fees:
                https://www.interactivebrokers.com/en/index.php?f=commission&p=stocks2
        """

        full_cost = 1.3
        if self.filled_quantity <= 500:
            full_cost = max(full_cost, 0.013 * self.filled_quantity)
        else:
            full_cost = max(full_cost, 0.008 * self.filled_quantity)

        if self.fill_cost is not None:
            full_cost = min(full_cost, 0.5 / 100.0 * self.filled_quantity * self.fill_cost)

        return full_cost

    def print_order(self):
        """
        Outputs the filled order stats -- for bookkeeping purposes.
        """
        print(f"Filled {self.filled_quantity}/{self.ordered_quantity} units of {self.symbol} {self.direction} order")

As with the OrderEvent I’ve added a print_order() to see details of the order that was filled. That’s it for the events. In the next article we will go over the Strategy component.


Feel free to share the article on socials: