Event-Driven Backtesting dengan Python - Bagian VIII

Penulis:Kebaikan, Dibuat: 2019-03-26 16:38:59, Diperbarui:

Sudah beberapa waktu sejak kami mempertimbangkan backtester event-driven, yang kami mulai diskusikan dalam artikel ini. Di Bagian VI saya menjelaskan cara mengkode model ExecutionHandler stand-in yang bekerja untuk situasi backtesting historis. Dalam artikel ini kami akan mengkode pengendali API Interactive Brokers yang sesuai untuk bergerak menuju sistem perdagangan langsung.

Saya sebelumnya telah membahas cara mengunduh Trader Workstation dan membuat akun demo Interactive Brokers serta cara membuat antarmuka dasar ke IB API menggunakan IbPy.

Ide utama dari kelas IBExecutionHandler (lihat di bawah) adalah untuk menerima contoh OrderEvent dari antrian acara dan kemudian mengeksekusi mereka langsung terhadap Interactive Brokers order API menggunakan perpustakaan IbPy. Kelas ini juga akan menangani Server Response pesan yang dikirim kembali melalui API. Pada tahap ini, satu-satunya tindakan yang dilakukan adalah untuk membuat contoh FillEvent yang sesuai yang kemudian akan dikirim kembali ke antrian acara.

Kelas itu sendiri mungkin bisa menjadi agak kompleks, dengan logika optimasi eksekusi serta penanganan kesalahan yang canggih.

Implementasi Python

Seperti biasa, tugas pertama adalah membuat file Python dan mengimpor pustaka yang diperlukan. File ini disebut ib_execution.py dan tinggal di direktori yang sama dengan file event-driven lainnya.

Kami mengimpor library penanganan tanggal/waktu yang diperlukan, objek IbPy dan objek Event tertentu yang ditangani oleh 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

Kita sekarang mendefinisikan kelas IBExecutionHandler.initconstructor pertama-tama membutuhkan pengetahuan tentang antrian acara. Ini juga membutuhkan spesifikasi order_routing, yang saya telah default ke SMART. Jika Anda memiliki persyaratan pertukaran tertentu, Anda dapat menentukan mereka di sini. Mata uang default juga telah ditetapkan menjadi Dolar AS.

Dalam metode ini kita membuat kamus fill_dict, yang dibutuhkan kemudian untuk digunakan dalam menghasilkan contoh FillEvent. Kita juga membuat objek koneksi tws_conn untuk menyimpan informasi koneksi kita ke Interactive Brokers API. Kita juga harus membuat awal default order_id, yang melacak semua perintah berikutnya untuk menghindari duplikat. Akhirnya kita mendaftarkan pengendali pesan (yang akan kita definisikan secara lebih rinci di bawah):

# ib_execution.py

class IBExecutionHandler(ExecutionHandler):
    """
    Handles order execution via the Interactive Brokers
    API, for use against accounts when trading live
    directly.
    """

    def __init__(self, events, 
                 order_routing="SMART", 
                 currency="USD"):
        """
        Initialises the IBExecutionHandler instance.
        """
        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()

IB API menggunakan sistem acara berbasis pesan yang memungkinkan kelas kami untuk merespons dengan cara tertentu ke pesan tertentu, dengan cara yang mirip dengan backtester event-driven itu sendiri.

Metode _reply_handler, di sisi lain, digunakan untuk menentukan apakah sebuah contoh FillEvent perlu dibuat. Metode ini bertanya apakah pesan openOrder telah diterima dan memeriksa apakah entri dalam fill_dict kami untuk orderId tertentu sudah ditetapkan. Jika tidak maka yang dibuat.

Jika melihat pesan orderStatus dan pesan tertentu menyatakan bahwa pesanan telah diisi, maka ia memanggil create_fill untuk membuat FillEvent.

# ib_execution.py
    
    def _error_handler(self, msg):
        """
        Handles the capturing of error messages
        """
        # Currently no error handling.
        print "Server Error: %s" % msg

    def _reply_handler(self, msg):
        """
        Handles of server replies
        """
        # Handle open order orderId processing
        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)
        # Handle Fills
        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)

Metode berikut, create_tws_connection, membuat koneksi ke API IB menggunakan obyek IbPy ibConnection.

# ib_execution.py
    
    def create_tws_connection(self):
        """
        Connect to the Trader Workstation (TWS) running on the
        usual port of 7496, with a clientId of 10.
        The clientId is chosen by us and we will need 
        separate IDs for both the execution connection and
        market data connection, if the latter is used elsewhere.
        """
        tws_conn = ibConnection()
        tws_conn.connect()
        return tws_conn

Untuk melacak pesanan terpisah (untuk tujuan pelacakan mengisi) metode berikut create_initial_order_id digunakan. Saya telah defaultnya ke 1, tetapi pendekatan yang lebih canggih adalah pertanyaan IB untuk ID terbaru yang tersedia dan menggunakannya. Anda selalu dapat mengatur ulang ID pesanan API saat ini melalui Trader Workstation > Global Configuration > API Settings panel:

# ib_execution.py
    
    def create_initial_order_id(self):
        """
        Creates the initial order ID used for Interactive
        Brokers to keep track of submitted orders.
        """
        # There is scope for more logic here, but we
        # will use "1" as the default for now.
        return 1

Metode berikut, register_handlers, hanya mendaftarkan metode pengendali kesalahan dan jawaban yang didefinisikan di atas dengan koneksi TWS:

# ib_execution.py
    
    def register_handlers(self):
        """
        Register the error and server reply 
        message handling functions.
        """
        # Assign the error handling function defined above
        # to the TWS connection
        self.tws_conn.register(self._error_handler, 'Error')

        # Assign all of the server reply messages to the
        # reply_handler function defined above
        self.tws_conn.registerAll(self._reply_handler)

Seperti tutorial sebelumnya tentang penggunaan IbPy, kita perlu membuat contoh Kontrak dan kemudian memasangkannya dengan contoh Order, yang akan dikirim ke API IB. Metode berikut, create_contract, menghasilkan komponen pertama dari pasangan ini. Ini mengharapkan simbol ticker, jenis sekuritas (misalnya saham atau masa depan), pertukaran / pertukaran utama dan mata uang. Ini mengembalikan contoh Kontrak:

# ib_execution.py
    
    def create_contract(self, symbol, sec_type, exch, prim_exch, curr):
        """
        Create a Contract object defining what will
        be purchased, at which exchange and in which currency.

        symbol - The ticker symbol for the contract
        sec_type - The security type for the contract ('STK' is 'stock')
        exch - The exchange to carry out the contract on
        prim_exch - The primary exchange to carry out the contract on
        curr - The currency in which to purchase the contract
        """
        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

Metode berikut, create_order, menghasilkan komponen kedua dari pasangan, yaitu contoh Order. Ini mengharapkan jenis order (misalnya pasar atau batas), jumlah aset untuk diperdagangkan dan action (beli atau jual).

# ib_execution.py
    
    def create_order(self, order_type, quantity, action):
        """
        Create an Order object (Market/Limit) to go long/short.

        order_type - 'MKT', 'LMT' for Market or Limit orders
        quantity - Integral number of assets to order
        action - 'BUY' or 'SELL'
        """
        order = Order()
        order.m_orderType = order_type
        order.m_totalQuantity = quantity
        order.m_action = action
        return order

Untuk menghindari duplikasi contoh FillEvent untuk ID order tertentu, kami menggunakan kamus yang disebut fill_dict untuk menyimpan kunci yang sesuai dengan ID order tertentu. Ketika pengisian telah dihasilkan, kunci filled dari entri untuk ID order tertentu ditetapkan menjadi True. Jika pesan Server Response berikutnya diterima dari IB yang menyatakan bahwa sebuah order telah diisi (dan merupakan pesan duplikat) maka tidak akan menyebabkan pengisian baru. Metode berikut create_fill_dict_entry melakukan ini:

# ib_execution.py
    
    def create_fill_dict_entry(self, msg):
        """
        Creates an entry in the Fill Dictionary that lists 
        orderIds and provides security information. This is
        needed for the event-driven behaviour of the IB
        server message behaviour.
        """
        self.fill_dict[msg.orderId] = {
            "symbol": msg.contract.m_symbol,
            "exchange": msg.contract.m_exchange,
            "direction": msg.order.m_action,
            "filled": False
        }

Metode berikut, create_fill, sebenarnya membuat contoh FillEvent dan menempatkannya pada antrian acara:

# ib_execution.py
    
    def create_fill(self, msg):
        """
        Handles the creation of the FillEvent that will be
        placed onto the events queue subsequent to an order
        being filled.
        """
        fd = self.fill_dict[msg.orderId]

        # Prepare the fill data
        symbol = fd["symbol"]
        exchange = fd["exchange"]
        filled = msg.filled
        direction = fd["direction"]
        fill_cost = msg.avgFillPrice

        # Create a fill event object
        fill = FillEvent(
            datetime.datetime.utcnow(), symbol, 
            exchange, filled, direction, fill_cost
        )

        # Make sure that multiple messages don't create
        # additional fills.
        self.fill_dict[msg.orderId]["filled"] = True

        # Place the fill event onto the event queue
        self.events.put(fill_event)

Sekarang setelah semua metode sebelumnya telah diimplementasikan, metode execute_order harus dihapus dari kelas dasar abstrak ExecutionHandler. Metode ini sebenarnya melakukan penempatan order dengan IB API.

Kami pertama-tama memeriksa bahwa event yang diterima ke metode ini sebenarnya adalah OrderEvent dan kemudian mempersiapkan objek Kontrak dan Order dengan parameter masing-masing.

Sangat penting untuk memanggil metode time.sleep ((1) untuk memastikan perintah benar-benar melalui ke IB. Penghapusan baris ini menyebabkan perilaku tidak konsisten dari API, setidaknya pada sistem saya!

Akhirnya, kita meningkatkan ID pesanan untuk memastikan kita tidak menggandakan pesanan:

# ib_execution.py
    
    def execute_order(self, event):
        """
        Creates the necessary InteractiveBrokers order object
        and submits it to IB via their API.

        The results are then queried in order to generate a
        corresponding Fill object, which is placed back on
        the event queue.

        Parameters:
        event - Contains an Event object with order information.
        """
        if event.type == 'ORDER':
            # Prepare the parameters for the asset order
            asset = event.symbol
            asset_type = "STK"
            order_type = event.order_type
            quantity = event.quantity
            direction = event.direction

            # Create the Interactive Brokers contract via the 
            # passed Order event
            ib_contract = self.create_contract(
                asset, asset_type, self.order_routing,
                self.order_routing, self.currency
            )

            # Create the Interactive Brokers order via the 
            # passed Order event
            ib_order = self.create_order(
                order_type, quantity, direction
            )

            # Use the connection to the send the order to IB
            self.tws_conn.placeOrder(
                self.order_id, ib_contract, ib_order
            )

            # NOTE: This following line is crucial.
            # It ensures the order goes through!
            time.sleep(1)

            # Increment the order ID for this session
            self.order_id += 1

Kelas ini merupakan dasar dari pengendali eksekusi Interactive Brokers dan dapat digunakan sebagai pengganti pengendali eksekusi simulasi, yang hanya cocok untuk backtesting.

Dengan cara ini kita menggunakan kembali sebanyak mungkin dari backtest dan sistem hidup untuk memastikan bahwa kode swap out diminimalkan dan dengan demikian perilaku di kedua adalah serupa, jika tidak identik.


Lebih banyak