Understanding HSI - IV - Momentum Signal - Efficient Ratio benchmarked to HSI

Posted by Chris IO on September 2, 2017

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_09-17.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_09-17.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 built-in 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 day-to-day 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(exit-entry)
                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 Most Profitable Trade

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: Self-explanatory Win Ratio: Percentage of profitable trades Average Gain: Profit/Number of Trades SD: Standard Deviation of profits Maximum Gain/Maximum Loss in one trade: self-explanatory —

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 risk-free asset, we are constantly assigning a weight on HSI and the risk-free 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_07-17.csv',header=True)
        self.data = pd.read_csv('Index_09-17.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 day-to-day 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 risk-free 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+(1-mean_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.125-a['HSI'][-1]**0.125)/np.std(a['Momentum']-a['HSI']))
        return a[['HSI','Momentum']].plot()
backend().meanfractal()
plt.show()
Information Ratio:
0.0716

Comparison to HSI

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.