7
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Python3ではじめるシステムトレード:簡単バックテストでシストレ

Last updated at Posted at 2019-09-20

誰でもできるバックテスト

バックテストはシステムトレードにはなくてはならない道具です。バックテストをするからシステムトレードをする意味があるとも言えます。FREDから日経225をダウンロードします。(経済データのダウンロード参照) つぎにクロスオーバー戦略という移動平均を2本使ってシグナルを出すテクニカル手法の1つを使って、バックテストをしてみます。

初期化

%matplotlib inline 
import matplotlib.pyplot as plt #描画ライブラリ
import pandas_datareader.data as web #データのダウンロードライブラリ
import pandas as pd

つぎに日経225のデータをFREDからダウンロードします。US Yahoo financeからダウンロードも可能です。(Yahoo Finance USから株価をダウンロードしてみた参照) その際にはデータの構造が変わります。

tsd = web.DataReader("NIKKEI225", 'fred','1949').dropna()
tsd.columns=['Close']
tsd.head(1),tsd.tail(1)

結果は

(             Close
 DATE              
 1949-05-16  176.21,                Close
 DATE                
 2019-09-20  22079.09)

となります。

トレンドフォロー戦略

データがダウンロードできたので、つぎはいよいよバックテストをする関数を作ります。

損切なしの単純タイプ

最初は損切しないタイプの戦略を行います。

def up_crossover_ma_two(tsd,window0,window9):
    y=pd.DataFrame(tsd).copy(9)
    y['pl']=0.0
    y['ma0']=tsd.Close.rolling(window0).mean().shift(1)
    y['ma9']=tsd.Close.rolling(window9).mean().shift(1)
    y['siz']=0.0
    y=y.dropna(how='any')#数値の無いものが1つでもあればその行を削除します。
    #init----------------------------------
    n=0
    buy=0
    sell=0
    buyF=0
    sellF=0
    size=0
    comm=0.0
    j=0
    for i in range(1,len(y)):
        c=y.Close.iloc[i]
        c0=y.Close.iloc[i-1]
        m0=y.ma0.iloc[i]
        m9=y.ma9.iloc[i]
        y.iloc[i,4]=size
        if buy!=0:
            y.iloc[i,1]=(c-c0)*size# pl
        if m0>m9:
            buyF=1
            sellF=0
        if m0<m9:
            sellF=1
            buyF=0
        if buyF==1 and buy==0:#entry long-position
            buy=c+c*comm*2
            y.iloc[i,1]=-c*comm*2
            if j==0:
                pl0=c
                j=1
                size=1
        if sellF==1 and buy!=0 and c>buy:#c>buyで利益だ出たときだけ利食います。
            buy=0
        buyF=0
        sellF=0
    return y

このプログラムを実行してみましょう。短いほうの移動平均を100日、長いほうの移動平均を400日にしてみます。

y=up_crossover_ma_two(tsd,100,400)
y.pl.cumsum().plot()
plt.show()

image.png

結果はいいのですが、これは偶然です。過去を分析するのは簡単です。過去のデータに移動平均の日数をうまくいくように適合すればよいわけですから。将来これでうまくいくという保証はありません。いろいろやってみてください。それがシステムトレードの本質です。

さて、このバックテストの欠点は何でしょうか?リスク管理が甘いということです。いったん利食いを行って、利益を確定した後に、価格が下がってくれればいいですが、当然上がる場合もあります。上がるとどうなりますか?一単位買えなくなります。そのことがこのバックテストでは考慮されていません。しかし、この程度の長期のクロスオーバーではそれもOKとしましょう。

しかし損切をしたり、もっと短い期間でトレードするときはもっと慎重にバックテストをするべきです。

利食いをして、つぎにエントリーしたときに、価格がその時よりも高ければ、持ち金では十分な資金がありません。その分析をしてみます。

def up_crossover_ma_two2(tsd,window0,window9):
    y=pd.DataFrame(tsd).copy(9)
    y['pl']=0.0
    y['ma0']=tsd.Close.rolling(window0).mean().shift(1)
    y['ma9']=tsd.Close.rolling(window9).mean().shift(1)
    y['siz']=0.0
    y=y.dropna(how='any')#数値の無いものが1つでもあればその行を削除します。
    #init----------------------------------
    n=0
    buy=0
    sell=0
    buyF=0
    sellF=0
    size=0
    comm=0.0
    j=0
    for i in range(1,len(y)):
        c=y.Close.iloc[i]
        c0=y.Close.iloc[i-1]
        m0=y.ma0.iloc[i]
        m9=y.ma9.iloc[i]
        y.iloc[i,4]=size
        if buy!=0:
            y.iloc[i,1]=(c-c0)*size# pl
        if m0>m9:
            buyF=1
            sellF=0
        if m0<m9:
            sellF=1
            buyF=0
        if buyF==1 and buy==0:#entry long-position
            buy=c+c*comm*2
            y.iloc[i,1]=-c*comm*2
            if j==0:
                pl0=c
                j=1
            cumpl=y.pl.cumsum().iloc[i]+pl0
            if cumpl>c:
                size=1
            else:
                size=cumpl/c
        if sellF==1 and buy!=0:# and c>buy:#c>buyで利益だ出たときだけ利食います。
            buy=0
        buyF=0
        sellF=0
    return y
y=up_crossover_ma_two2(tsd,100,400)
y.pl.cumsum().plot()
plt.show()

image.png

'''Python
y.siz.plot()
plt.show()
'''
image.png

なかなか厳しい状態にあります。

損切と短い期間での売買

多くの人はもっと短い期間で売買を繰り返す方法を好みます。また、損切を好みます。そうするといろいろな面で厄介なことが起きます。それは短期売買では利益を確定したり、損切をしたりした後に価格が下がってくれればいいですが、上がってしまうとⅠ単位買えなくなるという問題です。それを考慮したバックテストを紹介します。

def up_crossover_ma_two3(tsd,window0,window9):
    y=pd.DataFrame(tsd).copy(9)
    y['pl']=0.0
    y['ma0']=tsd.Close.rolling(window0).mean().shift(1)
    y['ma9']=tsd.Close.rolling(window9).mean().shift(1)
    y['siz']=0.0
    y=y.dropna(how='any')#数値の無いものが1つでもあればその行を削除します。
    #init----------------------------------
    n=0
    buy=0
    sell=0
    buyF=0
    sellF=0
    size=0
    comm=0.0
    j=0
    for i in range(1,len(y)):
        c=y.Close.iloc[i]
        c0=y.Close.iloc[i-1]
        m0=y.ma0.iloc[i]
        m9=y.ma9.iloc[i]
        y.iloc[i,4]=size
        if buy!=0:
            y.iloc[i,1]=(c-c0)*size# pl
        if m0>m9:
            buyF=1
            sellF=0
        if m0<m9:
            sellF=1
            buyF=0
        if buyF==1 and buy==0:#entry long-position
            buy=c+c*comm*2
            y.iloc[i,1]=-c*comm*2
            if j==0:
                pl0=c
                j=1
            cumpl=y.pl.cumsum().iloc[i]+pl0
            if cumpl>c:
                size=1
            else:
                size=cumpl/c
        if sellF==1 and buy!=0:# 利益が出なくてもエグジットします。
            buy=0
        buyF=0
        sellF=0
    return y

このプログラムを実際に回してみましょう。

y=up_crossover_ma_two2(tsd,100,400)
y.pl.cumsum().plot()
plt.show()

image.png

結果が少し違います。そこでsizの中身を見てみましょう。

y.siz.plot()

image.png

なるほどこの戦略では、sizが減ってしまっています。それはエグジットの後に価格が上がってしまったからです。利食いの後に価格が上がってしまうと、元の枚数を買い戻せなくなってしまいます。

バックテストの時期の影響

バックテストを行う期間によってそれぞれの戦略の結果が違ってきます。

tsd0=tsd.loc['1980':'1990']

for s in range(50,150,50):
    for l in range(150,400,100):
        y=up_crossover_ma_two2(tsd0,s,l)
        y.pl.cumsum().plot(label=str(s)+'-'+str(l))
plt.legend()
plt.show()

image.png

tsd0=tsd.loc['1990':'2000']

for s in range(50,150,50):
    for l in range(150,400,100):
        y=up_crossover_ma_two2(tsd0,s,l)
        y.pl.cumsum().plot(label=str(s)+'-'+str(l))
plt.legend()
plt.show()

image.png

tsd0=tsd.loc['2000':]

for s in range(50,150,50):
    for l in range(150,400,100):
        y=up_crossover_ma_two3(tsd0,s,l)
        y.pl.cumsum().plot(label=str(s)+'-'+str(l))
plt.legend()
plt.show()

image.png

バックテストの時期と短期トレード

短期売買では取引費用を含まなければいけませんが、以下のバックでストでは含んでいません。短期売買では取引費用の影響は絶大です。

tsd0=tsd.loc['1980':'1990']

for s in range(10,50,10):
    for l in range(50,100,25):
        y=up_crossover_ma_two3(tsd0,s,l)
        y.pl.cumsum().plot(label=str(s)+'-'+str(l))
plt.legend()
plt.show()

image.png

tsd0=tsd.loc['1990':'2000']

for s in range(10,50,10):
    for l in range(50,100,25):
        y=up_crossover_ma_two2(tsd0,s,l)
        y.pl.cumsum().plot(label=str(s)+'-'+str(l))
plt.legend()
plt.show()

image.png

tsd0=tsd.loc['2000':]

for s in range(10,50,10):
    for l in range(50,100,25):
        y=up_crossover_ma_two2(tsd0,s,l)
        y.pl.cumsum().plot(label=str(s)+'-'+str(l))
plt.legend()
plt.show()

image.png

いろいろ試してみて、システムトレードのセンスを養ってみてください。

符号検定の利用

def sign_test_trading(data, lookback=5, alpha=0.05):
    y=pd.DataFrame(tsd).copy(9)
    y['dif']=y.Close.diff()
    y['pl']=0.0
    y['siz']=0.0
    y=y.dropna(how='any')#数値の無いものが1つでもあればその行を削除します。
    #init----------------------------------
    n=0
    buy=0
    sell=0
    buyF=0
    sellF=0
    size=0
    comm=0.0
    j=0
    for i in range(lookback,len(y)):
        c=y.Close.iloc[i]
        c0=y.Close.iloc[i-1]
        y.iloc[i,3]=size
        if buy!=0:
            y.iloc[i,2]=(c-c0)*size# pl
        window = y["dif"].iloc[i-lookback:i]
        # ゼロを除外(r_t>0, r_t<0 のみを使う)
        pos_ret = (window > 0)
        neg_ret = (window < 0)
        n_pos = pos_ret.sum()
        n_neg = neg_ret.sum()
        n_eff = n_pos + n_neg
        if n_eff == 0:
            # すべてゼロなら何もしない
            continue 
        # --- 符号検定(右側検定:正の符号が多いか?)---
        res_pos = binomtest(k=n_pos, n=n_eff, p=0.5, alternative="greater")
        # --- 符号検定(左側検定:負の符号が多いか?)---
        res_neg = binomtest(k=n_neg, n=n_eff, p=0.5, alternative="greater")
        if res_pos.pvalue<alpha:
            buyF=1
            sellF=0
        else:
            sellF=1
            buyF=0
        if buyF==1 and buy==0:#entry long-position
            buy=c+c*comm*2
            y.iloc[i,1]=-c*comm*2
            if j==0:
                pl0=c
                j=1
            cumpl=y.pl.cumsum().iloc[i]+pl0
            if cumpl>c:
                size=1
            else:
                size=cumpl/c
        if sellF==1 and buy!=0:
            buy=0
        buyF=0
        sellF=0
    return y
comm=0.0
for s in range(50,300,50):
    y=sign_test_trading(tsd, s, alpha=0.05)
    cum_pl = y["pl"].cumsum()
    cum_pl.plot(label=str(s))
plt.legend()
plt.show()

image.png

# --- 3) バックテスト実行 ---
comm=0.0
y = sign_test_trading(tsd, lookback=200, alpha=0.05)

# 累積損益(価格ベースに換算するなら初期資産を掛ける)
cum_pl = y["pl"].cumsum()

# ベンチマーク(単純買い持ち)
#bh = tsd["Close"] / tsd["Close"].iloc[0]

# --- 4) プロット ---
plt.figure(figsize=(10, 5))
cum_pl.plot(label="Sign-test strategy")
#bh.plot(label="Buy & Hold (N225)")
plt.legend()
plt.title("NIKKEI225: Sign-test Trading vs Buy & Hold")
plt.xlabel("Date")
plt.ylabel("Equity (normalized)")
plt.grid(True)
plt.show()

image.png

ウィルコクソンの符号順位検定

from scipy.stats import wilcoxon
def wilcoxon_trading(data, lookback=5, alpha=0.05, min_nonzero=10):
    y=pd.DataFrame(tsd).copy(9)
    y['dif']=y.Close.diff()
    y['pl']=0.0
    y['siz']=0.0
    y["pos"]=0.0
    y=y.dropna(how='any')#数値の無いものが1つでもあればその行を削除します。
    #init----------------------------------
    n=0
    buy=0
    sell=0
    buyF=0
    sellF=0
    size=0
    comm=0.0
    j=0
    pos_col = y.columns.get_loc("pos")

    for i in range(lookback,len(y)):
        c=y.Close.iloc[i]
        c0=y.Close.iloc[i-1]
        y.iloc[i,3]=size
        if buy!=0:
            y.iloc[i,2]=(c-c0)*size# pl
        window = y["dif"].iloc[i-lookback:i]
        # 0 を除外(ウィルコクソン検定はゼロを特殊扱いするので)
        w = window[window != 0.0]

        # 有効サンプルが少なすぎる場合はスキップ
        if len(w) < min_nonzero:
            continue

        # ロング側の検定:H1: median > 0
        stat_long, p_long = wilcoxon(
            w,
            alternative="greater",   # median > 0
            zero_method="wilcox",    # ゼロは除外
            correction=False,
            mode="auto"
        )

        # ショート側の検定:H1: median < 0
        # これは -w に対して H1: median > 0 を検定するのと同値
        stat_short, p_short = wilcoxon(
            -w,
            alternative="greater",   # median(-w) > 0 <=> median(w) < 0
            zero_method="wilcox",
            correction=False,
            mode="auto"
        )

        pos = 0.0
        if (p_long < alpha) and (p_short >= alpha):
            pos = 1.0   # ロング
        #elif (p_short < alpha) and (p_long >= alpha):
        #    pos = -1.0  # ショート
        # 両方有意 or 両方非有意なら pos=0.0 のまま

        # 当日のポジションを代入(チェインドアサインメントを避ける)
        y.iat[i, pos_col] = pos
        if (p_long < alpha) and (p_short >= alpha):
            buyF=1
            sellF=0
        else:
            sellF=1
            buyF=0
        if buyF==1 and buy==0:#entry long-position
            buy=c+c*comm*2
            y.iloc[i,1]=-c*comm*2
            if j==0:
                pl0=c
                j=1
            cumpl=y.pl.cumsum().iloc[i]+pl0
            if cumpl>c:
                size=1
            else:
                size=cumpl/c
        if sellF==1 and buy!=0:
            buy=0
        buyF=0
        sellF=0
    return y

image.png

注意

本資料は学習用に作成されたものであり投資等における判断はご自身で行うようお願いいたします。

関連サイト

  • 特にシステムトレードに興味のある方

Jupyter notebookのインストール
Yahoo Finance USから株価をダウンロードしてみた
システムトレードってなに?
カルマンフィルター
システムトレードにおける対数の役割
経済データのダウンロード
グロスマン・ミラーモデル(翻訳)

  • 人工知能関連

誰でもわかるニューラルネットワーク:アプリのように動かす人工知能ーテンソルフロープレイグラウンド 
誰でもわかるニューラルネットワーク:正則化をテンソルフロープレイグラウンドで試してみた 

参考

「Python3ではじめるシステムトレード」(パンローリング)

「タートル流投資の魔術](徳間書店)
Pandas datareader (
https://pandas-datareader.readthedocs.io/en/latest/)

7
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?