Event-Driven Backtesting dengan Python - Bagian V

Penulis:Kebaikan, Dibuat: 2019-03-25 15:54:16, Diperbarui:

Dalam artikel sebelumnya tentang backtesting event-driven, kami membahas bagaimana membangun hierarki kelas Strategi. Strategi, sebagaimana didefinisikan di sini, digunakan untuk menghasilkan sinyal, yang digunakan oleh objek portofolio untuk membuat keputusan apakah akan mengirim pesanan. Seperti sebelumnya, wajar untuk membuat kelas dasar abstrak portofolio (ABC) yang semua subkelas berikutnya mewarisi.

Artikel ini menjelaskan objek NaivePortfolio yang melacak posisi dalam portofolio dan menghasilkan pesanan jumlah saham tetap berdasarkan sinyal. Objek portofolio selanjutnya akan mencakup alat manajemen risiko yang lebih canggih dan akan menjadi subjek artikel selanjutnya.

Pelacakan Posisi dan Manajemen Pesenan

Sistem manajemen pesanan portofolio mungkin merupakan komponen yang paling kompleks dari backtester event-driven. Perannya adalah untuk melacak semua posisi pasar saat ini serta nilai pasar posisi (dikenal sebagai holdings). Ini hanyalah perkiraan nilai likuidasi posisi dan sebagian berasal dari fasilitas penanganan data backtester.

Selain manajemen posisi dan kepemilikan, portofolio juga harus menyadari faktor risiko dan teknik ukuran posisi untuk mengoptimalkan pesanan yang dikirim ke broker atau bentuk akses pasar lainnya.

Berlanjut dalam hirarki kelas Event, objek Portfolio harus dapat menangani objek SignalEvent, menghasilkan objek OrderEvent dan menafsirkan objek FillEvent untuk memperbarui posisi.

Pelaksanaan

Kami membuat file baruportfolio.pydan mengimpor perpustakaan yang diperlukan. ini sama dengan sebagian besar implementasi kelas dasar abstrak lainnya. kita perlu mengimpor fungsi lantai dari perpustakaan matematika untuk menghasilkan ukuran urutan dengan nilai bulat. kita juga membutuhkan objek FillEvent dan OrderEvent karena Portfolio menangani keduanya.

# portfolio.py

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

dari abc import ABCMeta, metode abstrak dari lantai impor matematika

dari event import FillEvent, OrderEvent Seperti sebelumnya kita membuat ABC untuk Portfolio dan memiliki dua metode virtual murni update_signal dan update_fill. Yang pertama menangani sinyal perdagangan baru yang diambil dari antrian acara dan yang terakhir menangani mengisi yang diterima dari objek pengendali eksekusi.

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

Subyek utama dari artikel ini adalah kelas NaivePortfolio. Ini dirancang untuk menangani ukuran posisi dan kepemilikan saat ini, tetapi akan melakukan pesanan perdagangan dengan cara dumb dengan hanya mengirimnya langsung ke pialang dengan ukuran kuantitas tetap yang telah ditentukan sebelumnya, terlepas dari jumlah uang tunai yang dipegang.

The NaivePortfolio membutuhkan nilai modal awal, yang telah saya atur secara default sebesar 100.000 USD.

Portfolio berisi semua_posisi dan anggota_posisi saat ini. Yang pertama menyimpan daftar semua posisi sebelumnya yang tercatat pada saat timestamp dari peristiwa data pasar. Posisi hanyalah jumlah aset. Posisi negatif berarti aset telah diringkas. Anggota terakhir menyimpan kamus yang berisi posisi saat ini untuk pembaruan bar pasar terakhir.

Selain anggota posisi, portofolio menyimpan kepemilikan, yang menggambarkan nilai pasar saat ini dari posisi yang dipegang. Nilai pasar saat ini dalam hal ini berarti harga penutupan yang diperoleh dari bar pasar saat ini, yang jelas merupakan perkiraan, tetapi cukup wajar untuk saat ini. all_holdings menyimpan daftar historis semua kepemilikan simbol, sementara current_holdings menyimpan kamus terbaru dari semua nilai kepemilikan 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()

Metode berikut, construct_all_positions, hanya membuat kamus untuk setiap simbol, menetapkan nilai menjadi nol untuk masing-masing dan kemudian menambahkan kunci waktu tanggal, akhirnya menambahkannya ke daftar.

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

Metode construct_all_holdings mirip dengan yang di atas tetapi menambahkan kunci tambahan untuk kas, komisi dan total, yang masing-masing mewakili kas cadangan di akun setelah pembelian, komisi kumulatif yang terkumpul dan total ekuitas akun termasuk kas dan posisi terbuka. Posisi pendek diperlakukan sebagai negatif.

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

Metode berikut, construct_current_holdings hampir sama dengan metode di atas kecuali bahwa tidak membungkus kamus dalam daftar:

# 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 heartbeat, yaitu setiap kali data pasar baru diminta dari objek DataHandler, portofolio harus memperbarui nilai pasar saat ini dari semua posisi yang dipegang.

Sayangnya tidak ada hal seperti nilai pasar saat ini karena spread bid/ask dan masalah likuiditas. Oleh karena itu perlu untuk memperkirakan dengan mengalikan jumlah aset yang dimiliki dengan harga. Pendekatan yang saya ambil di sini adalah menggunakan harga penutupan bar terakhir yang diterima. Untuk strategi intraday ini relatif realistis. Untuk strategi harian ini kurang realistis karena harga pembukaan dapat berbeda secara substansial dari harga penutupan.

Metode update_timeindex menangani pelacakan kepemilikan baru. Pertama-tama memperoleh harga terbaru dari pengendali data pasar dan membuat kamus simbol baru untuk mewakili posisi saat ini, dengan menetapkan posisi new sama dengan posisi current. Ini hanya berubah ketika FillEvent diperoleh, yang ditangani nanti di portofolio. Metode kemudian menambahkan set posisi saat ini ke daftar all_positions. Selanjutnya kepemilikan diperbarui dengan cara yang sama, dengan pengecualian bahwa nilai pasar dihitung ulang dengan mengalikan posisi saat ini dengan harga penutupan bar terbaru (self.current_positions[s] * bar[s][0][5]). Akhirnya kepemilikan baru ditambahkan ke semua_positions:

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

Metode update_positions_from_fill menentukan apakah FillEvent adalah Buy atau Sell dan kemudian memperbarui kamus current_positions dengan menambahkan/mengurangi jumlah saham yang benar:

# 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 metode yang sama dengan metode di atas tetapi memperbarui nilai kepemilikan sebagai gantinya. Untuk mensimulasikan biaya pengisian, metode berikut tidak menggunakan biaya yang terkait dari FillEvent. Mengapa ini? Sederhananya, dalam lingkungan backtesting biaya pengisian sebenarnya tidak diketahui dan karenanya harus diperkirakan. Dengan demikian biaya pengisian ditetapkan menjadi harga pasar saat ini (harga penutupan bar terakhir).

Setelah biaya pengisian diketahui, saham saat ini, uang tunai dan nilai total dapat diperbarui.

# 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 virtual murni dari Portfolio ABC diimplementasikan di sini. Ini hanya mengeksekusi dua metode sebelumnya, update_positions_from_fill dan update_holdings_from_fill yang telah dibahas 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)

Sementara objek Portfolio harus menangani FillEvents, ia juga harus mengurus untuk menghasilkan OrderEvents pada saat penerimaan satu atau lebih SignalEvents. Metode generate_naive_order hanya mengambil sinyal untuk panjang atau pendek aset dan kemudian mengirim order untuk melakukannya untuk 100 saham aset tersebut. Jelas 100 adalah nilai sewenang-wenang. Dalam implementasi yang realistis nilai ini akan ditentukan oleh manajemen risiko atau overlay ukuran posisi. Namun, ini adalah NaivePortfolio dan jadi naively mengirim semua order langsung dari sinyal, tanpa sistem risiko.

Metode ini menangani keinginan, shorting dan keluar dari posisi, berdasarkan kuantitas saat ini dan simbol tertentu.

# 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

Metode update_signal hanya memanggil metode di atas dan menambahkan urutan yang dihasilkan ke antrian 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)

Metode terakhir dalam NaivePortfolio adalah generasi kurva ekuitas. Ini hanya menciptakan aliran pengembalian, berguna untuk perhitungan kinerja dan kemudian menormalkan kurva ekuitas untuk menjadi persentase berbasis. Dengan demikian ukuran awal akun 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 portofolio adalah aspek yang paling kompleks dari seluruh sistem backtest event-driven. Implementasi di sini, meskipun rumit, relatif sederhana dalam penanganan posisi. Versi berikutnya akan mempertimbangkan manajemen risiko dan ukuran posisi, yang akan mengarah pada ide yang jauh lebih realistis dari kinerja strategi.

Dalam artikel berikutnya kita akan mempertimbangkan bagian terakhir dari backtester event-driven, yaitu objek ExecutionHandler, yang digunakan untuk mengambil objek OrderEvent dan membuat objek FillEvent dari mereka.


Lebih banyak