PythonでFXシストレのバックテスト(1)

# はじめに
Pythonでシストレのバックテストをするライブラリってたくさんあるのですが、MetaTraderから入った人にとってはわかりにくいので、Pythonの練習がてらバックテストのためのコードを書いてみました。

[Pythonでシストレのバックテスト](http://forex.toyolab.com/?p=631)

ただ、最初のバージョンは、まず動くことを第一に書いたので、結構無駄があったり、実行速度が遅かったりしたので、今回、ちょっと改良してみました。

# FXヒストリカルデータの取得
株価だと、Yahoo!とかから直接ダウンロードして使えるものも多いのですが、FXだと5分足とか15分足とか複数のタイムフレームのデータを使うこともあるので、基本のデータとして1分足データが欲しいところです。

そうなると、データも大きいので、予めダウンロードしたデータを読み込む方が都合がいいかと思います。ここではサンプルデータとして以下のサイトからダウンロードしておきます。

[HistData.com](http://www.histdata.com/)

同じデータでもいくつかの形式があるのですが、pandasの関数を使う関係で、Generic ASCIIの形式のデータをダウンロードしておきます。

では、ダウンロードしたEUR/USDの2015年分の1分足データ'DAT_ASCII_EURUSD_M1_2015.csv'をpandasの関数で読み込んでみます。ただ、このままだと週明けの相場が日曜日から始まってしまうので、7時間だけ遅らせて、月曜日の0時から始めるようにします。

````py
import numpy as np
import pandas as pd

dataM1 = pd.read_csv('DAT_ASCII_EURUSD_M1_2015.csv', sep=';',
                     names=('Time','Open','High','Low','Close', ''),
                     index_col='Time', parse_dates=True)
dataM1.index += pd.offsets.Hour(7) #7時間のオフセット
````

# 任意のタイムフレームデータの作成
次に1分足のデータから任意のタイムフレームのデータを作成します。ここでもpandasの`resample()`, `ohlc()`という関数を使えば簡単にできます。

````py
# dfのデータからtfで指定するタイムフレームの4本足データを作成する関数
def TF_ohlc(df, tf):
    x = df.resample(tf).ohlc()
    O = x['Open']['open']
    H = x['High']['high']
    L = x['Low']['low']
    C = x['Close']['close']
    ret = pd.DataFrame({'Open': O, 'High': H, 'Low': L, 'Close': C},
                       columns=['Open','High','Low','Close'])
    return ret.dropna()

ohlc = TF_ohlc(dataM1, 'H') #１時間足データの作成
`````

`tf`に指定するキーワードは、

* 分 - `'T'`
* 時 - `'H'`
* 日 - `'D'`
* 週 - `'W'`
* 月 - `'M'`

で、15分の場合、`'15T'`と書けばいいです。これで、何分足のデータでも作ることができます。ここでは、1時間足のデータを作ってみました。

# テクニカル指標の作成

シストレの売買ルールで使うテクニカル指標は[GitHub](https://github.com/toyolab/MT5IndicatorsPy)に作ってあります。indicators.pyだけ取ってくれば使えます。ソースコードは省略しますが、内部の繰り返しでpandas使わないようにしたり、Numbaを使ったりしたので、極端に時間のかかる関数はなくなったと思います。~~ただし、アルゴリズムの関係でパラボリックSARは多少時間がかかります。~~

例えば、10バーの移動平均と30バーの移動平均は以下のように書けます。

````py
import indicators as ind #indicators.pyのインポート
FastMA = ind.iMA(ohlc, 10) #短期移動平均
SlowMA = ind.iMA(ohlc, 30) #長期移動平均
````

# テクニカル指標の表示

作成したテクニカル指標をチャートに表示させるのに、pandasのplot関数でもいいのですが、拡大縮小したいこともあるので、相場関係のサイトでよく見かける[HighCharts](http://www.highcharts.com/)に表示させるPythonのライブラリを使ってみます。

ここでは、簡単にインストールできた[pandas-highcharts](https://github.com/gtnx/pandas-highcharts)を使ってみます。

インストールは、

````
pip install pandas-highcharts
````

でOKです。FXの終値と２本の移動平均を表示させるコードは以下のようになります。

````py
from pandas_highcharts.display import display_charts
df = pd.DataFrame({'Close': ohlc['Close'], 'FastMA': FastMA, 'SlowMA': SlowMA})
display_charts(df, chart_type="stock", title="MA cross", figsize=(640,480), grid=True)
````
これを実行するとこんなチャートが表示されます。

![chart_y.png](https://qiita-image-store.s3.amazonaws.com/0/131169/db0a3afd-c2f5-fcd2-d84a-46320610148a.png)

１年分を表示させると移動平均がほとんど重なって見えないので、適当にズームすると、こんな感じになり、移動平均の違いが見えるようになります。

![chart_m.png](https://qiita-image-store.s3.amazonaws.com/0/131169/1c6f7853-aa1c-7b3e-3f02-270a192edffb.png)

なかなか便利です。

# 移動平均交差システムの売買ルール
売買システムの一例として、定番の移動平均の交差システムを取り上げます。売買ルールは、

* 買いシグナル - 短期移動平均線が長期移動平均線を上抜ける
* 売りシグナル - 短期移動平均線が長期移動平均線を下抜ける

という単純なものです。このシグナルはエントリーのシグナルで、ポジションを決済するためのエグジットシグナルは、買いエグジットは売りエントリーと同じ、売りエグジットは買いエントリー同じものとします。いわゆる途転売買です。

````py
#買いエントリーシグナル
BuyEntry = ((FastMA > SlowMA) & (FastMA.shift() <= SlowMA.shift())).values
#売りエントリーシグナル
SellEntry = ((FastMA < SlowMA) & (FastMA.shift() >= SlowMA.shift())).values
#買いエグジットシグナル
BuyExit = SellEntry.copy()
#売りエグジットシグナル
SellExit = BuyEntry.copy()
````

ここでは、それぞれのシグナルがnumpyのbool型の配列になっています。普通に考えると、時系列データを回しながらシグナルの判別をするところでしょうが、Pythonの場合、配列でまとめて処理した方が速いので、シグナルを配列に入れておきます。

# バックテストの実行
いよいよバックテストを行います。ヒストリカルデータと、上記の売買シグナルを入力して、実際に売買した価格と損益を時系列データとして出力させます。

````py
def Backtest(ohlc, BuyEntry, SellEntry, BuyExit, SellExit, lots=0.1, spread=2):
    Open = ohlc['Open'].values #始値
    Point = 0.0001 #1pipの値
    if(Open[0] > 50): Point = 0.01 #クロス円の1pipの値
    Spread = spread*Point #スプレッド
    Lots = lots*100000 #実際の売買量
    N = len(ohlc) #FXデータのサイズ
    BuyExit[N-2] = SellExit[N-2] = True #最後に強制エグジット
    BuyPrice = SellPrice = 0.0 # 売買価格
    
    LongTrade = np.zeros(N) # 買いトレード情報
    ShortTrade = np.zeros(N) # 売りトレード情報
    
    LongPL = np.zeros(N) # 買いポジションの損益
    ShortPL = np.zeros(N) # 売りポジションの損益

    for i in range(1,N):
        if BuyEntry[i-1] and BuyPrice == 0: #買いエントリーシグナル
            BuyPrice = Open[i]+Spread
            LongTrade[i] = BuyPrice #買いポジションオープン
        elif BuyExit[i-1] and BuyPrice != 0: #買いエグジットシグナル
            ClosePrice = Open[i]
            LongTrade[i] = -ClosePrice #買いポジションクローズ
            LongPL[i] = (ClosePrice-BuyPrice)*Lots #損益確定
            BuyPrice = 0

        if SellEntry[i-1] and SellPrice == 0: #売りエントリーシグナル
            SellPrice = Open[i]
            ShortTrade[i] = SellPrice #売りポジションオープン
        elif SellExit[i-1] and SellPrice != 0: #売りエグジットシグナル
            ClosePrice = Open[i]+Spread
            ShortTrade[i] = -ClosePrice #売りポジションクローズ
            ShortPL[i] = (SellPrice-ClosePrice)*Lots #損益確定
            SellPrice = 0

    return pd.DataFrame({'Long':LongTrade, 'Short':ShortTrade}, index=ohlc.index),\
            pd.DataFrame({'Long':LongPL, 'Short':ShortPL}, index=ohlc.index)
        
Trade, PL = Backtest(ohlc, BuyEntry, SellEntry, BuyExit, SellExit)
````

前のバージョンでは、この部分を何段階かに分けていたのですが、今回はまとめてみました。本来ならシグナルだけでトレードできればいいのですが、ポジションがあってもシグナルが出るケースもあるので、ポジションの有無もチェックする形にしました。

この関数の出力のうち、`Trade`には、買いと売りそれぞれに、売買価格をプラスの値、決済価格をマイナスの値で格納してあります。`PL`には、決済時点での実現損益が格納してあります。

# 売買システムの評価
`Trade`と`PL`の情報があれば、売買システムのだいたいの評価ができます。例えばこんな感じです。

````py
def BacktestReport(Trade, PL):
    LongPL = PL['Long']
    LongTrades = np.count_nonzero(Trade['Long'])//2
    LongWinTrades = np.count_nonzero(LongPL.clip_lower(0))
    LongLoseTrades = np.count_nonzero(LongPL.clip_upper(0))
    print('買いトレード数 =', LongTrades)
    print('勝トレード数 =', LongWinTrades)
    print('最大勝トレード =', LongPL.max())
    print('平均勝トレード =', round(LongPL.clip_lower(0).sum()/LongWinTrades, 2))
    print('負トレード数 =', LongLoseTrades)
    print('最大負トレード =', LongPL.min())
    print('平均負トレード =', round(LongPL.clip_upper(0).sum()/LongLoseTrades, 2))
    print('勝率 =', round(LongWinTrades/LongTrades*100, 2), '%\n')

    ShortPL = PL['Short']
    ShortTrades = np.count_nonzero(Trade['Short'])//2
    ShortWinTrades = np.count_nonzero(ShortPL.clip_lower(0))
    ShortLoseTrades = np.count_nonzero(ShortPL.clip_upper(0))
    print('売りトレード数 =', ShortTrades)
    print('勝トレード数 =', ShortWinTrades)
    print('最大勝トレード =', ShortPL.max())
    print('平均勝トレード =', round(ShortPL.clip_lower(0).sum()/ShortWinTrades, 2))
    print('負トレード数 =', ShortLoseTrades)
    print('最大負トレード =', ShortPL.min())
    print('平均負トレード =', round(ShortPL.clip_upper(0).sum()/ShortLoseTrades, 2))
    print('勝率 =', round(ShortWinTrades/ShortTrades*100, 2), '%\n')

    Trades = LongTrades + ShortTrades
    WinTrades = LongWinTrades+ShortWinTrades
    LoseTrades = LongLoseTrades+ShortLoseTrades
    print('総トレード数 =', Trades)
    print('勝トレード数 =', WinTrades)
    print('最大勝トレード =', max(LongPL.max(), ShortPL.max()))
    print('平均勝トレード =', round((LongPL.clip_lower(0).sum()+ShortPL.clip_lower(0).sum())/WinTrades, 2))
    print('負トレード数 =', LoseTrades)
    print('最大負トレード =', min(LongPL.min(), ShortPL.min()))
    print('平均負トレード =', round((LongPL.clip_upper(0).sum()+ShortPL.clip_upper(0).sum())/LoseTrades, 2))
    print('勝率 =', round(WinTrades/Trades*100, 2), '%\n')

    GrossProfit = LongPL.clip_lower(0).sum()+ShortPL.clip_lower(0).sum()
    GrossLoss = LongPL.clip_upper(0).sum()+ShortPL.clip_upper(0).sum()
    Profit = GrossProfit+GrossLoss
    Equity = (LongPL+ShortPL).cumsum()
    MDD = (Equity.cummax()-Equity).max()
    print('総利益 =', round(GrossProfit, 2))
    print('総損失 =', round(GrossLoss, 2))
    print('総損益 =', round(Profit, 2))
    print('プロフィットファクター =', round(-GrossProfit/GrossLoss, 2))
    print('平均損益 =', round(Profit/Trades, 2))
    print('最大ドローダウン =', round(MDD, 2))
    print('リカバリーファクター =', round(Profit/MDD, 2))
    return Equity
Equity = BacktestReport(Trade, PL)
````

````
買いトレード数 = 113
勝トレード数 = 39
最大勝トレード = 440.4
平均勝トレード = 82.57
負トレード数 = 74
最大負トレード = -169.4
平均負トレード = -43.89
勝率 = 34.51 %

売りトレード数 = 113
勝トレード数 = 49
最大勝トレード = 327.6
平均勝トレード = 78.71
負トレード数 = 64
最大負トレード = -238.5
平均負トレード = -43.85
勝率 = 43.36 %

総トレード数 = 226
勝トレード数 = 88
最大勝トレード = 440.4
平均勝トレード = 80.42
負トレード数 = 138
最大負トレード = -238.5
平均負トレード = -43.87
勝率 = 38.94 %

総利益 = 7077.0
総損失 = -6054.4
総損益 = 1022.6
プロフィットファクター = 1.17
平均損益 = 4.52
最大ドローダウン = 1125.4
リカバリーファクター = 0.91
````

ここでは、各項目を表示させていますが、機械学習や最適化を行う場合には、評価値のみを計算させるようにすればよいでしょう。

システムの評価として資産曲線を見たい場合も多いでしょう。上記の関数では、`Equity`として資産曲線を出力させているので、初期資産を加算してグラフにすると以下のようになります。

````py
Initial = 10000 # 初期資産
display_charts(pd.DataFrame({'Equity':Equity+Initial}), chart_type="stock", title="資産曲線", figsize=(640,480), grid=True)
````
![equity.png](https://qiita-image-store.s3.amazonaws.com/0/131169/6c6e6c1a-37cc-b54b-32ec-38a180c8c964.png)

# トレードチャートの表示

最後に、チャート上のどこで売買したかを表示させてみます。売買したポイントのみを表示させてもいいのですが、HighChartsでの方法がわからないので、ポジションをオープンしたポイントとクローズしたポイントをラインで結んでみます。

以下のようなコードで`Trade`情報からラインに変換してみます。

````py
def PositionLine(trade):
    PosPeriod = 0 #ポジションの期間
    Position = False #ポジションの有無
    Line = trade.copy()
    for i in range(len(Line)):
        if trade[i] > 0: Position = True 
        elif Position: PosPeriod += 1 # ポジションの期間をカウント
        if trade[i] < 0:
            if PosPeriod > 0:
                Line[i] = -trade[i]
                diff = (Line[i]-Line[i-PosPeriod])/PosPeriod
                for j in range(i-1, i-PosPeriod, -1):
                    Line[j] = Line[j+1]-diff # ポジションの期間を補間
                PosPeriod = 0
                Position = False
        if trade[i] == 0 and not Position: Line[i] = 'NaN'
    return Line

df = pd.DataFrame({'Open': ohlc['Open'],
                   'Long': PositionLine(Trade['Long'].values),
                   'Short': PositionLine(Trade['Short'].values)})
display_charts(df, chart_type="stock", title="トレードチャート", figsize=(640,480), grid=True)
````
同じようにHighChartsに表示されるので、適当にズームしてみます。

![trade.png](https://qiita-image-store.s3.amazonaws.com/0/131169/8acb23aa-89a2-cee4-9da7-3b60ecd73e2e.png)

途転売買のシステムなので、LongポジションとShortポジションが交互に出てくることがわかります。

# まとめ
売買ルールがテクニカル指標だけで記述できるようなシステムのバックテストを行うコードをPythonで書いてみました。PythonからHighChartsにグラフ表示できるライブラリはなかなか便利でした。まだまだ改善の余地はあるので、気が向いたら続きを書きたいと思います。

続きはこちらです。
[PythonでFXシストレのバックテスト(2)](http://qiita.com/toyolab/items/8fe0f05cc28af29a9ad9)

なお、本記事で掲載したコードは以下にアップロードしてあります。
[MT5IndicatorsPy/EA_sample.ipynb](https://nbviewer.jupyter.org/github/toyolab/MT5IndicatorsPy/blob/master/EA_sample.ipynb
)