Backtest flow

This document describes the basic flow of a Backtest, along with all details related to various Events. It will help you understand how exactly does the event-driven architecture of the Backtester work and what happens when you start a backtest of your strategy.

Overview

Events comprise a crucial part of the Backtester architecture. This section covers the overall specification of TimeEvents, EventNotifiers and EventListeners and contains a few examples of customly defined Events.

General types of events

The event-driven architecture of the Backtester, defines and utilizes the following types of events:

  • Event - super-type for all events,

  • EmptyQueueEvent - occurs whenever there are no events to dispatch in the EventManager,

  • EndTradingEvent- occurs when the trading should stop (e.g. when the backtest should be terminated). For now it is generated by DataHandler in the backtest if there is no more data available for the backtest. However, in the future, this could be triggered by the user (from GUI or console),

  • TimeEvent - various events which have the notion of time. Examples of events, which are based on the TimeEvent, would be RegularTimeEvent and PeriodicEvent, described in more details in the subsequent section.

Notifiers

Each of the events is correlated with a certain type of notifier. The notifiers should be registered along with their corresponding events types (defined by EventNotifier.events_type()) in the EventManager using the EventManager.register_notifiers() method. After being registered notifier can have listeners added to it. Hence, whenever an event of type associated with the EventNotifier occurs, EventManager will tell the notifier to notify all its listeners. Currently the following notifiers, grouped together in the Notifiers class, are defined in the Backtester:

  • AllEventNotifier - corresponds to the most general type of events (the Event).

  • EmptyQueueEventNotifier - corresponds to the EmptyQueueEvents.

  • EndTradingEventNotifier - corresponds to the EndTradingEvents.

  • Scheduler - corresponds to various types of TimeEvents. Scheduler is responsible for generating the time events, whenever the EventManager queue is empty. Then the TimeFlowController triggers the Scheduler to generate the events.

Listeners

In order to subscribe a listener to a certain type of Event, the subscribe function should be used. For example, in order to subscribe a listener to a certain type of TimeEvent, the following function should be used:

Scheduler.subscribe(TypeOfEvent, listener)

Listener will be only notified of events of this concrete type (e.g. MarketOpenEvent and not all TimeEvents). Listener needs to have a proper callback method, defined in TimeEvent.notify() method.

class CustomTimeEvent (TimeEvent):
    def notify(self, listener) -> None:
        listener.on_custom_event()

class CustomTimeEventListener:
    def on_custom_event(self):
        ...

Specific Time Events

In order to facilitate the operation of Backtester, the following events, based on TimeEvents, where defined:

RegularTimeEvent

These are events which occur on a regular basis (e.g. each day at 17:00, each first Wednesday of a month, etc.). An example of a RegularTimeEvent may be the MarketOpenEvent, occurring at every market open.

To facilitate the definition of RegularTimeEvent the RegularDateTimeRule helper class may be used. It allows to easily compute the next trigger time. It accepts a “time dictionary”, consisting of a subset of following fields: year, month, weekday, day, hour, minute, second, microsecond. In order to schedule the event for 8:15 a.m. every day, all the fields: hour, minute, second, microsecond should be filled. The RegularTimeEvent needs to implement trigger_time, next_trigger_time and notify functions (see example below).

class CustomRegularTimeEvent(RegularTimeEvent):
    """
    Rule which is triggered every Monday at 8:15 a.m.
    The listeners for this event should implement the
    on_monday_morning() method.
    """

    _trigger_time_dictionary = {
        "weekday": 0, "hour": 8, "minute": 15,
        "second": 0, "microsecond": 0
    }
    _time_rule = RegularDateTimeRule(**trigger_time_dict)

    @classmethod
    def trigger_time(cls) -> RelativeDelta:
        return RelativeDelta(**cls.trigger_time_dict)

    def next_trigger_time(self, now: datetime) -> datetime:
        next_trigger_time = self._time_rule.next_trigger_time(now)
        return next_trigger_time

    def notify(self, listener) -> None:
        listener.on_monday_morning(self)

Examples of predefined RegularTimEvents: - MarketOpenEvent, - MarketCloseEvent

PeriodicEvent

These are events which occur on a regular basis, similarly to RegularTimeEvents. The only diffrence is that, they may occur with a predefined frequency (e.g. daily frequency, 1 minute frequency etc.) within a given time range. For example

The PeriodicEvent is triggered only within the [start_time, end_time] time range with the given frequency. It is triggered always at the start_time, but not necessarily at the end_time. For example:

start_time = {
    "hour": 13, "minute": 20,
    "second": 0, "microsecond": 0
}
end_time = {
    "hour": 16, "minute": 0,
    "second": 0, "microsecond": 0
}
frequency = Frequency.MIN_30

This event will be triggered at 13:20, 13:50, 14:20, 14:50, 15:20, 15:50, but not at 16:00.

The next_trigger_time() in case of PeriodicEvent skips automatically Saturdays and Sundays.

IntradayBarEvent

IntradayBarEvent is a special type of PeriodicEvent, used by SimulatedExecutionHandler to support intraday trading. The frequency is hardcoded to be equal to 1 minute and the time range is equal to the range between market open and market close times. They call on_new_bar function on the listener at every trigger time between start_time and end_time, which denote the time range between market open (exclusive) and market close (exclusive).

SingleTimeEvent

SingleTimeEvent represents type of all these events that associated with one specific date time (e.g. 2017-05-13 13:00) and which will never be repeated in the future.

This type of events is mostly used along with data being passed between different components. In order to schedule new single time event, SingleTimeEvent.schedule_new_event(datetime, data) function should be used. Then, whenever this event occurs, it is possible to access this data using get_data function.

ScheduleOrderExecutionEvent

ScheduleOrderExecutionEvent is a special type of SingleTimeEvent. It is used in the Backtester to schedule the execution of Orders in the future.

Similarly to SingleTimeEvent, it also schedules new event by adding the (datetime, data) pair to the _datetimes_to_data dictionary, but it also assumes data has the structure of a dictionary, which maps orders executor instance to orders, which need to be executed. Multiple events can be scheduled for the same time - the orders and order executors will be appended to existing data.

Backtester Events Flow

The following graphics depicts the typical flow of events in the Backtester. It presents the content of Events Queue in the EventManager and order of executed events in a certain point of time. At the beginning of each Backtest execution the Events Queue is empty. The following example illustrates an intraday events flow, considering a point of time after the market open and before market close, when already a ScheduleOrderExecutionEvent was created.

../_images/qflib_events_flow.png
  1. The Events Queue is currently empty. When the EventManager.dispatch_next_event() function is called, the EmptyQueueEvent is returned.

  2. The EmptyQueueEvent notifier notifies its listener - TimeFlowController, which triggers Scheduler to generate new TimeEvents. All the generated TimeEvents are appended to the Events Queue in the following order:

    • ScheduleOrderExecutionEvent have the highest priority and thus, they appear first in the queue,

    • IntradayBarEvent - these events have the same priority as MarketOpenEvent and MarketCloseEvent,

    • all other defined TimeEvents.

  3. EventManager.dispatch_next_event() returns the ScheduleOrderExecutionEvent.

  4. The notifier ofScheduleOrderExecutionEvent (Scheduler) notifies its listener - SimulatedExecutionHandler. At this point, all scheduled Orders are being accepted by corresponding SimulatedExecutor (e.g. market orders are appended to the list of awaiting orders of MarketOrderExecutor).

    Afterwards, the EventManager.dispatch_next_event() function is called and IntradayBarEvent is returned.

  5. The notifier ofIntradayBarEvent (Scheduler) notifies its listener - SimulatedExecutionHandler. At this point of time, all of the orders, that are stored in the lists of awaiting orders in each of the SimulatedExecutors, are being processed and if they fulfill the necessary conditions (e.g. a price for the specified asset is currently available), they are being executed. In the backterster, the execution of a single order is simulated by converting the Order into Transaction.

    When the processing and execution of the orders is finished, all positions that are currently open in the Portfolio are updated, by getting their most recent prices (Portfolio.update()). In case of intraday trading, the Portfolio is updated additionally at the time of market open on MarketOpenEvent, and after the market close on AfterMarketCloseEvent.

    Afterwards, the EventManager.dispatch_next_event() function is called and a custom event is returned.

  6. The notifier of the custom event notifies its listeners, which then perform the necessary actions. Then, the EventManager.dispatch_next_event() function is called and the next custom event is returned.

  7. The notifier of the next custom event notifies its listeners and the Events Queue becomes empty again.

Daily variant

The above described example presents the intraday variant. In case of daily trading, the SimulatedExecutionHandler does not subscribe to IntradayBarEvent. It is only subscribed to MarketOpenEvent, MarketCloseEvent, AfterMarketCloseEvent and the ScheduleOrderExecutionEvent. The Portfolio in this case is updated only on the AfterMarketCloseEvent.