PythonでFXシストレのバックテスト
で、バックテストのコードを書いたので、こんどはシストレのパラメータの最適化をやってみます。トレードシステムの最適化といっても、今流行りのディープラーニングをやるわけではなく、単にテクニカル指標のパラメータの値を色々と変えて、最も評価値の高くなるものを見つけるだけです。Pythonのプログラミングの練習のためです。
準備
PythonでFXシストレのバックテスト
と同じく、FXのヒストリカルデータを準備します。前と同じくEUR/USDの2015年の1時間足のデータを作っておきます。
import numpy as np
import pandas as pd
import indicators as ind #indicators.pyのインポート
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時間のオフセット
ohlc = ind.TF_ohlc(dataM1, 'H') #1時間足データの作成
indicators.pyはGitHubに上げてあるものを使います。
バックテストとその評価
バックテストの関数は前回と同じものを使います。ヒストリカルデータと売買シグナルを入れて売買結果と損益を算出します。
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)
システムの評価としては、次の関数で、 総損益、取引数、平均損益、プロフィットファクター、最大ドローダウン、リカバリーファクターを算出します。だいたいMetaTraderの最適化で出力されるのと同じものです。
def BacktestReport(Trade, PL):
LongPL = PL['Long']
ShortPL = PL['Short']
LongTrades = np.count_nonzero(Trade['Long'])//2
ShortTrades = np.count_nonzero(Trade['Short'])//2
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
#取引数
Trades = LongTrades+ShortTrades
#平均損益
if Trades==0: Average = 0
else: Average = Profit/Trades
#プロフィットファクター
if GrossLoss==0: PF=100
else: PF = -GrossProfit/GrossLoss
#最大ドローダウン
Equity = (LongPL+ShortPL).cumsum()
MDD = (Equity.cummax()-Equity).max()
#リカバリーファクター
if MDD==0: RF=100
else: RF = Profit/MDD
return np.array([Profit, Trades, Average, PF, MDD, RF])
最適化するパラメータとその範囲
前回のバックテストでは、長期移動平均の期間を30、短期移動平均の期間を10、と固定してテストしましたが、今回の最適化では、この二つの期間を変えてみます。
変化させる期間は、長期移動平均で10から50、短期移動平均で5から30としてみます。次のように配列に入れておきます。
SlowMAperiod = np.arange(10, 51) #長期移動平均期間の範囲
FastMAperiod = np.arange(5, 31) #短期移動平均期間の範囲
それぞれの期間は41通り、26通りですが、二つの期間の組み合わせだと、$41\times 26=1066$通りとなります。
最適化
このパラメータの期間の範囲を代入して最適化を行います。期間の組み合わせの数が増えてくると計算時間が無視できなくなるので、極力、無駄な計算を省かなくてはいけません。
とりあえず、41通り、26通りの移動平均の時系列をあらかじめ計算しておきます。そして、1066通りの組み合わせに対して、売買シグナルの生成、バックテスト、評価を行い、パラメータの値、評価値を出力します。コードの一例は以下のようになります。
def Optimize(ohlc, SlowMAperiod, FastMAperiod):
SlowMA = np.empty([len(SlowMAperiod), len(ohlc)]) #長期移動平均
for i in range(len(SlowMAperiod)):
SlowMA[i] = ind.iMA(ohlc, SlowMAperiod[i])
FastMA = np.empty([len(FastMAperiod), len(ohlc)]) #短期移動平均
for i in range(len(FastMAperiod)):
FastMA[i] = ind.iMA(ohlc, FastMAperiod[i])
N = len(SlowMAperiod)*len(FastMAperiod)
Eval = np.empty([N, 6]) #評価項目
Slow = np.empty(N) #長期移動平均期間
Fast = np.empty(N) #短期移動平均期間
def shift(x, n=1): return np.concatenate((np.zeros(n), x[:-n])) #シフト関数
k = 0
for i in range(len(SlowMAperiod)):
for j in range(len(FastMAperiod)):
#買いエントリーシグナル
BuyEntry = (FastMA[j] > SlowMA[i]) & (shift(FastMA[j]) <= shift(SlowMA[i]))
#売りエントリーシグナル
SellEntry = (FastMA[j] < SlowMA[i]) & (shift(FastMA[j]) >= shift(SlowMA[i]))
#買いエグジットシグナル
BuyExit = SellEntry.copy()
#売りエグジットシグナル
SellExit = BuyEntry.copy()
#バックテスト
Trade, PL = Backtest(ohlc, BuyEntry, SellEntry, BuyExit, SellExit)
Eval[k] = BacktestReport(Trade, PL)
Slow[k] = SlowMAperiod[i]
Fast[k] = FastMAperiod[j]
k += 1
return pd.DataFrame({'Slow':Slow, 'Fast':Fast, 'Profit': Eval[:,0], 'Trades':Eval[:,1],
'Average':Eval[:,2],'PF':Eval[:,3], 'MDD':Eval[:,4], 'RF':Eval[:,5]},
columns=['Slow','Fast','Profit','Trades','Average','PF','MDD','RF'])
result = Optimize(ohlc, SlowMAperiod, FastMAperiod)
計算時間が気になっていましたが、Core i5-3337U 1.8GHzのCPUで、12秒ほどでした。同じ条件の最適化をMetaTrader5でやってみましたが、50秒近くかかったので、Pythonとしては、まあまあ実用可能な速度だったと思います。
最適化の結果
最適化の結果は、好みの項目でソートすることで、最適なパラメータの値がわかります。例えば、総損益でソートすると以下のようになります。
result.sort_values('Profit', ascending=False).head(20)
Slow Fast Profit Trades Average PF MDD RF
445 27.0 8.0 2507.1 264.0 9.496591 1.423497 485.1 5.168213
470 28.0 7.0 2486.0 260.0 9.561538 1.419642 481.2 5.166251
446 27.0 9.0 2263.3 252.0 8.981349 1.376432 624.7 3.623019
444 27.0 7.0 2171.4 272.0 7.983088 1.341276 504.7 4.302358
471 28.0 8.0 2102.3 250.0 8.409200 1.359030 540.3 3.890986
497 29.0 8.0 2093.3 242.0 8.650000 1.365208 603.8 3.466876
495 29.0 6.0 2063.5 256.0 8.060547 1.342172 620.6 3.325008
498 29.0 9.0 2053.5 238.0 8.628151 1.362451 686.5 2.991260
546 31.0 5.0 1959.4 254.0 7.714173 1.344256 529.7 3.699075
520 30.0 5.0 1940.3 276.0 7.030072 1.313538 681.7 2.846267
496 29.0 7.0 1931.5 248.0 7.788306 1.322891 611.3 3.159660
422 26.0 11.0 1903.4 248.0 7.675000 1.309702 708.7 2.685763
523 30.0 8.0 1903.0 232.0 8.202586 1.327680 823.9 2.309746
524 30.0 9.0 1875.8 234.0 8.016239 1.328598 908.6 2.064495
573 32.0 6.0 1820.8 242.0 7.523967 1.320688 639.8 2.845889
420 26.0 9.0 1819.1 258.0 7.050775 1.282035 667.0 2.727286
572 32.0 5.0 1808.2 256.0 7.063281 1.313564 522.9 3.458023
598 33.0 5.0 1799.6 248.0 7.256452 1.317183 613.2 2.934768
419 26.0 8.0 1777.4 274.0 6.486861 1.273817 552.7 3.215849
434 26.0 23.0 1739.6 368.0 4.727174 1.241049 1235.5 1.408013
これより、総損益が最大となるパラメータの値は、長期移動平均の期間が27、短期移動平均の期間が8ということになります。
試しに、この期間でバックテストした資産曲線は以下のようになります。
いいですね。ただ、パラメータの最適化をすれば、この程度の結果が出るのは当たり前で、あまり喜べるものではありません。別の期間のバックテストをしてがっかりするだけです。
今回はMetaTraderのバックテストより速い結果が出ただけでよしとします。MetaTraderではティック単位のバックテストもできるのですが、それをPythonでやろうとすると、やっぱりかなり時間がかかるんじゃないかと思います。まだまだ先は長いです。