pandas DataFrameのapply関数を用いて時系列データの汎用的な簡易バックテストを行うサンプル
[2.各パラメータ組み合わせ(df_params)]にapply関数を用いて
[1.テストデータ(df_ohlcv)]を与えて[3.ストラテジー(def strategy)]を実行します。
[1.テストデータ], [2.ストラテジー]をカスタマイズすることで様々なテストを作成できます。
本サンプルは以下の条件とサンプルロジックを例に説明しています。
【注意】ロジックも評価方法も解説のためのサンプルであり、有効なテスト結果ではありません。
# coding: utf-8
import time
import requests
from collections import OrderedDict
import numpy as np
import pandas as pd
pd.set_option("display.max_rows", 10)
#------------------------------------
# [テスト対象期間設定]
PERIOD = 1 # 時間足(分)
BARS = 10080 # 期間数
#------------------------------------
# BitMEX OHLCVデータ取得 (1m足 直近1週間)
to_time = int(time.time())
from_time = to_time - BARS * PERIOD * 60
param = {"period": PERIOD, "from": from_time, "to": to_time}
url = "https://www.bitmex.com/api/udf/history?symbol=XBTUSD&resolution={period}&from={from}&to={to}".format(**param)
data = requests.get(url).json()
df_ohlcv = pd.DataFrame( \
OrderedDict(timestamp=data["t"], open=data["o"], high=data["h"], \
low=data["l"], close=data["c"], volume=data["v"]))
df_ohlcv = df_ohlcv.iloc[:BARS]
# UnixTimeから日付変換してindexに設定
df_ohlcv["datetime"] = pd.to_datetime(df_ohlcv["timestamp"], unit="s")
df_ohlcv = df_ohlcv.set_index("datetime")
df_ohlcv.index = df_ohlcv.index.tz_localize("UTC")
df_ohlcv.index = df_ohlcv.index.tz_convert("Asia/Tokyo")
# 取得結果表示
display(df_ohlcv)
timestamp | open | high | low | close | volume | |
---|---|---|---|---|---|---|
datetime | ||||||
2019-05-14 19:23:00+09:00 | 1557829380 | 8024.0 | 8032.0 | 8015.5 | 8016.0 | 4146640 |
2019-05-14 19:24:00+09:00 | 1557829440 | 8016.0 | 8022.5 | 8005.5 | 8021.0 | 4138129 |
2019-05-14 19:25:00+09:00 | 1557829500 | 8021.0 | 8020.5 | 7988.5 | 7988.5 | 3155079 |
2019-05-14 19:26:00+09:00 | 1557829560 | 7988.5 | 8019.0 | 7988.5 | 8012.5 | 3760529 |
2019-05-14 19:27:00+09:00 | 1557829620 | 8012.5 | 8013.0 | 7993.0 | 7993.0 | 2983371 |
... | ... | ... | ... | ... | ... | ... |
2019-05-21 19:18:00+09:00 | 1558433880 | 7940.0 | 7944.5 | 7939.5 | 7944.5 | 2240989 |
2019-05-21 19:19:00+09:00 | 1558433940 | 7944.5 | 7950.0 | 7941.0 | 7941.5 | 4931035 |
2019-05-21 19:20:00+09:00 | 1558434000 | 7941.5 | 7952.5 | 7941.0 | 7952.5 | 1381971 |
2019-05-21 19:21:00+09:00 | 1558434060 | 7952.5 | 7952.5 | 7948.5 | 7951.5 | 854896 |
2019-05-21 19:22:00+09:00 | 1558434120 | 7951.5 | 7951.5 | 7941.5 | 7943.5 | 2762757 |
10080 rows × 6 columns
短期SMAと長期SMAに設定する期間数のリストを作成し、その全パラメータ組み合わせを作成する
import itertools
#------------------------------------
# [パラメータリスト]
SHORT_TERM = [1, 3, 5, 10, 15, 30, 60, 120, 240, 480, 720, 1440] # 短期SMA期間
LONG_TERM = [3, 5, 10, 15, 30, 60, 120, 240, 480, 720, 1440, 2880, 4320] # 長期SMA期間
#------------------------------------
# パラメータ全組み合わせ(itertools.product : 直積)
df_params = pd.DataFrame(list(itertools.product(SHORT_TERM, LONG_TERM)), \
columns=["short", "long"])
# SHORT < LONGの組み合わせのみに絞り込み
df_params = df_params[df_params["short"] < df_params["long"]]
# indexリセット
df_params.reset_index(drop=True, inplace=True)
# パラメータ組み合わせ表示
display(df_params)
short | long | |
---|---|---|
0 | 1 | 3 |
1 | 1 | 5 |
2 | 1 | 10 |
3 | 1 | 15 |
4 | 1 | 30 |
... | ... | ... |
85 | 720 | 1440 |
86 | 720 | 2880 |
87 | 720 | 4320 |
88 | 1440 | 2880 |
89 | 1440 | 4320 |
90 rows × 2 columns
df_params.apply()に適用するストラテジー関数を定義します。
apply関数のargsにdf_ohlcvを設定するのでストラテジー内で使用することができます。
[params]
[returns]
# ストラテジー定義
def strategy(params, ohlcv):
# 現在ポジションの約定価格とポジション方向
position = {"price":0, "side":0} # side: 0-> no, -1->short, 1->long
# トレード回数
total_trades = 0
# 期間毎の損益値幅
lst_pl = []
# 終値(close)の短期/長期SMAをそれぞれ算出
close = ohlcv["close"].values # 終値ndarray
# 短期SMA
sma_s = ohlcv["close"].rolling(window=params["short"], min_periods=1).mean().values
# 長期SMA
sma_l = ohlcv["close"].rolling(window=params["long"], min_periods=1).mean().values
# 全期間分を順次判定していく
# GC/DCでドテンするロジック
for i in range(1,len(close)):
pl = 0 # 損益値幅
# ゴールデンクロス判定
if (sma_s[i-1] <= sma_l[i-1] and sma_s[i] > sma_l[i]):
if position["side"] == 0:
# ポジションがない場合、Longポジション設定
position = {"price":close[i], "side":1}
total_trades += 1
elif position["side"] == -1:
# ショートポジションの場合、ドテンロング
pl = position["price"] - close[i] # ポジション約定価格と終値から損益値幅計算
position = {"price":close[i], "side":1} # Longポジション設定
total_trades += 1
# デッドクロス判定
elif (sma_s[i-1] >= sma_l[i-1] and sma_s[i] < sma_l[i]):
if position["side"] == 0:
# ポジションがない場合、Shortポジション設定
position = {"price":close[i], "side":-1}
total_trades += 1
elif position["side"] == 1:
# ロングポジションの場合、ドテンショート
pl = close[i] - position["price"] # ポジション約定価格と終値から損益値幅計算
position = {"price":close[i], "side":-1} # Shortポジション設定
total_trades += 1
# 損益値幅リスト追加
lst_pl.append(pl)
# 最後にポジションクローズ
if position["side"] == -1:
lst_pl.append(position["price"] - close[-1])
total_trades += 1
elif position["side"] == 1:
lst_pl.append(close[-1] - position["price"])
total_trades += 1
# トータルPL(獲得値幅)とPL推移リストをreturn
return sum(lst_pl), total_trades, lst_pl
各パラメータ組み合わせに対して定義したテストストラテジーを適用します。
また、ストラテジー引数にテスト期間のohlcvデータを渡します。
# パラメータ組み合わせにストラテジーを適用
result = df_params.apply(strategy, axis=1, args=(df_ohlcv,))
# テスト結果表示
print("パラメータNo. (トータル損益値幅, トータルトレード回数, [期間毎の損益値幅リスト])")
display(result)
パラメータNo. (トータル損益値幅, トータルトレード回数, [期間毎の損益値幅リスト])
0 (1101.5, 3561, [0, -32.5, -24.0, -19.5, 0, -0.... 1 (6163.5, 2473, [0, -32.5, -24.0, -19.5, 0, 0, ... 2 (4983.5, 1639, [0, -32.5, -24.0, -19.5, 0, 0, ... 3 (2756.5, 1347, [0, -32.5, -24.0, -19.5, 0, 0, ... 4 (501.5, 1033, [0, -32.5, -24.0, -19.5, 0, 0, 0... ... 85 (-103.5, 9, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... 86 (698.5, 7, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0... 87 (-298.5, 5, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... 88 (963.0, 8, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0... 89 (-420.0, 7, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... Length: 90, dtype: object
パラメータ毎のトータル損益値幅を散布図にて表示する
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
%matplotlib inline
# パラメータ別トータル損益値幅をDataFrame化
df_total_pl = pd.DataFrame([r[0] for r in result], columns=["pl"])
# トータル損益値幅表示
display(df_total_pl)
# 散布図
plt.grid(color="gray")
plt.scatter(x=df_total_pl.index, y=df_total_pl["pl"]) # x:パラメータNo, y:トータル損益値幅
plt.show()
pl | |
---|---|
0 | 1101.5 |
1 | 6163.5 |
2 | 4983.5 |
3 | 2756.5 |
4 | 501.5 |
... | ... |
85 | -103.5 |
86 | 698.5 |
87 | -298.5 |
88 | 963.0 |
89 | -420.0 |
90 rows × 1 columns
# 最大利益値幅のパラメータNoを取得
max_idx = df_total_pl["pl"].values.argmax()
print("best params no:{} short:{} long:{} total pl:{}".format(
max_idx, df_params["short"][max_idx], df_params["long"][max_idx], df_total_pl["pl"][max_idx]))
# 最大利益値幅の損益推移を表示
fig, ax = plt.subplots()
ax.set_title("Profit and loss graph") # タイトル
ax.xaxis.set_major_formatter(mdates.DateFormatter("%m/%d\n%H:%M")) # X軸ラベル
plt.grid(color="gray")
np_date = df_ohlcv.index.values # X:OHLCV期間時刻
np_sum_pl = np.cumsum(result[max_idx][2]) # Y:累積損益値幅
plt.plot(np_date, np_sum_pl)
plt.show()
best params no:1 short:1 long:5 total pl:6163.5