54
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

【FXシストレ入門】初めてのバックテストをPythonで実行してみる【超初心者向け】

概要

最近、シストレ絶賛勉強中の @kazama1209 と申します。

先日、「Pythonで為替レートを取得しチャートとして可視化するまでの手順」というめちゃくちゃ初心者向けの記事を書いたのですが、あれから色々試行錯誤した結果、簡単なバックテストを実行できるようになったので今回はその辺について書いていきたいと思います。

バックテスト: システムトレードにおいて、売買ルールの有効性を検証するために過去のデータを用いて一定期間にどの程度のパフォーマンスが得られたかをシミュレーションする事。

対象読者

  • シストレに挑戦してみたい
  • でも何から始めれば良いかわからない(環境構築も含めて)
  • とりあえず手を動かして雰囲気を掴みたい

DockerでJupytar Notebookの環境構築をするところから始めるので、誰がやっても必ず同じ結果になる記事になっています。(ゆえに超初心者向け)

環境構築

  • Docker
  • Python3
  • Jupytar Notebook

Jupytar Notebook: Webブラウザ上でPythonを記述・実行できる統合開発環境。

各ディレクトリ&ファイルを作成

$ mkdir fx-backtesting
$ cd fx-backtesting
$ mkdir work
$ touch docker-compose.yml

最終的に次のような構成になっていればOK。

fx-backtesting
├─ work
├─ docker-compose.yml

docker-compose.ymlを編集

./docker-compose.yml
version: "3"
services:
  notebook:
    image: jupyter/datascience-notebook
    ports:
      - "8888:8888"
    environment:
      - JUPYTER_ENABLE_LAB=yes
    volumes:
      - ./work:/home/jovyan/work
    command: start-notebook.sh --NotebookApp.token=''

コンテナを起動

$ docker-compose up -d

localhost:8888 にアクセスし

スクリーンショット 2021-03-08 0.23.39.png

こんな感じの画面が表示されれば成功です。

ヒストリカルデータを取得

バックテストを行うにあたり、ある程度期間を持たせた過去のデータが必要になるので、

http://www.histdata.com

↑のサイトからダウンロードしておきましょう。

スクリーンショット 2021-03-21 19.44.16.png

今回はユーロドル(EUR/USD)の2020年分のデータを使用します。

zipファイルのダウンロードが終わったら、解凍して中身のCSVファイルを「work」ディレクトリ配下に起きましょう。

fx-backtesting
├─ work
  ├─ DAT_ASCII_EURUSD_M1_2020.csv
├─ docker-compose.yml

手動でポチポチやるのも良いですが、面倒なら下記のコマンドを叩けば一発でいけると思います。

$ unzip ~/Downloads/HISTDATA_COM_ASCII_EURUSD_M12020.zip DAT_ASCII_EURUSD_M1_2020.csv -d ./work

コードを実装

大体の準備が完了したので、そろそろコードを書いていきましょう。

スクリーンショット 2021-03-21 20.05.26.png

「work」ディレクトリへ入り、「Notebook」から「Python3」を選択。

スクリーンショット 2021-03-21 20.05.51.png

するとこんな感じでコードを記述できるようになるので、ここに色々と書いていきます。

データフレームを作成

import pandas as pd # データ解析用ライブラリ

# ヒストリカルデータが記載されているCSVファイルを読み込み
df = pd.read_csv('./DAT_ASCII_EURUSD_M1_2020.csv', sep=';',
                     names=('Time','Open','High','Low','Close', ''),
                     index_col='Time', parse_dates=True)
df

スクリーンショット 2021-03-21 20.12.26.png

こんな感じで2020年1月1日〜12月31日までの為替レートが取得できていれば成功です。

※コードの実行は上部バーの「▶︎」を押すとできます。

タイムフレームを変更

どの時間軸でトレードをするかというのは人によって好みだとは思いますが、僕の場合は1分足でトレードをする事はまず無いので1時間足に変更したいと思います。

ちなみに、単一の時間軸に限らず複数の時間軸から分析する手法を「マルチタイムフレーム分析」と呼ぶので覚えておくと良いかもしれません。

def make_mtf_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_1h = make_mtf_ohlc(df, '1H')
ohlc_1h

スクリーンショット 2021-03-21 20.23.43.png

引数に渡す「tf(タイムフレーム)」の値の例としては、以下の通り。

  • 'T': 分
  • 'H': 時間
  • 'D': 日
  • 'W': 週
  • 'M': 月

たとえば、1時間足のデータが欲しければ「'H'」もしくは「'1H'」と書けばOKですし、4時間足のデータが欲しければ「'4H'」と書きます。

チャートとして可視化

ここで一旦、先ほど作成したデータをチャートとして可視化してみましょう。

from matplotlib import pyplot as plt # データを可視化(チャートなどの形で表示)するためのライブラリ。
import seaborn as sns # 同上
sns.set()

fig = plt.figure(figsize = (10, 8))

ax = fig.add_subplot(1, 1, 1)
ax.set_title('EUR/USD')

sns.lineplot(x = ohlc_1h.index, y = ohlc_1h.High, ax = ax)
plt.tight_layout()

スクリーンショット 2021-03-21 20.33.29.png

良い感じに表示されましたね。

バックテストを実行

さて、いよいよバックテストのお時間です。

バックテストを行うためには、売買ルール(どんな条件の時に売買するか)や実際の取引タイミングなど色々細かな設定をしていかなければなりません。

中にはそれらを細かに自分で一から書いていく方もいるかもしれませんが、今回の趣旨はあくまでも「入門」という事もあり、感覚を掴むというのが主な目的という意味で素直に他の方が作ってくれたライブラリに頼る事にします。

Python製のバックテスト用ライブラリとしては、

  • Backtesting.py
  • Backtrader
  • PyAlgoTrade
  • pybacktest

といった多彩なものがリリースされているようですが、今回はその中でも比較的シンプルとされている「Backtesting.py」を使ってみたいと思います。

!pip install backtesting

スクリーンショット 2021-03-21 20.46.27.png

Backtesting.pyは標準ライブラリではないため、ここでインストールしておきます。

from backtesting import Backtest, Strategy
from backtesting.lib import crossover
from backtesting.test import SMA

class SmaCross(Strategy): # 今回はサンプルとして良く採用される単純移動平均線(SMA)の交差を売買ルールに。
    def init(self): # 初期設定(移動平均線などの値を決める)
        price = self.data.Close
        self.ma1 = self.I(SMA, price, 20) # 短期の移動平均線
        self.ma2 = self.I(SMA, price, 80) # 長期の移動平均線

    def next(self): # ヒストリカルデータの行ごとに呼び出される処理
        if crossover(self.ma1, self.ma2): # ma1がma2を上回った時(つまりゴールデンクロス)
            self.buy() # 買い
        elif crossover(self.ma2, self.ma1): # ma1がma2を下回った時(つまりデッドクロス)
            self.sell() # 売り

bt = Backtest(ohlc_1h, SmaCross, cash=500, commission=0, exclusive_orders=True)

# バックテスト実行に渡せる引数
# class Backtest(data, strategy, *, cash=10000, commission=0.0, margin=1.0, trade_on_close=False, exclusive_orders=False)
# data: ヒストリカルデータ
# strategy: Strategyを継承した独自のストラテジー(今回で言えばSmaCross)
# cash: 資金(今回は500ドルで試してみた)
# commission: 手数料(今回は簡略化のために0)
# margin: 資金に対する取引額の割合。たとえば、資金500ドルでmargin1.0なら500ドル全額で取引するし、0.5なら半分の250ドルで取引する。
# trade_on_close: 現在の足の終値で取引するかどうか(Falseなら次の足の始値で取引)
# exclusive_orders: 新規注文時に前の注文をクローズするかどうか

stats = bt.run() # バックテストを実行
print(stats) # バックテストの結果を表示

スクリーンショット 2021-03-21 20.58.04.png

今回のバックテストにおける売買ルールは、最もメジャーといっても過言ではないゴールデンクロスとデッドクロスにしました。

ゴールデンクロス: 長期の移動平均線を、短期の移動平均線が下から上に突き抜ける現象。
デッドクロス: 長期の移動平均線を、短期の移動平均線が上から下に突き抜ける現象。

goldencross.png

引用元: https://www.oanda.jp/lab-education/dictionary/goldencross/

移動平均線というのはとある一定期間における平均値を線で結んだものであるため、短期の移動平均線が長期の移動平均線と交差する瞬間というのは、トレンドの転換を表す可能性があるというわけですね。

必ずしも例のように上手く決まるわけではありませんが、多くの人が参考にしている現象という事でここでは一旦採用してみる事にします。

短期移動平均線と長期移動平均線の設定値については人それぞれ好みもあるでしょうが、僕はそれぞれ20・80を設定しました。

バックテストの結果、次のような集計値が返ってくると思います。

Start                     2020-01-01 17:00:00 # 開始日時
End                       2020-12-31 16:00:00 # 終了日時
Duration                    364 days 23:00:00 # 期間
Exposure Time [%]                   98.491172 # ポジションを持っていた期間の割合
Equity Final [$]                    587.36774 # 最終的な資金
Equity Peak [$]                     601.25101 # ピーク時の資金
Return [%]                          17.473548 # 利益率
Buy & Hold Return [%]                8.926103
Return (Ann.) [%]                   13.796855
Volatility (Ann.) [%]                7.517942
Sharpe Ratio                          1.83519
Sortino Ratio                        3.330257
Calmar Ratio                         2.209586
Max. Drawdown [%]                   -6.244091 # 最大ドローダウン(最大資金からの損失率)
Avg. Drawdown [%]                   -0.350062 # 平均ドローダウン
Max. Drawdown Duration      209 days 14:00:00
Avg. Drawdown Duration        3 days 03:00:00
# Trades                                   76 # トレード回数
Win Rate [%]                        44.736842 # 勝率
Best Trade [%]                       4.640433 # 1回の取引で得た利益の最大値
Worst Trade [%]                     -0.911485 # 1回の取引で被った損失の最大値
Avg. Trade [%]                       0.212387 # 損益の平均値
Max. Trade Duration          18 days 07:00:00
Avg. Trade Duration           4 days 18:00:00
Profit Factor                        2.176097
Expectancy [%]                       0.217177
SQN                                  1.839067
_strategy                            SmaCross
_equity_curve                             ...
_trades                       Size  EntryB...

重要と思われる項目にコメントをつけておきました。その他の部分はより高度な分析を行う際に必要な感じなので、気になる方は適宜調べてみてください。(ぶっちゃけ僕も良くわからない部分が多いです。)

ざっくり言うと、今回の戦略(ゴールデンクロス時に買い、デッドクロス時に売り)を採用して2020年のユーロドル相場で勝負した場合、年間で76回のトレードを行い、勝率は44%、最終的な利益率は+17%くらいが見込めたというわけですね。

なお、今回のバックテストを可視化する事も可能です。

スクリーンショット 2021-03-21 21.41.10.png

  • 1段目
    • 資金の推移
  • 2段目
    • 損益の分布
  • 3段目
    • 実際にトレードが発生した場所など

これらを上手く組み合わせていく事で、より勝率の高い戦略が考えられるのではないでしょうか。

あとがき

以上、初めてのバックテストを行ってみました。採用した売買ルールはいたってシンプルなものですし、試行回数が少ない(2020年のユーロドル相場でしか試していない)ため、実際の相場で通用するかどうかと言われれば微妙だと思います。

しかしながら、バックテストがどういうものなのか、ちょっとしたコツは掴めたのでよしとしましょう。

今後はゴールデンクロス・デッドクロス以外の売買ルールも試してみたいですし、「Backtesting.py」以外のライブラリも吟味してみたいと思います。

あくまで初心者向けの記事になっているので、一読した後は各々自分なりの手法を考えてみてください。今回はFXで例えていますが、仮想通貨でも大体同じような考えでいけるはず。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
54
Help us understand the problem. What are the problem?