📜 ⬆️ ⬇️

Event-oriented Python backtesting step by step. Part 5 (and last)



In previous articles, we talked about what an event-oriented backtesting system was, dismantled the class hierarchy necessary for its functioning, discussed how such systems use market data , and also track positions and generate purchase orders. In addition, we described the process of evaluating the performance of tested strategies.

In today's article we will look at the process of creating a broker system API handler for the transition to real trading.
')
Note : As an example, the author uses the API of a foreign company Interactive Brokers, hence the names of the modules under discussion (IBExecutionHandler, etc.). ITinvest has its own SmartCOM API , which can be used to create systems like the one described.

The idea is to create an IBexecutionHandler class that will receive OrderEvent instances from the event queue, and then send them to the stock exchange via the brokerage system API using a special Python library to work with it IBPy . This class will also process the “Server Response” messages sent via the API. Then the appropriate instances of FillEvent will be created, which fall into the event queue.

This class in the implementation process can become quite difficult if you seriously take up the optimization of the system and develop a more complex error handling system. However, in our case for educational purposes, the implementation is kept relatively simple.

Python implementation


As usual, at the very beginning you need to create a Python file and import all the necessary libraries. The file will be called ib_execution.py and “lives” in the same directory as the rest of the event-oriented back tester files.

Import the libraries needed to handle the date and time, IbPy objects and objects that are processed by IBExecutionHandler

# ib_execution.py import datetime import time from ib.ext.Contract import Contract from ib.ext.Order import Order from ib.opt import ibConnection, message from event import FillEvent, OrderEvent from execution import ExecutionHandler 

Then you need to define the class IBExecutionHandler . The __init__ constructor requires knowledge of the event queue. In addition, the order_routing specification is order_routing (the default is “SMART”). If it is necessary to describe any requirements related to a specific exchange, this can also be done here. The default currency is US dollars.

Inside the method, create a dictionary fill_dict , which will later be used to generate instances of FillEvent . We will also create the tws_conn object, which will store information for connecting to the broker API. In addition, you need to create an initial order_id , which will be used to track subsequent id orders in order to avoid duplication. Finally, we register message handlers (we will define them below):

 # ib_execution.py class IBExecutionHandler(ExecutionHandler): """      API         . """ def __init__(self, events, order_routing="SMART", currency="USD"): """   IBExecutionHandler. """ self.events = events self.order_routing = order_routing self.currency = currency self.fill_dict = {} self.tws_conn = self.create_tws_connection() self.order_id = self.create_initial_order_id() self.register_handlers() 

The broker API from the example uses an event notification system that allows our class to respond to specific messages in a certain way - this is similar to the work of the event-oriented back tester itself. For brevity, we do not include error handling code, except for output to the thermal via the error_method method.

The _reply_handler method is used to determine if you need to create an instance of FillEvent . The method asks if the message “openOrder” was received, and checks if the orderId for this is a corresponding mark in fill_dict . If not, it is created.

If an “orderStatus” message is found that says that a particular order has been executed, then create_fill is create_fill to create a FillEvent . In addition, for debugging and logging purposes, this message is displayed on the screen:

 # ib_execution.py def _error_handler(self, msg): """   «»   . """ #       print "Server Error: %s" % msg def _reply_handler(self, msg): """      """ #      orderId if msg.typeName == "openOrder" and \ msg.orderId == self.order_id and \ not self.fill_dict.has_key(msg.orderId): self.create_fill_dict_entry(msg) #    if msg.typeName == "orderStatus" and \ msg.status == "Filled" and \ self.fill_dict[msg.orderId]["filled"] == False: self.create_fill(msg) print "Server Response: %s, %s\n" % (msg.typeName, msg) 

Then the create_tws_connection method is created - it is needed to connect to the broker API using the ibConnection object. By default, it uses port 7496 and clientId equal to 10. After creating the object, the connect method is called to connect directly:

 # ib_execution.py def create_tws_connection(self): """       7496  clientId 10.  clientId     -  Id         ,   - . """ tws_conn = ibConnection() tws_conn.connect() return tws_conn 

To track individual orders, use the create _initial_order_id method. In our example, this id equals 1, but in a more elaborate system it would be possible to request the last available ID via the brokerage system API and use it.

 # ib_execution.py def create_initial_order_id(self): """   order ID,     . """ #      #,       1. return 1 

The following register_handlers method simply registers errors and methods for processing server responses:

 # ib_execution.py def register_handlers(self): """       . "" self.tws_conn.register(self._error_handler, 'Error') self.tws_conn.registerAll(self._reply_handler) 

Next, you need to create a Contract instance and associate it with an Order instance that will be sent to the broker's API. The create_contract method generates the first component of this pair. He needs a ticker symbol, type of financial instrument (stock, futures, etc.), exchange and currency. It returns an instance of Contract:

 # ib_execution.py def create_contract(self, symbol, sec_type, exch, prim_exch, curr): """   Contract,  ,   ,       . symbol -    sec_type -    ('STK'  ) exch - ,      prim_exch -  ,      curr -   """ contract = Contract() contract.m_symbol = symbol contract.m_secType = sec_type contract.m_exchange = exch contract.m_primaryExch = prim_exch contract.m_currency = curr return contract 

The following create_order method create_order responsible for creating the second element of the pair — the Order instance. He needs the type of order (mart or limit), the number of shares for the transaction and the action (purchase or sale). It returns an instance of Order:

 # ib_execution.py def create_order(self, order_type, quantity, action): """   Order ( Market/Limit)    long/short. order_type - 'MKT', 'LMT'   Market  Limit quantity –  ,      action - 'BUY'  'SELL' """ order = Order() order.m_orderType = order_type order.m_totalQuantity = quantity order.m_action = action return order 

To avoid duplicating instances of FillEvent for specific order IDs, we use the fill_dict dictionary, which stores the keys for specific order identifiers. When an order execution message is generated, the key value filled for a specific ID is set to True. If the subsequent ServerResponse message from the brokerage system indicates that the order has been executed (and this duplicate message), then a new fill event is not created.

 # ib_execution.py def create_fill_dict_entry(self, msg): """     Fill Dictionary,   orderID.     -     . """ self.fill_dict[msg.orderId] = { "symbol": msg.contract.m_symbol, "exchange": msg.contract.m_exchange, "direction": msg.order.m_action, "filled": False } 

Another create_fill method creates create_fill events and places them in a queue:

 # ib_execution.py def create_fill(self, msg): """  FillEvent,         """ fd = self.fill_dict[msg.orderId] #     symbol = fd["symbol"] exchange = fd["exchange"] filled = msg.filled direction = fd["direction"] fill_cost = msg.avgFillPrice #   FillEvent fill = FillEvent( datetime.datetime.utcnow(), symbol, exchange, filled, direction, fill_cost ) # ,  -       self.fill_dict[msg.orderId]["filled"] = True #   fill   self.events.put(fill_event) 

After the implementation of all the methods described above, it remains only to override the execute_order method from the abstract base class ExecutionHandler . In particular, this method is responsible for placing orders using the brokerage system API.

First of all, you need to check that the event received by the method is really an OrderEvent , and then prepare Contract and Order objects for it with the appropriate parameters. After they are created, the placeOrder method from IbPy is called for the corresponding order_id .

In addition, it is extremely important to call the time.sleep (1) method to make sure that the order has actually passed to the brokerage system. Removing this parameter may result in inconsistent interaction with the API.

And, finally, you should increment the order ID value so as not to duplicate the orders:

 # ib_execution.py def execute_order(self, event): """           API.         fill,    . : event –   Event    . """ if event.type == 'ORDER': #     asset = event.symbol asset_type = "STK" order_type = event.order_type quantity = event.quantity direction = event.direction #          Order ib_contract = self.create_contract( asset, asset_type, self.order_routing, self.order_routing, self.currency ) #         Order ib_order = self.create_order( order_type, quantity, direction ) #      self.tws_conn.placeOrder( self.order_id, ib_contract, ib_order ) # :     #     ,   ! time.sleep(1) #   ID     self.order_id += 1 

This class forms the interaction with the broker system and can be used to work in simulator mode, which is suitable only for testing on historical data. In order to use the system in a real trading situation, you need to create a real data flow processor from the exchange to replace the historical data flow. We will talk about this in future articles.

As you can see, during the development of the back tester and modules for real trading, we wherever possible resorted to reuse the code - this will reduce the number of errors to a minimum and make sure that the behavior of different parts of the system will be similar, if not identical, as for trading both online and in backtesting.

That's all for today, thank you for your attention! We will be happy to answer your questions and comments. Do not forget to subscribe to our blog !

All materials cycle:

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


All Articles