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 theEventManager
,EndTradingEvent
- occurs when the trading should stop (e.g. when the backtest should be terminated). For now it is generated byDataHandler
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 theTimeEvent
, would beRegularTimeEvent
andPeriodicEvent
, 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 (theEvent
).EmptyQueueEventNotifier
- corresponds to theEmptyQueueEvents
.EndTradingEventNotifier
- corresponds to theEndTradingEvents
.Scheduler
- corresponds to various types ofTimeEvents
. Scheduler is responsible for generating the time events, whenever theEventManager
queue is empty. Then theTimeFlowController
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.
The Events Queue is currently empty. When the
EventManager.dispatch_next_event()
function is called, theEmptyQueueEvent
is returned.The
EmptyQueueEvent
notifier notifies its listener -TimeFlowController
, which triggersScheduler
to generate newTimeEvents
. All the generatedTimeEvents
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 asMarketOpenEvent
andMarketCloseEvent
,all other defined
TimeEvents
.
EventManager.dispatch_next_event()
returns theScheduleOrderExecutionEvent
.The notifier of
ScheduleOrderExecutionEvent
(Scheduler
) notifies its listener -SimulatedExecutionHandler
. At this point, all scheduled Orders are being accepted by correspondingSimulatedExecutor
(e.g. market orders are appended to the list of awaiting orders ofMarketOrderExecutor
).Afterwards, the
EventManager.dispatch_next_event()
function is called andIntradayBarEvent
is returned.The notifier of
IntradayBarEvent
(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 theSimulatedExecutors
, 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 onMarketOpenEvent
, and after the market close onAfterMarketCloseEvent
.Afterwards, the
EventManager.dispatch_next_event()
function is called and a custom event is returned.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.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
.