Pengujian Kembali yang Dikendalikan Acara dengan Python - Bahagian V

Penulis:Kebaikan, Dicipta: 2019-03-25 15:54:16, Dikemas kini:

Dalam artikel sebelumnya mengenai backtesting yang didorong peristiwa, kami telah membincangkan cara membina hierarki kelas Strategi. Strategi, seperti yang ditakrifkan di sini, digunakan untuk menjana isyarat, yang digunakan oleh objek portfolio untuk membuat keputusan sama ada untuk menghantar pesanan. Seperti sebelum ini, adalah semula jadi untuk membuat kelas asas abstrak portfolio (ABC) yang semua subkelas berikutnya mewarisi.

Artikel ini menerangkan objek NaivePortfolio yang menjejaki kedudukan dalam portfolio dan menjana pesanan kuantiti saham tetap berdasarkan isyarat. Objek portfolio kemudian akan merangkumi alat pengurusan risiko yang lebih canggih dan akan menjadi subjek artikel seterusnya.

Pengesanan Kedudukan dan Pengurusan Perintah

Sistem pengurusan pesanan portfolio mungkin merupakan komponen yang paling kompleks dari backtester yang didorong oleh peristiwa. Peranannya adalah untuk mengesan semua kedudukan pasaran semasa serta nilai pasaran kedudukan (dikenali sebagai holdings). Ini hanyalah anggaran nilai pembubaran kedudukan dan diperoleh sebahagiannya dari kemudahan pengendalian data backtester.

Sebagai tambahan kepada pengurusan kedudukan dan pegangan, portfolio juga perlu mengetahui faktor risiko dan teknik saiz kedudukan untuk mengoptimumkan pesanan yang dihantar ke broker atau bentuk akses pasaran yang lain.

Meneruskan dalam semangat hierarki kelas Acara objek Portfolio mesti dapat mengendalikan objek SignalEvent, menjana objek OrderEvent dan menafsirkan objek FillEvent untuk mengemas kini kedudukan.

Pelaksanaan

Kami mencipta fail baruportfolio.pydan mengimport perpustakaan yang diperlukan. Ini adalah sama seperti kebanyakan pelaksanaan kelas asas abstrak yang lain. Kita perlu mengimport fungsi lantai dari perpustakaan matematik untuk menjana saiz pesanan bernilai bulat. Kita juga memerlukan objek FillEvent dan OrderEvent kerana Portfolio mengendalikan kedua-duanya.

# portfolio.py

import datetime
import numpy as np
import pandas as pd
import Queue

daripada abc import ABCMeta, abstrakmetod dari lantai import matematik

daripada import peristiwa FillEvent, OrderEvent Seperti sebelum ini, kita membuat ABC untuk Portfolio dan mempunyai dua kaedah maya murni update_signal dan update_fill. Yang pertama mengendalikan isyarat perdagangan baru yang diambil dari barisan acara dan yang terakhir mengendalikan mengisi yang diterima dari objek pengendali pelaksanaan.

# portfolio.py

class Portfolio(object):
    """
    The Portfolio class handles the positions and market
    value of all instruments at a resolution of a "bar",
    i.e. secondly, minutely, 5-min, 30-min, 60 min or EOD.
    """

    __metaclass__ = ABCMeta

    @abstractmethod
    def update_signal(self, event):
        """
        Acts on a SignalEvent to generate new orders 
        based on the portfolio logic.
        """
        raise NotImplementedError("Should implement update_signal()")

    @abstractmethod
    def update_fill(self, event):
        """
        Updates the portfolio current positions and holdings 
        from a FillEvent.
        """
        raise NotImplementedError("Should implement update_fill()")

Subjek utama artikel ini adalah kelas NaivePortfolio. Ia direka untuk mengendalikan saiz kedudukan dan pegangan semasa, tetapi akan melaksanakan pesanan dagangan dengan cara dumb dengan hanya menghantarnya terus ke broker dengan saiz kuantiti tetap yang telah ditentukan sebelumnya, tanpa mengira wang tunai yang dipegang. Ini semua andaian yang tidak realistik, tetapi mereka membantu menggariskan bagaimana sistem pengurusan pesanan portfolio (OMS) berfungsi dengan cara yang didorong oleh peristiwa.

NaivePortfolio memerlukan nilai modal awal, yang saya tetapkan pada lalai 100,000 USD. Ia juga memerlukan tarikh-waktu permulaan.

Portfolio mengandungi semua_posisi dan ahli_posisi semasa. Yang pertama menyimpan senarai semua kedudukan terdahulu yang direkodkan pada tarikh kejadian data pasaran. Posisi hanyalah kuantiti aset. Posisi negatif bermaksud aset telah diletakkan. Ahli terakhir menyimpan kamus yang mengandungi kedudukan semasa untuk kemas kini bar pasaran terakhir.

Sebagai tambahan kepada ahli kedudukan, portfolio menyimpan pegangan, yang menggambarkan nilai pasaran semasa kedudukan yang dipegang. Nilai pasaran semasa dalam kes ini bermaksud harga penutupan yang diperoleh dari bar pasaran semasa, yang jelas adalah pendekatan, tetapi cukup munasabah untuk masa ini. all_holdings menyimpan senarai sejarah semua pegangan simbol, manakala current_holdings menyimpan kamus terkini semua nilai pegangan simbol.

# portfolio.py

class NaivePortfolio(Portfolio):
    """
    The NaivePortfolio object is designed to send orders to
    a brokerage object with a constant quantity size blindly,
    i.e. without any risk management or position sizing. It is
    used to test simpler strategies such as BuyAndHoldStrategy.
    """
    
    def __init__(self, bars, events, start_date, initial_capital=100000.0):
        """
        Initialises the portfolio with bars and an event queue. 
        Also includes a starting datetime index and initial capital 
        (USD unless otherwise stated).

        Parameters:
        bars - The DataHandler object with current market data.
        events - The Event Queue object.
        start_date - The start date (bar) of the portfolio.
        initial_capital - The starting capital in USD.
        """
        self.bars = bars
        self.events = events
        self.symbol_list = self.bars.symbol_list
        self.start_date = start_date
        self.initial_capital = initial_capital
        
        self.all_positions = self.construct_all_positions()
        self.current_positions = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )

        self.all_holdings = self.construct_all_holdings()
        self.current_holdings = self.construct_current_holdings()

Kaedah berikut, construct_all_positions, hanya membuat kamus untuk setiap simbol, menetapkan nilai kepada sifar untuk masing-masing dan kemudian menambah kunci tarikh dan masa, akhirnya menambahkannya ke senarai.

# portfolio.py

    def construct_all_positions(self):
        """
        Constructs the positions list using the start_date
        to determine when the time index will begin.
        """
        d = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
        d['datetime'] = self.start_date
        return [d]

Kaedah construct_all_holdings adalah sama dengan yang di atas tetapi menambah kunci tambahan untuk tunai, komisen dan jumlah, yang masing-masing mewakili wang tunai kosong dalam akaun selepas sebarang pembelian, komisen terkumpul dan jumlah ekuiti akaun termasuk wang tunai dan mana-mana kedudukan terbuka.

# portfolio.py

    def construct_all_holdings(self):
        """
        Constructs the holdings list using the start_date
        to determine when the time index will begin.
        """
        d = dict( (k,v) for k, v in [(s, 0.0) for s in self.symbol_list] )
        d['datetime'] = self.start_date
        d['cash'] = self.initial_capital
        d['commission'] = 0.0
        d['total'] = self.initial_capital
        return [d]

Kaedah berikut, construct_current_holdings hampir sama dengan kaedah di atas kecuali ia tidak membungkus kamus dalam senarai:

# portfolio.py

    def construct_current_holdings(self):
        """
        This constructs the dictionary which will hold the instantaneous
        value of the portfolio across all symbols.
        """
        d = dict( (k,v) for k, v in [(s, 0.0) for s in self.symbol_list] )
        d['cash'] = self.initial_capital
        d['commission'] = 0.0
        d['total'] = self.initial_capital
        return d

Pada setiap detak jantung, iaitu setiap kali data pasaran baru diminta dari objek DataHandler, portfolio mesti mengemas kini nilai pasaran semasa semua kedudukan yang dipegang. Dalam senario perdagangan langsung maklumat ini boleh dimuat turun dan dianalisis terus dari broker, tetapi untuk pelaksanaan backtesting adalah perlu untuk mengira nilai ini secara manual.

Malangnya tidak ada perkara seperti nilai pasaran semasa disebabkan oleh spread tawaran / permintaan dan isu kecairan. Oleh itu, perlu untuk menganggarkannya dengan mengalikan kuantiti aset yang dipegang dengan harga. Pendekatan yang saya ambil di sini adalah menggunakan harga penutupan bar terakhir yang diterima. Untuk strategi intraday ini agak realistik. Untuk strategi harian ini kurang realistik kerana harga pembukaan boleh berbeza dengan harga penutupan.

Metode update_timeindex mengendalikan penjejakan pegangan baru. Pertama, ia memperoleh harga terkini dari pengendali data pasaran dan membuat kamus simbol baru untuk mewakili kedudukan semasa, dengan menetapkan kedudukan new sama dengan kedudukan current. Ini hanya diubah apabila FillEvent diperoleh, yang ditangani kemudian dalam portfolio. Kaedah itu kemudian menambahkan set kedudukan semasa ini ke senarai semua_posisi. Seterusnya pegangan dikemas kini dengan cara yang sama, kecuali nilai pasaran dikira semula dengan mengalikan kedudukan semasa dengan harga penutupan bar terbaru (self.current_positions[s] * bar[s][0][5]). Akhirnya pegangan baru dilampirkan ke semua_posisi:

# portfolio.py

    def update_timeindex(self, event):
        """
        Adds a new record to the positions matrix for the current 
        market data bar. This reflects the PREVIOUS bar, i.e. all
        current market data at this stage is known (OLHCVI).

        Makes use of a MarketEvent from the events queue.
        """
        bars = {}
        for sym in self.symbol_list:
            bars[sym] = self.bars.get_latest_bars(sym, N=1)

        # Update positions
        dp = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
        dp['datetime'] = bars[self.symbol_list[0]][0][1]

        for s in self.symbol_list:
            dp[s] = self.current_positions[s]

        # Append the current positions
        self.all_positions.append(dp)

        # Update holdings
        dh = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
        dh['datetime'] = bars[self.symbol_list[0]][0][1]
        dh['cash'] = self.current_holdings['cash']
        dh['commission'] = self.current_holdings['commission']
        dh['total'] = self.current_holdings['cash']

        for s in self.symbol_list:
            # Approximation to the real value
            market_value = self.current_positions[s] * bars[s][0][5]
            dh[s] = market_value
            dh['total'] += market_value

        # Append the current holdings
        self.all_holdings.append(dh)

Kaedah update_positions_from_fill menentukan sama ada FillEvent adalah Beli atau Jual dan kemudian mengemas kini kamus current_positions dengan menambah/mengurangkan jumlah saham yang betul:

# portfolio.py

    def update_positions_from_fill(self, fill):
        """
        Takes a FilltEvent object and updates the position matrix
        to reflect the new position.

        Parameters:
        fill - The FillEvent object to update the positions with.
        """
        # Check whether the fill is a buy or sell
        fill_dir = 0
        if fill.direction == 'BUY':
            fill_dir = 1
        if fill.direction == 'SELL':
            fill_dir = -1

        # Update positions list with new quantities
        self.current_positions[fill.symbol] += fill_dir*fill.quantity

Update_holdings_from_fill adalah sama dengan kaedah di atas tetapi mengemas kini nilai pegangan. Untuk mensimulasikan kos pengisian, kaedah berikut tidak menggunakan kos yang berkaitan dengan FillEvent. Mengapa ini? Secara ringkas, dalam persekitaran backtesting kos pengisian sebenarnya tidak diketahui dan oleh itu mesti dianggarkan. Oleh itu kos pengisian ditetapkan kepada harga pasaran semasa (harga penutupan bar terakhir).

Apabila kos pengisian diketahui, pegangan semasa, tunai dan nilai keseluruhan semua boleh dikemas kini.

# portfolio.py

    def update_holdings_from_fill(self, fill):
        """
        Takes a FillEvent object and updates the holdings matrix
        to reflect the holdings value.

        Parameters:
        fill - The FillEvent object to update the holdings with.
        """
        # Check whether the fill is a buy or sell
        fill_dir = 0
        if fill.direction == 'BUY':
            fill_dir = 1
        if fill.direction == 'SELL':
            fill_dir = -1

        # Update holdings list with new quantities
        fill_cost = self.bars.get_latest_bars(fill.symbol)[0][5]  # Close price
        cost = fill_dir * fill_cost * fill.quantity
        self.current_holdings[fill.symbol] += cost
        self.current_holdings['commission'] += fill.commission
        self.current_holdings['cash'] -= (cost + fill.commission)
        self.current_holdings['total'] -= (cost + fill.commission)

Metode update_fill maya murni dari Portfolio ABC dilaksanakan di sini. Ia hanya menjalankan dua kaedah sebelumnya, update_positions_from_fill dan update_holdings_from_fill yang telah dibincangkan di atas:

# portfolio.py

    def update_fill(self, event):
        """
        Updates the portfolio current positions and holdings 
        from a FillEvent.
        """
        if event.type == 'FILL':
            self.update_positions_from_fill(event)
            self.update_holdings_from_fill(event)

Walaupun objek Portfolio mesti mengendalikan FillEvents, ia juga mesti menguruskan penjanaan OrderEvents apabila menerima satu atau lebih SignalEvents. Kaedah generate_naive_order hanya mengambil isyarat untuk panjang atau pendek aset dan kemudian menghantar pesanan untuk melakukannya untuk 100 saham aset tersebut. Jelas 100 adalah nilai sewenang-wenang. Dalam pelaksanaan yang realistik nilai ini akan ditentukan oleh pengurusan risiko atau pemasangan saiz kedudukan. Walau bagaimanapun, ini adalah NaivePortfolio dan jadi ia naively menghantar semua pesanan terus dari isyarat, tanpa sistem risiko.

Kaedah ini mengendalikan keinginan, pendek dan keluar dari kedudukan, berdasarkan kuantiti semasa dan simbol tertentu. Objek OrderEvent yang sepadan kemudian dihasilkan:

# portfolio.py

    def generate_naive_order(self, signal):
        """
        Simply transacts an OrderEvent object as a constant quantity
        sizing of the signal object, without risk management or
        position sizing considerations.

        Parameters:
        signal - The SignalEvent signal information.
        """
        order = None

        symbol = signal.symbol
        direction = signal.signal_type
        strength = signal.strength

        mkt_quantity = floor(100 * strength)
        cur_quantity = self.current_positions[symbol]
        order_type = 'MKT'

        if direction == 'LONG' and cur_quantity == 0:
            order = OrderEvent(symbol, order_type, mkt_quantity, 'BUY')
        if direction == 'SHORT' and cur_quantity == 0:
            order = OrderEvent(symbol, order_type, mkt_quantity, 'SELL')   
    
        if direction == 'EXIT' and cur_quantity > 0:
            order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL')
        if direction == 'EXIT' and cur_quantity < 0:
            order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY')
        return order

Kaedah update_signal hanya memanggil kaedah di atas dan menambah urutan yang dihasilkan ke barisan acara:

# portfolio.py

    def update_signal(self, event):
        """
        Acts on a SignalEvent to generate new orders 
        based on the portfolio logic.
        """
        if event.type == 'SIGNAL':
            order_event = self.generate_naive_order(event)
            self.events.put(order_event)

Kaedah akhir dalam NaivePortfolio adalah penjanaan lengkung ekuiti. Ini hanya mewujudkan aliran pulangan, berguna untuk pengiraan prestasi dan kemudian menormalkan lengkung ekuiti untuk berasaskan peratusan. Oleh itu, saiz awal akaun sama dengan 1.0:

# portfolio.py

    def create_equity_curve_dataframe(self):
        """
        Creates a pandas DataFrame from the all_holdings
        list of dictionaries.
        """
        curve = pd.DataFrame(self.all_holdings)
        curve.set_index('datetime', inplace=True)
        curve['returns'] = curve['total'].pct_change()
        curve['equity_curve'] = (1.0+curve['returns']).cumprod()
        self.equity_curve = curve

Objek Portfolio adalah aspek yang paling kompleks dari keseluruhan sistem backtest yang didorong oleh peristiwa. pelaksanaan di sini, walaupun rumit, agak asas dalam pengendalian kedudukan.

Dalam artikel seterusnya kita akan mempertimbangkan bahagian terakhir backtester yang didorong oleh peristiwa, iaitu objek ExecutionHandler, yang digunakan untuk mengambil objek OrderEvent dan membuat objek FillEvent dari mereka.


Lebih lanjut