📜 ⬆️ ⬇️

Event-oriented Python backtesting step by step. Part 1



Earlier in our blog on Habré, we considered various stages of developing trading systems (there are online courses on the topic), among which one of the most important is testing for historical data (backtesting). Today we will talk about the practical implementation of an event-oriented backtest module using Python.

Event-oriented software


Before diving into the development of a backtester, you should understand the concept of event-oriented systems. One of the most obvious examples of such programs are computer games. In a video game, there are many components that interact with each other in real time with a high frame rate. To cope with the load helps the implementation of all calculations inside the "infinite" loop, which is also called the event loop or game loop .
')
On each loop tick, a function is called to get the last event that was generated by some action in the game. Depending on the nature of this event (keystroke, mouse click), a follow-up action is taken that either interrupts the loop or creates additional events, and the process continues. It is possible to illustrate all this with such pseudocode:

while True: #    new_event = get_new_event() #    #        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() #       tick(50) #  50  

The code will again and again check for new events and perform actions based on them. In particular, this creates an illusion of response in real time. As will become clear later, this is just what we need to start the simulation of high-frequency trading.

Why event-oriented backtester


Event-oriented systems have several advantages over the vectorized approach:


However, not everything is so cloudless, and an event-oriented system has its flaws. First, they are much more difficult to create and test - more “moving parts”, and therefore more bugs. Therefore, to create them it is recommended to apply the development through testing . Second, they are slower than vectorized systems.

Backster Review


To apply an event-oriented approach, first of all, it is necessary to deal with parts of our system that will be responsible for certain areas of work:


Above, we described the basic model of the trading layout, which can be complicated and expanded in many areas, for example, in the area of ​​operation of the Portfolio module. In addition, you can make different models of transaction costs in a separate class hierarchy. In our case, however, this will only create unnecessary difficulties, so we will only gradually introduce more realism into the system.

Below is a piece of Python code that demonstrates the practical work of the backtester. There are two loops in the code. The outer loop is used to give the back tester a heartbeat. In online trading, this means the frequency with which the market data is requested. For testing strategies on historical data, this is not a required component, since market data is poured into the system in parts - see the bars.update_bars() .

An internal loop is needed to handle events from the Queue object. Specific events are delegated to the respective components in the queue, new events are added sequentially. When the queue is empty heartbeat loop makes a new round:

 #      bars = DataHandler(..) strategy = Strategy(..) port = Portfolio(..) broker = ExecutionHandler(..) while True: #   (  ,    ) if bars.continue_backtest == True: bars.update_bars() else: break #   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  time.sleep(10*60) 

Event Classes


There are four types of events in the described scheme:


The parent class is called Event - this is a base class that does not provide any functionality or special interface. In further implementations, the Event class will most likely become more difficult, so it is worthwhile to foresee such an opportunity in advance by creating a class hierarchy:

 # event.py class Event(object): """ Event —   ,     () ,       . """ pass 

MarketEvent inherits from Event and carries a little more than a simple self-identification like 'MARKET':

 # event.py class MarketEvent(Event): """          . """ def __init__(self): """  MarketEvent. """ self.type = 'MARKET' 

SignalEvent requires a ticker symbol, time stamp and order direction, which the portfolio object can use as a “tip” when trading:

 # event.py class SignalEvent(Event): """    Signal   Strategy.    Portfolio,    . """ def __init__(self, symbol, datetime, signal_type): """  SignalEvent. : symbol -  ,   Google — 'GOOG'. datetime -     . signal_type - 'LONG'  'SHORT'. """ self.type = 'SIGNAL' self.symbol = symbol self.datetime = datetime self.signal_type = signal_type 

OrderEvent more complicated than SignalEvent , and contains an additional field for specifying the number of units of a financial instrument in an order. The number is determined by the limitations of the Portfolio object. In addition, OrderEvent contains the print_order() method, which is used to output information to the console if necessary:

 # event.py class OrderEvent(Event): """     Order   .    (, GOOG),  (market  limit),   . """ def __init__(self, symbol, order_type, quantity, direction): """    ( MKT   LMT),          (BUY  SELL). : symbol - ,     . order_type - 'MKT'  'LMT'   Market  Limit. quantity - -  (integer)     . direction - 'BUY'  'SELL'     . """ self.type = 'ORDER' self.symbol = symbol self.order_type = order_type self.quantity = quantity self.direction = direction def print_order(self): """  ,    Order. """ print "Order: Symbol=%s, Type=%s, Quantity=%s, Direction=%s" % \ (self.symbol, self.order_type, self.quantity, self.direction) 

FillEvent is an Event increased complexity. It contains the timestamp of the execution of the order, the ticker and information about the exchange on which it was executed, the number of units of a financial instrument (stocks, futures, etc.), the actual price of the transaction and related fees.

Related costs are calculated using the brokerage system API ( ITinvest has its own API ). In our example, a US broker system is used, the commission of which is at least $ 1.30 per order with a single rate of $ 0.013 or $ 0.08 per share, depending on whether the number of shares exceeds 500 units or not.

 # event.py class FillEvent(Event): """     (Filled Order),  .    ,   /   .    . """ def __init__(self, timeindex, symbol, exchange, quantity, direction, fill_cost, commission=None): """   FillEvent.  ,  , , ,   () .     ,   Fill            (  API) : timeindex -      . symbol - ,    . exchange - ,     . quantity -     . direction -   ('BUY'  'SELL') fill_cost -  . commission -  ,   . """ 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): """       API  (  , , ..   ).    . """ 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 

That's all, thank you for your attention. In the next part, we will talk about using market information ( DataHandler class) for testing historical data and real trading.

All materials cycle:

Source: https://habr.com/ru/post/263097/


All Articles