Pengujian Kembali yang Dikendalikan Acara dengan Python - Bahagian VIII

Penulis:Kebaikan, Dicipta: 2019-03-26 16:38:59, Dikemas kini:

Sudah lama sejak kita telah mempertimbangkan backtester yang didorong oleh peristiwa, yang kami mulakan membincangkan dalam artikel ini. Di Bahagian VI saya menerangkan cara mengkodkan model ExecutionHandler stand-in yang berfungsi untuk situasi backtesting sejarah. Dalam artikel ini kita akan mengkodkan pengendali API Interactive Brokers yang sepadan untuk bergerak ke arah sistem perdagangan langsung.

Saya sebelum ini telah membincangkan cara memuat turun Trader Workstation dan membuat akaun demo Interactive Brokers serta cara membuat antara muka asas untuk IB API menggunakan IbPy.

Idea asas kelas IBExecutionHandler (lihat di bawah) adalah untuk menerima contoh OrderEvent dari barisan acara dan kemudian untuk melaksanakan mereka terus terhadap API pesanan Interactive Brokers menggunakan perpustakaan IbPy. Kelas ini juga akan mengendalikan mesej Server Response yang dihantar kembali melalui API. Pada peringkat ini, satu-satunya tindakan yang diambil adalah untuk membuat contoh FillEvent yang sepadan yang kemudian akan dihantar kembali ke barisan acara.

Kelas itu sendiri mungkin menjadi agak kompleks, dengan logik pengoptimuman pelaksanaan serta penanganan ralat yang canggih.

Pelaksanaan Python

Seperti biasa, tugas pertama adalah untuk membuat fail Python dan mengimport perpustakaan yang diperlukan. Fail ini dipanggil ib_execution.py dan tinggal di direktori yang sama dengan fail yang digerakkan oleh peristiwa yang lain.

Kami mengimport perpustakaan penanganan tarikh / masa yang diperlukan, objek IbPy dan objek acara 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 kini menentukan kelas IBExecutionHandler.mulakanconstructor pertama memerlukan pengetahuan tentang barisan acara. Ia juga memerlukan spesifikasi order_routing, yang saya telah lalai untuk SMART. Jika anda mempunyai keperluan pertukaran tertentu, anda boleh menentukan mereka di sini. Mata wang lalai juga telah ditetapkan kepada Dolar AS.

Dalam kaedah ini kita mencipta kamus fill_dict, yang diperlukan kemudian untuk digunakan dalam menjana contoh FillEvent. Kita juga mencipta objek sambungan tws_conn untuk menyimpan maklumat sambungan kami ke API Broker Interaktif. Kita juga perlu membuat order_id lalai awal, yang menjejaki semua perintah berikutnya untuk mengelakkan pendua. Akhirnya kita mendaftar pengendali mesej (yang akan kita tentukan dengan lebih terperinci 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 peristiwa berasaskan mesej yang membolehkan kelas kami bertindak balas dengan cara tertentu kepada mesej tertentu, dengan cara yang serupa dengan backtester yang didorong peristiwa itu sendiri.

Kaedah _reply_handler, sebaliknya, digunakan untuk menentukan sama ada contoh FillEvent perlu dicipta. Kaedah ini bertanya sama ada mesej openOrder telah diterima dan memeriksa sama ada entri dalam fill_dict kami untuk orderId tertentu telah ditetapkan. Jika tidak maka satu dibuat.

Jika ia melihat mesej orderStatus dan mesej tertentu menyatakan bahawa pesanan telah diisi, maka ia memanggil create_fill untuk membuat FillEvent. Ia juga mengeluarkan mesej ke terminal untuk tujuan logging / debug:

# 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)

Kaedah berikut, create_tws_connection, mewujudkan sambungan ke API IB menggunakan objek IbPy ibConnection. Ia menggunakan port lalai 7496 dan clientId lalai 10. Setelah objek dicipta, kaedah sambung dipanggil untuk melakukan sambungan:

# 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 mengesan pesanan yang berasingan (untuk tujuan mengesan mengisi) kaedah berikut create_initial_order_id digunakan. Saya telah menetapkannya sebagai 1, tetapi pendekatan yang lebih canggih adalah pertanyaan IB untuk ID yang paling baru dan gunakan itu. Anda sentiasa boleh menetapkan semula ID pesanan API semasa 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

Kaedah berikut, register_handlers, hanya mendaftarkan kaedah pengendali ralat dan jawapan yang ditakrifkan di atas dengan sambungan 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 mengenai penggunaan IbPy, kita perlu membuat contoh Kontrak dan kemudian memasangkannya dengan contoh Perintah, yang akan dihantar ke API IB. Kaedah berikut, create_contract, menghasilkan komponen pertama pasangan ini. Ia mengharapkan simbol ticker, jenis sekuriti (misalnya stok atau masa depan), pertukaran / pertukaran utama dan mata wang. Ia 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

Kaedah berikut, create_order, menjana komponen kedua pasangan, iaitu contoh Perintah. Ia mengharapkan jenis pesanan (contohnya pasaran atau had), kuantiti aset untuk berdagang dan action (beli atau jual). Ia mengembalikan contoh Perintah:

# 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 mengelakkan mengulangi contoh FillEvent untuk ID pesanan tertentu, kami menggunakan kamus yang dipanggil fill_dict untuk menyimpan kunci yang sepadan dengan ID pesanan tertentu. Apabila mengisi telah dihasilkan, kunci filled entri untuk ID pesanan tertentu ditetapkan menjadi Benar. Jika mesej Server Response berikutnya diterima dari IB yang menyatakan bahawa pesanan telah diisi (dan merupakan mesej duplikat) ia tidak akan membawa kepada isi baru. Kaedah 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
        }

Kaedah berikut, create_fill, sebenarnya mencipta contoh FillEvent dan meletakkannya pada barisan 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 bahawa semua kaedah sebelumnya telah dilaksanakan, masih tinggal untuk mengabaikan kaedah execute_order dari kelas asas abstrak ExecutionHandler.

Kami mula-mula memeriksa bahawa peristiwa yang diterima untuk kaedah ini sebenarnya adalah OrderEvent dan kemudian menyediakan objek Kontrak dan Order dengan parameter masing-masing.

Ia adalah sangat penting untuk memanggil time.sleep ((1) kaedah untuk memastikan perintah benar-benar melalui ke IB. Penghapusan baris ini membawa kepada tingkah laku yang tidak konsisten API, sekurang-kurangnya 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 membentuk asas pengendali pelaksanaan Interactive Brokers dan boleh digunakan sebagai pengganti pengendali pelaksanaan simulasi, yang hanya sesuai untuk backtesting.

Dengan cara ini kita menggunakan semula sebanyak mungkin dari backtest dan sistem hidup untuk memastikan bahawa kod swap out adalah meminimumkan dan dengan itu tingkah laku di kedua-dua adalah sama, jika tidak sama.


Lebih lanjut