In the last article, I have done a comparison of EWMA pairs as a short sell signal. I treated the signal as a categorical one(0,1), assumpting that only one contract is traded whenever a signal prompts a trade. In reality, signals are rarely dealed as a categorical one. In this article, in addition to finding the best categorical signals, I would like to backtest the quality of the indicator by incorporating the quantitative aspects of it into the performance metrics.
Inspired by the simple and effective calculation of Efficient Ratio(ER) by Kaufman, I would like to use this technical indicator as the proxy of momentum. The calculation of this signal can be found here. Like any other momentum signal, ER requires a specification of time span, for which the signal would count into calculation. A shorter time span leads to a more sensitive signal, and vice versa. The signal ranges from 0 to 1, where the magnitude 1 signals the strongest momentum.
Download Index_0917.csv on here
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
class backend:
def __init__(self):
#read data
self.data = pd.read_csv('Index_0917.csv',error_bad_lines=False)
#set the time as the index for plotting
self.data.index = pd.to_datetime(self.data.pop('Date'))
#replace null with previous values
self.data['Adj Close'].replace(to_replace='null',method='ffill',inplace=True)
self.data['Open'].replace(to_replace='null',method='ffill',inplace=True)
self.data.close = pd.to_numeric(self.data['Adj Close'])
self.data.open = pd.to_numeric(self.data['Open'])
First initialize the data under the init part, fill up the NaN datapoint with its previous data using the pandas builtin method ffill
.
def ERAverage(self,num):
df = pd.DataFrame()
a = self.data
for count in range(1,num):
#direction index, 1 for up 0 for down
a['direction'] = np.where(a.close.diff(count)>0,1,0)
#absolute difference relative to count days before
a['abs'] = a.close.diff(count).abs()
#cumulative daytoday difference
a['volatility'] = a.close.diff().abs().rolling(count).sum()
#the Efficient Ratio index for count days average
a['fractal'] = a['abs']/a['volatility']*a['direction']
#concatenation
df = pd.concat([df, a['fractal']], axis=1)
df = pd.DataFrame(df).sum(1,skipna=True)/num
return df
This function return a series of Efficient Ratio based on num
which is the specified time span. Direction first specified upward movement as 1, downward movement as 0. Then calculate the ratio of the absolute change of one day, to the volatility(accumulation of all absolute differences across the time span). If the index moves downward, the ratio simply multiplies by 0 to return 0. If the absolute difference is equal to the volatility, meaning that the index has increased everyday within the time span, the signal returns 1.
Since the indicator is quite jumpy and irregular, I have employed an arithematic average of ER(1) up to the specified time span. So for ER(12), an average of ER(1),ER(2) up to ER(12) would be return. This is just my judgment, you may employ a different averaging technique like EWMA or simply use one time series.
Now with the function to calculate ER, we can simulate the profit and loss by PnL(threshold,l)
:
def PnL(self,threshold,l):
a = self.data
mean_shift = self.ERAverage(l)
#signal
x = mean_shift >= threshold
holding = False
pnL_histroy = []
entry = 0
exit = 0
for i in range(x.shape[0]1):
if x.iloc[i] == True:
if holding == False:
#trade on the next day
entry = a.open.iloc[i+1]
holding = True
else:
pass
if x.iloc[i] == False:
if holding == False:
pass
else:
# trade on the next day
exit = a.open.iloc[i+1]
pnL_histroy.append(exitentry)
holding = False
# profit, number of trade, win ratio, profit per trade,SD, maxprofit, max loss
if sum(pnL_histroy) == 0:
return 0,0,0,0,0,0,0
return sum(pnL_histroy),len(pnL_histroy),sum([1 for i in pnL_histroy if i>=0])/len(pnL_histroy),sum(pnL_histroy)/len(pnL_histroy), np.std(pnL_histroy),max(pnL_histroy),min(pnL_histroy)
#iteratively tries different combinations
def output(self,thresholdl=0.3,thresholdh=1,averagel=3,averageh=23):
threshold,span,profit,number,winratio,GPT,SD,MP,ML = [],[],[],[],[],[],[],[],[]
for i in np.arange(thresholdl,thresholdh,0.05):
for j in range(averagel,averageh):
p,num,winr,gpt,sd,mp,ml = self.PnL(i,j)
##This function only exists to make the output looks nice and readily to be converted to csv
threshold.append(i)
span.append(j)
profit.append(p)
number.append(num)
winratio.append(winr)
GPT.append(gpt)
SD.append(sd)
MP.append(mp)
ML.append(ml)
df = pd.DataFrame(data={'Threshold':threshold,'TimeSpan':span,'profit':profit,'number of trade':number,'Win Ratio':winratio,
'Average Gain':GPT,'SD':SD,'Maximum Gain in One Trade':MP,'Maximum Loss in One Trade':ML})
# specify the index as well.
return df[['Threshold','TimeSpan','profit','number of trade','Win Ratio','Average Gain','SD','Maximum Gain in One Trade','Maximum Loss in One Trade']]
This is the function that iteratively tries each combination of time span and entry threshold of ER. I tried threshold from (0.3  1) with a step size of 0.05, and timespan from 3 to 23.
The Result Panel:
Threshold: the threshold of Efficient Ratio for entering into trade TimeSpan: the time span for which ER is calcaluated Profit: Points in the index Numer of Trade: Selfexplanatory Win Ratio: Percentage of profitable trades Average Gain: Profit/Number of Trades SD: Standard Deviation of profits Maximum Gain/Maximum Loss in one trade: selfexplanatory —
Now let’s try to implement the same strategy in a quantiative approach. Assume we are constructing a portfolio based on Hang Seng Index and a riskfree asset, we are constantly assigning a weight on HSI and the riskfree asset respectively which add up to 1. Here we use the mean ER as the weighting factor.
This is a rather conservative strategy since we only fully leverage on HSI when the ER is 1, while holding some portion of risk free asset during the remaining time. Inherently we are testing the efficiency of the signal itself, the profit at the end can be easily manipulated by increasing the leverage.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
class backend:
def __init__(self):
#self.future = pd.read_csv('Future_0717.csv',header=True)
self.data = pd.read_csv('Index_0917.csv',error_bad_lines=False)
#set the time as the index for plotting
self.data.index = pd.to_datetime(self.data.pop('Date'))
#replace null with previous values
self.data['Adj Close'].replace(to_replace='null',method='ffill',inplace=True)
self.data['Open'].replace(to_replace='null',method='ffill',inplace=True)
self.data.close = pd.to_numeric(self.data['Adj Close'])
self.data.open = pd.to_numeric(self.data['Open'])
def ERAverage(self,num):
df = pd.DataFrame()
a = self.data
for count in range(1,num):
#direction index, 1 for up 0 for down
a['direction'] = np.where(a.close.diff(count)>0,1,0)
#absolute difference relative to count days before
a['abs'] = a.close.diff(count).abs()
#cumulative daytoday difference
a['volatility'] = a.close.diff().abs().rolling(count).sum()
#the Efficient Ratio index for count days average
a['fractal'] = a['abs']/a['volatility']*a['direction']
#concatenation
df = pd.concat([df, a['fractal']], axis=1)
df = pd.DataFrame(df).sum(1,skipna=True)/num
return df
def meanfractal(self,l=12):
a = self.data
# shift the index by 1 day of delay
mean_shift = self.ERAverage(l).shift(1)
# same for the price
price_shift = a.close.shift(1)
# remaining money on riskfree rate
factor = 1.02**(1/252)
# assign a weighting to daily ups downs based on the mean_ER(0 to 1) yesterday
a['Momentum'] = (a.close/price_shift*mean_shift+(1mean_shift)*factor).cumprod()
a.dropna(inplace=True)
#normalize the beginning level to 100%
a['HSI'] = a.close.div(a.close[0])
print('Information Ratio:')
print((a['Momentum'][1]**0.125a['HSI'][1]**0.125)/np.std(a['Momentum']a['HSI']))
return a[['HSI','Momentum']].plot()
backend().meanfractal()
plt.show()
Information Ratio:
0.0716
As shown in the graph, the strategy is quite good at defensing during down times, while preserving the upward gains during up times. Having said that, collecting 40% profit with risk free rate included in the period of 8 years, only made an annualized return of 4.2%, not far from the 2% assumed base rate. It is quite good considering how poorly the HSI index has done, but not amazingly impressive. Moreover, the performance does not necessarily mean the strategy is a good one under the category of momentum. The performance of other momentum strategies or proxies are subject to further evidence/testing.

Previous
Numerai Tutorial  II  Label Specific Preprocessing and Iterative Screening 
Next
Pair Trading  I  DBSCAN on HK Stocks