Pythonで「REST API」を利用しバックテストを実行する方法について解説
PythonでREST API(以下、API)を利用して、FXトレードのバックテストを実行する方法について解説します。
自動売買を行う際、考案したトレード戦略をいきなり本番環境で運用するのは高リスクです。
バックテストを実施して過去相場における運用成績をシミュレーションしておくと、本番環境で運用する前にリスクをある程度把握する事ができます。
バックテストの流れ
まずはバックテストの流れについて確認しておきましょう。
一例として、次のような流れが考えられます。
- 1.為替データの準備
- 2.トレード戦略の実装
- 3.バックテストの実装
- 4.結果の確認
まずは為替データの準備です。
そのデータを使ってトレード戦略をプログラムで実装してバックテストを行い、結果を確認します。
結果が期待通りにならない場合は、トレード戦略そのものを見直したり、パラメーターの調整などを行い、結果の改善を試みます。
満足できる結果が得られたら、次のフェーズ(本番環境やデモ環境で運用するなど)に移ります。
本記事では、OANDAのAPIを使って為替データを用意し、Pythonライブラリのbacktestingを使ってバックテストを実行していきます。
backtesting: バックテスト用のPythonライブラリ
Pythonライブラリのbacktestingを利用すると、株価やFXなどのバックテストを容易に実装する事ができます。
サンプルコード
最初にbacktestingによるバックテストの実行結果を確認しておきましょう。
次のコードでは、Googleの株価データを使用して、移動平均線(10日と20日)のゴールデンクロスとデッドクロスを使ってトレードした結果をシミュレーションしています。
backtestingで紹介されているサンプルコードと同じものです。
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
from backtesting.test import SMA, GOOG
class SmaCross(Strategy):
n1 = 10
n2 = 20
def init(self):
close = self.data.Close
self.sma1 = self.I(SMA, close, self.n1)
self.sma2 = self.I(SMA, close, self.n2)
def next(self):
if crossover(self.sma1, self.sma2):
self.buy()
elif crossover(self.sma2, self.sma1):
self.sell()
bt = Backtest(GOOG, SmaCross,
cash=10000, commission=.002,
exclusive_orders=True)
output = bt.run()
上記のコードでバックテストを実行する事ができます。
SmaCrossというクラスの中に、initとnextがあります。
initで利用するインディケーターを定義し、nextで売買条件を指定します。
backtesting.Backtestでバックテストのクラスを定義して、runで実行します。
クラスを定義する際には、株価データ(GOOG)とトレード戦略(SmaCross)を指定し、さらに現金(cash)や手数料(comission)などの設定も可能です。
結果の概要を表示
outputを表示すると、バックテストの結果の概要を確認することができます。
print(output)
Returnが最終損益です。
その他にも、期間中ホールドしていた場合の損益(Buy & Hold Return)や平均損益(Avg. Trade)や勝率(Win Rate)、最大利益(Best Trade)や最大損失(Worst Trade)、平均取引期間(Avg. Trade Duration)、取引回数(# Trades)など、さまざまな情報がまとめられています。
トレード履歴を可視化
さらにトレード履歴をチャートで可視化することも可能です。
plotで表示することができます。
bt.plot()
こちらのチャートは拡大・縮小したり、ホバーすると該当のデータをインタラクティブに確認できたりします。
トレード期間や損益状況などを期間別に確認する事ができます。
この結果を踏まえて、パラメータの調整や売買条件を追加するなどしていけば、戦略を改善する事ができます。
FXのトレード戦略を実装する
バックテストの流れやライブラリについて確認できましたので、ここからはOANDAの為替データを使って、バックテストを実装していきます。
ここでは次の2つのトレード戦略でバックテストを行います。
- 1.トレード戦略1: 移動平均線を使ったトレード戦略
- 2.トレード戦略2: 平均足と移動平均線を使ったトレード戦略
為替データの準備
まずは為替データの準備です。
次のコードでは、APIを使ってドル円の日足データを過去1000日分取得しています。
取得データはJson形式になっているので、DataFrameに変換しています。
コードの詳細についてはPythonで「REST API」から為替データを取得する方法について解説をご参照ください。
import oandapyV20.endpoints.instruments as instruments
from oandapyV20 import API
import pandas as pd
account_id = "アカウントIDを設定してください"
access_token = "APIトークンを設定してください"
# パラメータの設定
params = {
"granularity": "D", # ローソク足の間隔
"count": 1000, # 取得する足数
"price": "M", # B: Bid, A:Ask, M:Mid
}
# 通貨ペアの設定
instrument = "USD_JPY"
# API用のクラスを定義
client = API(access_token=access_token, environment="practice")
# 為替データを取得
r = instruments.InstrumentsCandles(instrument=instrument, params=params)
client.request(r)
# 為替データを格納するためのリストを定義
price_data = []
# データを1つずつ取り出してprice_dataに格納する
for row in r.response["candles"]:
# 辞書を定義
data = {}
# dataに格納する
data["datetime"] = row["time"]
data.update(row["mid"])
data["Volume"] = row["volume"]
# price_dataに追加する
price_data.append(data)
# DataFrameに変換する
df = pd.DataFrame(price_data)
# カラム名の定義
columns = {
"o": "Open",
"h": "High",
"l": "Low",
"c": "Close"
}
# カラム名の変更
df.rename(columns=columns, inplace=True)
# datetimeの型変換
datetime_format = "%Y-%m-%dT%H:%M:%S.000000000Z"
df["datetime"] = pd.to_datetime(df["datetime"], format=datetime_format)
# datatimeをindexに指定
df.set_index("datetime", inplace=True)
# 数値データをfloatに変換
df = df.astype(float)
# 直近5日分のみ表示
df.tail()
paramsのgranualityでローソク足の間隔、countで取得するデータ数、そしてinstrumentsで通貨ペアを設定できます。
これで為替データの準備ができました。
参考記事: Pythonで「REST API」から為替データを取得する方法について解説
トレード戦略1: 移動平均線を使ったトレード戦略
まずはサンプルコードと同様の、移動平均線を使ったトレード戦略でバックテストを実装してみます。
先ほどのサンプルコードの株価データ(GOOG)を、用意した為替データ(df)に変更すれば実装できます。
Cashを1万円とし、ComissionはFXではSpreadに該当しますが、ここでは0.0003としました。
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
from backtesting.test import SMA, GOOG
class SmaCross(Strategy):
n1 = 10
n2 = 20
def init(self):
close = self.data.Close
self.sma1 = self.I(SMA, close, self.n1)
self.sma2 = self.I(SMA, close, self.n2)
def next(self):
if crossover(self.sma1, self.sma2):
self.buy()
elif crossover(self.sma2, self.sma1):
self.sell()
bt = Backtest(df, SmaCross,
cash=10000, commission=.0005,
exclusive_orders=True)
output = bt.run()
print(output)
最終損益は約7%のマイナスになってしまいました。
チャートでも結果を確認してみましょう。
bt.plot()
後半は比較的利益をあげる事ができています。
問題は前半の2019年1月〜2020年半ばあたりまでの期間です。
移動平均線によるゴールデンクロスやデッドクロスを活用した戦略で利益を上げられるのは、はっきりとしたトレンドが形成されている場合に限ります。
レンジ相場で利益をあげることは難しいです。
実際、前半のレンジ相場では損失が大きく膨らんでいきます。
これを改善するためには、トレンドの方向感によって戦略をうまく切り替える必要があります。
明確なトレンドが発生していない場合は、取引を行わないのも有効です。
パラメータの最適化
上記の戦略では移動平均線の期間を10日と20日にしました。
この数字をうまく調整する事ができればパフォーマンスの改善が期待できます。
この際、backtestingにはパラメータを最適化するための関数(optimize)が用意されています。
次のコードでは、短期と長期の移動平均線の期間(n1とn2)を5〜100日の5日刻みで、バックテストを実行しています。
全ての組み合わせを計算して、最もパフォーマンスの良い結果を返します。
bt.optimize(n1=range(5, 100, 5), n2=(5, 100, 5))
最終損益は+32.3792%とかなり改善しました。
チャートも確認します。
bt.plot()
チャートから、設定された移動平均線の期間は30日と100日である事が確認できます。
特に後半の上昇トレンドでは大きな利益をあげる事ができています。
このように、optimizeの機能を活用すれば、最適なパラメータを簡単に見つけ出す事ができます。
トレード戦略2: 平均足と移動平均線を使ったトレード戦略
次に少し異なるトレード戦略を実装してみましょう。
こちらの記事を参考にして、トレード戦略を考えてみました。
参考記事: 平均足と単純移動平均線を利用したスイングトレードのアイデア
考案したトレード戦略をまとめると次のようになります。
- 1.取引対象
- ○ 通貨ペア:ドル円
- ○ 間隔:日足
- 2.使用するテクニカル指標
- ○ 30日移動平均線
- ○ 平均足
- 3.エントリー条件(全ての条件を満たしたらロングでエントリーする)
- ○ 30日移動平均線が上向いている
- ○ 高値が30日移動平均線を上回った状態で、平均足が陰線から陽線に切り替わる
- 4.イグジット条件(いずれかの条件を満たしたら売却する)
- ○ 逆指値(買いの時、直近の陰線の安値を損切りラインに設定する)
- ○ 平均足が陽線から陰線に切り替わる
ドル円の日足データを対象に、移動平均線でトレンドを把握します。
上昇トレンドで、なおかつ価格が移動平均線を上回っているとき、平均足が陰線から陽線に切り替わったタイミングを押し目と判断してロングでエントリーします。
この際、直近の陰線の安値を損切りラインに設定します。
イグジットのタイミングは、逆指値で約定する、あるいは平均足が陽線から陰線に切り替わるときです。
売買価格はシグナル発生翌日の平均足の始値とします。
これをPythonを使って実装してみます。
ローソク足を平均足に変換する
まず、平均足のデータを用意します。
これはローソク足データから変換して作成することができます。
次のコードでは、APIで取得した為替データを平均足に変換しています。
# dfのデータをdf_meanにコピー
df_mean = df.copy()
# 平均足の終値を計算
df_mean["Close"] = df[["Open", "High", "Low", "Close"]].mean(axis=1)
# 平均足の始値を計算
# 1日目用:前日のローソク足の始値・高値・安値・終値の平均値を計算
df_mean["Open"] = df[["Open", "High", "Low", "Close"]].shift().mean(axis=1)
# 2日目以降用:前日の平均足の始値と終値の平均値を計算
price_open = []
for i in range(len(df_mean)):
if i == 0:
price_open.append(np.nan)
else:
price_open.append(np.nanmean([price_open[-1], df_mean.iloc[i-1]["Close"]]))
df_mean["Open"] = price_open
# 冒頭5行を表示
df_mean.head()
ローソク足を平均足に変換する方法については、こちらの記事を参考にしました。
高値と安値は同じ値を使います。
始値は、前日の平均足の始値と終値の平均値です。
終値はローソク足の始値、終値、高値、安値の平均値になります。
参考記事: ローソク足を改良した平均足
テクニカル指標データの準備
次にテクニカル指標のデータを準備します。
平均足の準備はすでに完了しているので、その他で必要なデータを用意します。
- 1.移動平均線(ここでは30日移動平均線を使う)
- 2.トレンド(移動平均線の傾きで判断する)
- 3.陽線と陰線の反転
次のコードで、これらのデータを用意しています。
import numpy as np
# 陽線と陰線を判定 陽線は1、陰線は2
df_mean["Line"] = np.where(df_mean["Close"] - df_mean["Open"]>0, 1, 0)
# 直前の陽線・陰線と比較 1なら陰線から陽線へ反転 -1なら陽線から陰線へ反転を示す
df_mean["Line"] = df_mean["Line"].diff().fillna(0)
# 30日移動平均線
df_mean["SMA30"] = df["Close"].rolling(window=30).mean()
# 30日移動平均線の傾きを計算してdf_meanに追加
df_mean["Trend"] = df["SMA30"].diff()
# 直近5日間のデータを表示
df_mean.tail()
Line、Trend、SMA30のカラムが追加されました。
トレンドの判定
次にトレンドの判定方法を考えます。
Trendカラムの数値がプラスであれば移動平均線は上向いていることが確認できます。
ただし、この数値が小さいとレンジ相場の可能性が高くなり、相場の方向感が安定しません。
したがって、明確なトレンドと判断するためには、ある程度大きな数値である必要があります。
ここでは、Trendカラムの各値の絶対値を計算して、その平均値の半分の値を基準としました。
これよりもTrendの数値が大きければ、明確な上昇トレンドと判定します。
trend_threshold = df_mean["Trend"].abs().mean() / 2
print(trend_threshold)
バックテストを実装
必要な準備が整ったので、バックテストを実行していきます。
今回のように、オリジナルのトレード戦略でバックテストを実行する際には、関数を別途用意する必要があります。
self.Iにインディケーターを定義しておくと、結果のチャートに表示されるので便利です。
self.Iを指定する際には、self.I(関数, 引数)の形にする必要があるので、便宜的にIndicatorという関数を用意しました。
from backtesting import Backtest, Strategy
from backtesting.test import SMA
trend_threshold = df_mean["Trend"].abs().mean() / 2
def Indicator(data):
return pd.Series(data)
class System(Strategy):
n = 30
def init(self):
close = self.data.Close
self.sma = self.I(Indicator, self.data.SMA30)
self.line = self.I(Indicator, self.data.Line)
self.trend = self.I(Indicator, self.data.Trend)
def next(self):
loss_cut_price = self.data.Low[-2]
if self.trend>=trend_threshold and self.line==1 and self.data.High > self.sma:
self.buy(sl=loss_cut_price)
elif self.line==-1:
self.position.close()
bt = Backtest(df_mean.dropna(), System, cash=10000, commission=0.0003, trade_on_close=False)
output = bt.run()
initでインディケーター、nextで売買条件を定義します。
エントリー条件は、Trendの値が基準値を超え、なおかつ高値が移動平均線を上回っている状態で平均足が陰線から陽線に切り替わったタイミングで、翌日の始値でエントリーします。
この際、直近の陰線の安値を損切りラインに設定します。
slはStop Lossの略で、損切りラインを設定する事ができます。
利食いポイントを設定する場合は、tp(take profit)で指定できます。
平均足が陽線から陰線に切り替わったタイミングでポジションをクローズします。
結果の概要を見てみます。
print(output)
最終損益は34.897%となりました。
取引回数は全39回、勝率は約66%でした。
チャートでも結果を見てみます。
トレンドの判定基準を少し厳しく設定したことで、前半の相場の方向感がはっきりしていないレンジ相場では取引回数を減らし、後半の上昇トレンドで大きく利益を取れている事が確認できます。
このように、backtestingを活用すればバックテストを容易に実装する事ができます。
自動売買を行う際には、バックテストを実行して、過去の運用実績を確認した上で本番環境で運用するとある程度リスクを把握する事ができます。
本記事の執筆者:TAT
本記事の執筆者:TAT | 経歴 |
---|---|
2016年大学院卒業後、外資系IT企業に入社。 その後は金融情報→不動産テック→アドテク企業で、Pythonを用いたプロセスオートメーション、ダッシュボード開発、データ分析、AI開発などの業務に従事。 プログラミングや株式投資に関する情報を発信する「気ままなブログ」を運営中。 |
「REST API」をさらに学びたい方へオススメのコンテンツ
OANDA証券では、「REST API」に関する記事を豊富に提供しています。
「REST API」を利用するための手順、プログラミング言語の一つPythonを使い「REST API」を使用するまでの手順など、初心者の方向けコンテンツも豊富にあるので、APIを使った取引を始めてみたい方はぜひ参考にしてください。
ただしAPIを利用した取引は、OANDA証券の口座開設+いくつかの条件があります。
事前に確認を行い、ぜひOANDA証券の口座を開設し「REST API」を使った取引をご検討ください。
本ホームページに掲載されている事項は、投資判断の参考となる情報の提供を目的としたものであり、投資の勧誘を目的としたものではありません。投資方針、投資タイミング等は、ご自身の責任において判断してください。本サービスの情報に基づいて行った取引のいかなる損失についても、当社は一切の責を負いかねますのでご了承ください。また、当社は、当該情報の正確性および完全性を保証または約束するものでなく、今後、予告なしに内容を変更または廃止する場合があります。なお、当該情報の欠落・誤謬等につきましてもその責を負いかねますのでご了承ください。