Backtesting Strategi Pasangan Reversi Rata-rata Intraday Antara SPY Dan IWM

Penulis:Kebaikan, Dibuat: 2019-03-28 10:51:06, Diperbarui:

Dalam artikel ini kita akan membahas strategi trading intraday pertama kita. Ini akan menggunakan ide trading klasik, yaitu pairs trading. Dalam hal ini kita akan menggunakan dua Exchange Traded Funds (ETF), SPY dan IWM, yang diperdagangkan di New York Stock Exchange (NYSE) dan mencoba mewakili indeks pasar saham AS, S&P500 dan Russell 2000, masing-masing.

Strategi ini secara luas menciptakan spread antara pasangan ETF dengan lama satu dan memperpendek jumlah yang lain. Rasio panjang ke pendek dapat didefinisikan dengan banyak cara seperti menggunakan teknik seri waktu kointegrasi statistik. Dalam skenario ini kita akan menghitung rasio lindung nilai antara SPY dan IWM melalui regresi linier bergulir. Hal ini kemudian akan memungkinkan kita untuk membuat spread antara SPY dan IWM yang dinormalisasi menjadi z-score. Sinyal perdagangan akan dihasilkan ketika z-score melebihi ambang batas tertentu dengan keyakinan bahwa spread akan kembali ke rata-rata.

Alasan untuk strategi ini adalah bahwa SPY dan IWM kira-kira mencirikan situasi yang sama, yaitu ekonomi dari sekelompok perusahaan AS dengan kapitalisasi besar dan kecil. Premisnya adalah bahwa jika seseorang mengambil spread harga maka itu harus membalikkan rata-rata, karena sementara peristiwa lokal (dalam waktu) dapat mempengaruhi indeks S&P500 atau Russell 2000 secara terpisah (seperti perbedaan kapitalisasi kecil / kapitalisasi besar, tanggal rebalancing atau perdagangan blok), rangkaian harga jangka panjang keduanya kemungkinan akan terintegrasi.

Strategi

Strategi ini dilaksanakan dalam langkah-langkah berikut:

  1. Data - batang SPY dan IWM 1 menit diperoleh dari April 2007 hingga Februari 2014.
  2. Pengolahan - Data diselaraskan dengan benar dan bar yang hilang saling dibuang.
  3. Spread - rasio lindung nilai antara dua ETF dihitung dengan mengambil regresi linier bergulir. Ini didefinisikan sebagai koefisien regresi β menggunakan jendela lookback yang bergeser ke depan 1 bar dan menghitung ulang koefisien regresi. Dengan demikian rasio lindung nilai βi, untuk bar bi dihitung di seluruh titik bi−1−k ke bi−1 untuk melihat kembali k bar.
  4. Z-Score - Skor standar dari spread dihitung dengan cara biasa. Ini berarti mengurangi rata-rata (sampel) dari spread dan membagi dengan (sampel) standar deviasi dari spread. Alasan untuk ini adalah untuk membuat parameter ambang lebih mudah untuk diinterpretasikan karena z-score adalah kuantitas tanpa dimensi. Saya sengaja memperkenalkan bias lookhead ke dalam perhitungan untuk menunjukkan betapa halusnya itu. Cobalah dan awaslah!
  5. Perdagangan - Sinyal panjang dihasilkan ketika z-score negatif turun di bawah ambang batas yang telah ditentukan sebelumnya (atau pasca dioptimalkan), sedangkan sinyal pendek adalah kebalikannya. Sinyal keluar dihasilkan ketika z-score absolut turun di bawah ambang batas tambahan. Untuk strategi ini saya telah (agak sewenang-wenang) memilih ambang batas masuk mutlak z-score = 2 dan ambang batas keluar z-score = 1.

Mungkin cara terbaik untuk memahami strategi secara mendalam adalah untuk benar-benar menerapkannya. Bagian berikut menggambarkan kode Python lengkap (file tunggal) untuk menerapkan strategi mean-reverting ini. Saya telah berkomentar secara liberal kode untuk membantu pemahaman.

Implementasi Python

Seperti halnya semua tutorial Python/pandas, Anda harus memiliki pengaturan lingkungan penelitian Python seperti yang dijelaskan dalam tutorial ini. Setelah pengaturan, tugas pertama adalah mengimpor perpustakaan Python yang diperlukan. Untuk backtest ini, matplotlib dan panda diperlukan.

Versi perpustakaan tertentu yang saya gunakan adalah sebagai berikut:

  • Python - 2.7.3
  • NumPy - 1.8.0
  • panda - 0.12.0
  • Matplotlib - 1.1.0 Mari kita lanjutkan dan mengimpor perpustakaan:
# mr_spy_iwm.py

import matplotlib.pyplot as plt
import numpy as np
import os, os.path
import pandas as pd

Fungsi berikut create_pairs_dataframe mengimpor dua file CSV yang berisi bilah intraday dari dua simbol. Dalam kasus kami ini akan menjadi SPY dan IWM. Ini kemudian membuat pasangan dataframe terpisah, yang menggunakan indeks dari kedua file asli. Karena timestamp mereka kemungkinan berbeda karena perdagangan yang terlewatkan dan kesalahan, ini menjamin bahwa kita akan memiliki data yang cocok. Ini adalah salah satu manfaat utama dari menggunakan perpustakaan data analyis seperti panda. Kode boilerplate ditangani untuk kita dengan cara yang sangat efisien.

# mr_spy_iwm.py

def create_pairs_dataframe(datadir, symbols):
    """Creates a pandas DataFrame containing the closing price
    of a pair of symbols based on CSV files containing a datetime
    stamp and OHLCV data."""

    # Open the individual CSV files and read into pandas DataFrames
    print "Importing CSV data..."
    sym1 = pd.io.parsers.read_csv(os.path.join(datadir, '%s.csv' % symbols[0]),
                                  header=0, index_col=0, 
                                  names=['datetime','open','high','low','close','volume','na'])
    sym2 = pd.io.parsers.read_csv(os.path.join(datadir, '%s.csv' % symbols[1]),
                                  header=0, index_col=0, 
                                  names=['datetime','open','high','low','close','volume','na'])

    # Create a pandas DataFrame with the close prices of each symbol
    # correctly aligned and dropping missing entries
    print "Constructing dual matrix for %s and %s..." % symbols    
    pairs = pd.DataFrame(index=sym1.index)
    pairs['%s_close' % symbols[0].lower()] = sym1['close']
    pairs['%s_close' % symbols[1].lower()] = sym2['close']
    pairs = pairs.dropna()
    return pairs

Langkah selanjutnya adalah melakukan regresi linier bergulir antara SPY dan IWM. Dalam hal ini IWM adalah prediktor (x) dan SPY adalah respons (y). Saya telah menetapkan jendela lookback default 100 bar. Seperti yang dibahas di atas ini adalah parameter strategi. Agar strategi dianggap kuat, kita idealnya ingin melihat profil pengembalian (atau ukuran kinerja lainnya) sebagai fungsi melengkung periode lookback.

Setelah koefisien beta bergulir dihitung dalam model regresi linier untuk SPY-IWM, kita menambahkannya ke pasangan DataFrame dan menjatuhkan baris kosong. Ini merupakan set pertama bar sama dengan ukuran lookback sebagai ukuran trimming.

Akhirnya, kita membuat z-score dari spread, yang dihitung dengan mengurangi rata-rata spread dan menormalkan dengan standar deviasi spread. Perhatikan bahwa ada bias lookahead yang agak halus terjadi di sini. Saya sengaja meninggalkannya dalam kode karena saya ingin menekankan betapa mudahnya membuat kesalahan seperti itu dalam penelitian. Rata-rata dan standar deviasi dihitung untuk seluruh rentetan waktu spread. Jika ini mencerminkan akurasi historis yang benar maka informasi ini tidak akan tersedia karena secara implisit menggunakan informasi masa depan. Oleh karena itu kita harus menggunakan mean rolling dan stdev untuk menghitung z-score.

# mr_spy_iwm.py

def calculate_spread_zscore(pairs, symbols, lookback=100):
    """Creates a hedge ratio between the two symbols by calculating
    a rolling linear regression with a defined lookback period. This
    is then used to create a z-score of the 'spread' between the two
    symbols based on a linear combination of the two."""
    
    # Use the pandas Ordinary Least Squares method to fit a rolling
    # linear regression between the two closing price time series
    print "Fitting the rolling Linear Regression..."
    model = pd.ols(y=pairs['%s_close' % symbols[0].lower()], 
                   x=pairs['%s_close' % symbols[1].lower()],
                   window=lookback)

    # Construct the hedge ratio and eliminate the first 
    # lookback-length empty/NaN period
    pairs['hedge_ratio'] = model.beta['x']
    pairs = pairs.dropna()

    # Create the spread and then a z-score of the spread
    print "Creating the spread/zscore columns..."
    pairs['spread'] = pairs['spy_close'] - pairs['hedge_ratio']*pairs['iwm_close']
    pairs['zscore'] = (pairs['spread'] - np.mean(pairs['spread']))/np.std(pairs['spread'])
    return pairs

Dalam create_long_short_market_signals sinyal perdagangan dibuat. Ini dihitung dengan pergi panjang spread ketika z-score negatif melebihi z-score negatif dan pergi pendek spread ketika z-score positif melebihi z-score positif.

Untuk mencapai situasi ini, perlu diketahui, untuk setiap bar, apakah strategi adalah in atau out dari pasar. long_market dan short_market adalah dua variabel yang didefinisikan untuk melacak posisi pasar panjang dan pendek. Sayangnya ini jauh lebih sederhana untuk dikodekan dengan cara iteratif dibandingkan dengan pendekatan vektor dan dengan demikian lambat dihitung. Meskipun bar 1 menit membutuhkan ~ 700.000 titik data per file CSV, itu masih relatif cepat dihitung pada mesin desktop lama saya!

Untuk mengulangi pada Pandas DataFrame (yang memang TIDAK merupakan operasi umum) perlu menggunakan metode iterrows, yang menyediakan generator untuk mengulangi:

# mr_spy_iwm.py

def create_long_short_market_signals(pairs, symbols, 
                                     z_entry_threshold=2.0, 
                                     z_exit_threshold=1.0):
    """Create the entry/exit signals based on the exceeding of 
    z_enter_threshold for entering a position and falling below
    z_exit_threshold for exiting a position."""

    # Calculate when to be long, short and when to exit
    pairs['longs'] = (pairs['zscore'] <= -z_entry_threshold)*1.0
    pairs['shorts'] = (pairs['zscore'] >= z_entry_threshold)*1.0
    pairs['exits'] = (np.abs(pairs['zscore']) <= z_exit_threshold)*1.0

    # These signals are needed because we need to propagate a
    # position forward, i.e. we need to stay long if the zscore
    # threshold is less than z_entry_threshold by still greater
    # than z_exit_threshold, and vice versa for shorts.
    pairs['long_market'] = 0.0
    pairs['short_market'] = 0.0

    # These variables track whether to be long or short while
    # iterating through the bars
    long_market = 0
    short_market = 0

    # Calculates when to actually be "in" the market, i.e. to have a
    # long or short position, as well as when not to be.
    # Since this is using iterrows to loop over a dataframe, it will
    # be significantly less efficient than a vectorised operation,
    # i.e. slow!
    print "Calculating when to be in the market (long and short)..."
    for i, b in enumerate(pairs.iterrows()):
        # Calculate longs
        if b[1]['longs'] == 1.0:
            long_market = 1            
        # Calculate shorts
        if b[1]['shorts'] == 1.0:
            short_market = 1
        # Calculate exists
        if b[1]['exits'] == 1.0:
            long_market = 0
            short_market = 0
        # This directly assigns a 1 or 0 to the long_market/short_market
        # columns, such that the strategy knows when to actually stay in!
        pairs.ix[i]['long_market'] = long_market
        pairs.ix[i]['short_market'] = short_market
    return pairs

Pada tahap ini kita telah memperbarui pasangan untuk berisi sinyal panjang/pendek yang sebenarnya, yang memungkinkan kita untuk menentukan apakah kita perlu berada di pasar. Sekarang kita perlu membuat portofolio untuk melacak nilai pasar posisi. Tugas pertama adalah membuat kolom posisi yang menggabungkan sinyal panjang dan pendek. Ini akan berisi daftar elemen dari (1,0,−1), dengan 1 mewakili posisi panjang/pasar, 0 mewakili tidak ada posisi (harus keluar) dan -1 mewakili posisi pendek/pasar. Kolom sym1 dan sym2 mewakili nilai pasar posisi SPY dan IWM pada akhir setiap bar.

Setelah nilai pasar ETF telah dibuat, kita menambahkannya untuk menghasilkan nilai pasar total di akhir setiap bar. Ini kemudian diubah menjadi aliran pengembalian oleh metode pct_change untuk objek Seri itu. Baris kode berikutnya membersihkan entri yang buruk (elemen NaN dan inf) dan akhirnya menghitung kurva ekuitas penuh.

# mr_spy_iwm.py

def create_portfolio_returns(pairs, symbols):
    """Creates a portfolio pandas DataFrame which keeps track of
    the account equity and ultimately generates an equity curve.
    This can be used to generate drawdown and risk/reward ratios."""
    
    # Convenience variables for symbols
    sym1 = symbols[0].lower()
    sym2 = symbols[1].lower()

    # Construct the portfolio object with positions information
    # Note that minuses to keep track of shorts!
    print "Constructing a portfolio..."
    portfolio = pd.DataFrame(index=pairs.index)
    portfolio['positions'] = pairs['long_market'] - pairs['short_market']
    portfolio[sym1] = -1.0 * pairs['%s_close' % sym1] * portfolio['positions']
    portfolio[sym2] = pairs['%s_close' % sym2] * portfolio['positions']
    portfolio['total'] = portfolio[sym1] + portfolio[sym2]

    # Construct a percentage returns stream and eliminate all 
    # of the NaN and -inf/+inf cells
    print "Constructing the equity curve..."
    portfolio['returns'] = portfolio['total'].pct_change()
    portfolio['returns'].fillna(0.0, inplace=True)
    portfolio['returns'].replace([np.inf, -np.inf], 0.0, inplace=True)
    portfolio['returns'].replace(-1.0, 0.0, inplace=True)

    # Calculate the full equity curve
    portfolio['returns'] = (portfolio['returns'] + 1.0).cumprod()
    return portfolio

Peraturanutamafungsi membawa semuanya bersama. file CSV intraday terletak di jalur datadir. pastikan untuk memodifikasi kode di bawah ini untuk menunjuk ke direktori tertentu Anda.

Untuk menentukan seberapa sensitif strategi terhadap periode lookback, perlu menghitung metrik kinerja untuk rentang lookback. Saya telah memilih pengembalian persentase total akhir portofolio sebagai ukuran kinerja dan rentang lookback di [50,200] dengan kenaikan 10. Anda dapat melihat dalam kode berikut bahwa fungsi sebelumnya dibungkus dalam loop for di seluruh rentang ini, dengan ambang batas lainnya tetap. Tugas akhir adalah menggunakan matplotlib untuk membuat bagan garis lookback vs return:

# mr_spy_iwm.py

if __name__ == "__main__":
    datadir = '/your/path/to/data/'  # Change this to reflect your data path!
    symbols = ('SPY', 'IWM')

    lookbacks = range(50, 210, 10)
    returns = []

    # Adjust lookback period from 50 to 200 in increments
    # of 10 in order to produce sensitivities
    for lb in lookbacks: 
        print "Calculating lookback=%s..." % lb
        pairs = create_pairs_dataframe(datadir, symbols)
        pairs = calculate_spread_zscore(pairs, symbols, lookback=lb)
        pairs = create_long_short_market_signals(pairs, symbols, 
                                                z_entry_threshold=2.0, 
                                                z_exit_threshold=1.0)

        portfolio = create_portfolio_returns(pairs, symbols)
        returns.append(portfolio.ix[-1]['returns'])

    print "Plot the lookback-performance scatterchart..."
    plt.plot(lookbacks, returns, '-o')
    plt.show()

Jika kita melihat situasi di mana lookback independen dari return ini akan menjadi penyebab keprihatinan:imgSPY-IWM linear regression hedge-ratio lookback period sensitivity analysis (Analisis sensitivitas periode)

Tidak ada artikel backtesting yang akan lengkap tanpa kurva ekuitas yang miring ke atas! Jadi jika Anda ingin memetakan kurva pengembalian kumulatif vs waktu, Anda dapat menggunakan kode berikut. Ini akan memetakan portofolio akhir yang dihasilkan dari studi parameter lookback. Dengan demikian, Anda perlu memilih lookback tergantung pada grafik mana yang ingin Anda visualisasikan.

# mr_spy_iwm.py

    # This is still within the main function
    print "Plotting the performance charts..."
    fig = plt.figure()
    fig.patch.set_facecolor('white')

    ax1 = fig.add_subplot(211,  ylabel='%s growth (%%)' % symbols[0])
    (pairs['%s_close' % symbols[0].lower()].pct_change()+1.0).cumprod().plot(ax=ax1, color='r', lw=2.)

    ax2 = fig.add_subplot(212, ylabel='Portfolio value growth (%%)')
    portfolio['returns'].plot(ax=ax2, lw=2.)

    fig.show()

Grafik kurva ekuitas berikut adalah untuk periode 100 hari:imgSPY-IWM linear regression hedge-ratio lookback period sensitivity analysis (Analisis sensitivitas periode)

Perlu dicatat bahwa pengurangan SPY signifikan pada tahun 2009 selama periode krisis keuangan. Strategi juga mengalami periode volatilitas pada tahap ini. Juga dicatat bahwa kinerja telah memburuk sedikit pada tahun lalu karena sifat tren yang kuat dari SPY pada periode ini, yang mencerminkan indeks S&P500.

Perhatikan bahwa kita masih harus memperhitungkan bias lookahead saat menghitung z-score dari spread. Selanjutnya, semua perhitungan ini telah dilakukan tanpa biaya transaksi. Strategi ini pasti akan sangat buruk sekali faktor-faktor ini dipertimbangkan. Biaya, bid / ask spread dan slippage saat ini tidak diperhitungkan. Selain itu strategi tersebut diperdagangkan dalam unit pecahan ETF, yang juga sangat tidak realistis.

Dalam artikel berikutnya kita akan membuat backtester yang didorong oleh peristiwa yang jauh lebih canggih yang akan memperhitungkan faktor-faktor ini dan memberi kita kepercayaan yang jauh lebih besar dalam kurva ekuitas dan metrik kinerja kita.


Lebih banyak