Backtesting FX Systre with Python Now that we've written the backtesting code, let's try optimizing the parameters of the systole. Optimizing the trading system does not mean deep learning, which is popular nowadays, but simply changing the parameter values of technical indicators to find the one with the highest evaluation value. This is for practicing Python programming.
Backtesting FX Systre with Python As with, prepare the historical data of FX. As before, I will make the data for the 2015 hourly chart of EUR / USD.
import numpy as np
import pandas as pd
import indicators as ind #indicators.Import of 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 hour offset
ohlc = ind.TF_ohlc(dataM1, 'H') #Creation of 1-hour data
For indicators.py, use the one listed on GitHub.
Use the same backtest function as last time. Calculate the trading result and profit / loss by inputting historical data and trading signal.
def Backtest(ohlc, BuyEntry, SellEntry, BuyExit, SellExit, lots=0.1, spread=2):
Open = ohlc['Open'].values #Open price
Point = 0.0001 #Value of 1pip
if(Open[0] > 50): Point = 0.01 #1 pip value of cross circle
Spread = spread*Point #Spread
Lots = lots*100000 #Actual trading volume
N = len(ohlc) #FX data size
BuyExit[N-2] = SellExit[N-2] = True #Finally forced exit
BuyPrice = SellPrice = 0.0 #Selling price
LongTrade = np.zeros(N) #Buy trade information
ShortTrade = np.zeros(N) #Sell trade information
LongPL = np.zeros(N) #Profit and Loss of Buy Position
ShortPL = np.zeros(N) #Sell position profit or loss
for i in range(1,N):
if BuyEntry[i-1] and BuyPrice == 0: #Buy entry signal
BuyPrice = Open[i]+Spread
LongTrade[i] = BuyPrice #Buy position open
elif BuyExit[i-1] and BuyPrice != 0: #Buy exit signal
ClosePrice = Open[i]
LongTrade[i] = -ClosePrice #Buy position closed
LongPL[i] = (ClosePrice-BuyPrice)*Lots #Profit and loss settlement
BuyPrice = 0
if SellEntry[i-1] and SellPrice == 0: #Sell entry signal
SellPrice = Open[i]
ShortTrade[i] = SellPrice #Sell position open
elif SellExit[i-1] and SellPrice != 0: #Sell exit signal
ClosePrice = Open[i]+Spread
ShortTrade[i] = -ClosePrice #Sell position closed
ShortPL[i] = (SellPrice-ClosePrice)*Lots #Profit and loss settlement
SellPrice = 0
return pd.DataFrame({'Long':LongTrade, 'Short':ShortTrade}, index=ohlc.index),\
pd.DataFrame({'Long':LongPL, 'Short':ShortPL}, index=ohlc.index)
To evaluate the system, use the following functions to calculate the total profit / loss, the number of transactions, the average profit / loss, the profit factor, the maximum drawdown, and the recovery factor. It is almost the same as the output by MetaTrader optimization.
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()
#Total profit and loss
Profit = GrossProfit+GrossLoss
#Number of transactions
Trades = LongTrades+ShortTrades
#Average profit and loss
if Trades==0: Average = 0
else: Average = Profit/Trades
#Profit factor
if GrossLoss==0: PF=100
else: PF = -GrossProfit/GrossLoss
#Maximum drawdown
Equity = (LongPL+ShortPL).cumsum()
MDD = (Equity.cummax()-Equity).max()
#Recovery factor
if MDD==0: RF=100
else: RF = Profit/MDD
return np.array([Profit, Trades, Average, PF, MDD, RF])
In the previous backtest, the long-term moving average period was fixed at 30 and the short-term moving average period was fixed at 10, but in this optimization, we will change these two periods.
The period of change is 10 to 50 for the long-term moving average and 5 to 30 for the short-term moving average. Put it in the array as follows.
SlowMAperiod = np.arange(10, 51) #Range of long-term moving average period
FastMAperiod = np.arange(5, 31) #Range of short-term moving averages
There are 41 and 26 ways for each period, but the combination of the two periods is $ 41 \ times 26 = 1066 $.
Optimize by substituting the period range of this parameter. As the number of combination of periods increases, the calculation time cannot be ignored, so it is necessary to eliminate unnecessary calculations as much as possible.
For the time being, calculate the time series of 41 and 26 moving averages in advance. Then, for 1066 combinations, buy / sell signals are generated, backtested, and evaluated, and parameter values and evaluation values are output. An example of the code is as follows.
def Optimize(ohlc, SlowMAperiod, FastMAperiod):
SlowMA = np.empty([len(SlowMAperiod), len(ohlc)]) #Long-term moving average
for i in range(len(SlowMAperiod)):
SlowMA[i] = ind.iMA(ohlc, SlowMAperiod[i])
FastMA = np.empty([len(FastMAperiod), len(ohlc)]) #Short-term moving average
for i in range(len(FastMAperiod)):
FastMA[i] = ind.iMA(ohlc, FastMAperiod[i])
N = len(SlowMAperiod)*len(FastMAperiod)
Eval = np.empty([N, 6]) #Evaluation item
Slow = np.empty(N) #Long-term moving average period
Fast = np.empty(N) #Short-term moving average period
def shift(x, n=1): return np.concatenate((np.zeros(n), x[:-n])) #Shift function
k = 0
for i in range(len(SlowMAperiod)):
for j in range(len(FastMAperiod)):
#Buy entry signal
BuyEntry = (FastMA[j] > SlowMA[i]) & (shift(FastMA[j]) <= shift(SlowMA[i]))
#Sell entry signal
SellEntry = (FastMA[j] < SlowMA[i]) & (shift(FastMA[j]) >= shift(SlowMA[i]))
#Buy exit signal
BuyExit = SellEntry.copy()
#Sell exit signal
SellExit = BuyEntry.copy()
#Backtest
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)
I was worried about the calculation time, but it took about 12 seconds with a Core i5-3337U 1.8GHz CPU. I tried optimizing the same conditions with MetaTrader 5, but it took nearly 50 seconds, so I think it was a fairly practical speed for Python.
You can find the optimum parameter value by sorting the optimization results by the item you like. For example, if you sort by total profit / loss, it will be as follows.
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
From this, the values of the parameters that maximize the total profit and loss are 27 for the long-term moving average period and 8 for the short-term moving average period.
As a test, the asset curve backtested during this period looks like this:
Sounds good. However, it is natural that such a result can be obtained by optimizing the parameters, and it is not very pleasing. You just have to backtest for another period and be disappointed.
This time, it's okay to get faster results than MetaTrader's backtest. MetaTrader can also backtest in tick units, but I think it will take a long time to do it in Python. There is still a long way to go.
Recommended Posts