tony lam

A trend following strategy based on volatility approach

Trading Strategy


In this article, we will introduce a simple trend following strategy using a volatility approach.


Volatility Decomposition: Upside vs Downside

Volatility is used to reflect the magnitude of market fluctuation, and it is usually used to observe market sentiment or predict market trends. In financial market, due to the behavior of "Market take the stairs up and the elevator down", the volatility distribution is not symmetric. Therefore, it is necessary to distinguish upside and downside volatility.

In this article, we take the opening price as the benchmark, the fluctuation above the opening price is defined as the upward volatility, otherwise it is the downward volatility. Under normal circumstances, the upward volatility is greater than the downward volatility when the market is in the upward trend, and vice versa in the downward trend.


Calculation

In above definition, the difference between upward and downward volatility is measured as:


difft := (Hight + Lowt ) / Opent - 2


To smooth out the short-term fluctuations, a N-day moving average is applied.


Smoothed Difft = (difft + difft-1 + difft-N+1 ) / N


The market is said to be in an upward trend when the Smoothed Difft is calculated to be positive; and vice versa in a downward trend when its value is negative.


Trading Logic

Suppose we take a 60-day moving average, and apply this strategy to the daily closing price of SP500 Index CFD (i.e. SPXUSD) over the year of 2015.01 - 2016.12

  • Based on a sliding window approach to collect the previous 250 closing price
  • Calculate the 60-day MA smoothed volatility difference
  • Order open conditions:
    • if we have NO outstanding position,
      • if the smoothed difference >0, we open a buy order
      • if the smoothed difference <0, we open a sell order
  • Order close conditions:
    • if we have outstanding position,
      • if we previously submit a buy order and the smooted diff become negative, close the buy order
      • if we previously submit a sell order and the smooted diff become positive, close the sell order
  • Repeat the process until the backtest period end

Now, let's write down our trading algorithm step-by-step.

Step 1. Calculate smoothed up/down volatility difference

First of all, we define 'ma_period' and 'symbol' at initialization.

1
2
3
4
5
6
7
8
from AlgoAPI import AlgoAPIUtil, AlgoAPI_Backtest
import pandas as pd

class AlgoEvent:
    def __init__(self):
        self.ma_period = 60
        self.symbol = "SPXUSD"
        

We use the API function getHistoricalBar to collect historical observations. Then, we use the 'pandas' library to calculate the simple moving average.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    def on_marketdatafeed(self, md, ab):
        # get historical data
        res = self.evt.getHistoricalBar(
            contract={"instrument": self.symbol}, 
            numOfBar=250, 
            interval='D'
        )
        
        # calculate smoothed volatility difference
        diff = [(res[t]['h']+res[t]['l'])/res[t]['o']-2  for t in res]
        diff_ma = pd.Series(diff).rolling(self.ma_period).mean()
    
        # extract the current diff_ma value        
        signal = diff_ma[self.ma_period-1]
        
        # print result to console 
        self.evt.consoleLog(md.timestamp, signal)
        

Step 2. Order Entry Conditions

We create a function 'open_order' to handle order submissions.

1
2
3
4
5
6
7
8
9
    def open_order(self, buysell):
        order = AlgoAPIUtil.OrderObject(
            instrument = self.symbol,
            openclose = 'open', 
            buysell = buysell,    #1=buy, -1=sell
            ordertype = 0,  #0=market, 1=limit
            volume = 0.01
        )
        self.evt.sendOrder(order)

Now, we update the system's function 'on_marketdatafeed' for order open logic. We will use the API function 'getSystemOrders()' to get our outstanding order inventory. (refer to line #20 - #33 below)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
    def on_marketdatafeed(self, md, ab):
        # get historical data
        res = self.evt.getHistoricalBar(
            contract={"instrument": self.symbol}, 
            numOfBar=250, 
            interval='D'
        )
        
        # calculate smoothed volatility difference
        diff = [(res[t]['h']+res[t]['l'])/res[t]['o']-2  for t in res]
        diff_ma = pd.Series(diff).rolling(self.ma_period).mean()
    
        # extract the current diff_ma value        
        signal = diff_ma[self.ma_period-1]
        
        # print result to console 
        self.evt.consoleLog(md.timestamp, signal)
        
        
        # get current order inventory
        positions, osOrders, _ = self.evt.getSystemOrders()
        pos = positions[self.symbol]["netVolume"]
        
        # open order condition
        if pos==0:
            
            # open buy order
            if signal > 0:
                self.open_order(buysell=1)
                                
            # open sell order
            elif signal < 0:
                self.open_order(buysell=-1)

Step 3. Order Close Condition

We continue to update the system's function 'on_marketdatafeed' for close order logic (refer to line #36 - #54 below).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
    def on_marketdatafeed(self, md, ab):
        # get historical data
        res = self.evt.getHistoricalBar(
            contract={"instrument": self.symbol}, 
            numOfBar=250, 
            interval='D'
        )
        
        # calculate smoothed volatility difference
        diff = [(res[t]['h']+res[t]['l'])/res[t]['o']-2  for t in res]
        diff_ma = pd.Series(diff).rolling(self.ma_period).mean()
    
        # extract the current diff_ma value        
        signal = diff_ma[self.ma_period-1]
        
        # print result to console 
        self.evt.consoleLog(md.timestamp, signal)
        
        
        # get current order inventory
        positions, osOrders, _ = self.evt.getSystemOrders()
        pos = positions[self.symbol]["netVolume"]
        
        # open order condition
        if pos==0:
            
            # open buy order
            if signal > 0:
                self.open_order(buysell=1)
                                
            # open sell order
            elif signal < 0:
                self.open_order(buysell=-1)

        
        # close order condition
        else:
            isClose = False
            
            # outstanding position > 0 and signal < 0
            if pos>0 and signal<0:
                isClose = True
            # outstanding position < 0 and signal > 0
            elif pos<0 and signal>0:
                isClose = True
            
            # close all outstanding trade
            if isClose:
                for tradeID in osOrders:
                    order = AlgoAPIUtil.OrderObject(
                        tradeID=tradeID,
                        openclose = 'close'
                    )
                    self.evt.sendOrder(order)

Full Source Code

Combining all above, the full script is presented below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
from AlgoAPI import AlgoAPIUtil, AlgoAPI_Backtest
import pandas as pd

class AlgoEvent:
    def __init__(self):
        self.ma_period = 60
        self.symbol = "SPXUSD"
        
    def start(self, mEvt):
        self.evt = AlgoAPI_Backtest.AlgoEvtHandler(self, mEvt)
        self.evt.start()
        
    def on_marketdatafeed(self, md, ab):
        # get historical data
        res = self.evt.getHistoricalBar(
            contract={"instrument": self.symbol}, 
            numOfBar=250, 
            interval='D'
        )
        
        # calculate smoothed volatility difference
        diff = [(res[t]['h']+res[t]['l'])/res[t]['o']-2  for t in res]
        diff_ma = pd.Series(diff).rolling(self.ma_period).mean()
    
        # extract the current diff_ma value        
        signal = diff_ma[self.ma_period-1]
        
        # print result to console 
        self.evt.consoleLog(md.timestamp, signal)
        
        
        # get current order inventory
        positions, osOrders, _ = self.evt.getSystemOrders()
        pos = positions[self.symbol]["netVolume"]
        
        # open order condition
        if pos==0:
            
            # open buy order
            if signal > 0:
                self.open_order(buysell=1)
                                
            # open sell order
            elif signal < 0:
                self.open_order(buysell=-1)

        
        # close order condition
        else:
            isClose = False
            
            # outstanding position > 0 and signal < 0
            if pos>0 and signal<0:
                isClose = True
            # outstanding position < 0 and signal > 0
            elif pos<0 and signal>0:
                isClose = True
            
            # close all outstanding trade
            if isClose:
                for tradeID in osOrders:
                    order = AlgoAPIUtil.OrderObject(
                        tradeID=tradeID,
                        openclose = 'close'
                    )
                    self.evt.sendOrder(order)
                    

    def open_order(self, buysell):
        order = AlgoAPIUtil.OrderObject(
            instrument = self.symbol,
            openclose = 'open', 
            buysell = buysell,    #1=buy, -1=sell
            ordertype = 0,  #0=market, 1=limit
            volume = 0.01
        )
        self.evt.sendOrder(order)

Results

Now, we are prepared to backtest this strategy.

Backtest Settings:

  • Instrument: SPXUSD
  • Period: 2015.01 - 2016.12
  • Initial Capital: US$10,000
  • Data Interval: 1-day bar
  • Leverage: 1
  • Allow Shortsell: True

Backtest Result:

result

Final Thoughts

The result above does not perform well. Below are some ideas to improve this trading strategy:

  • The current entry condition solely based on a non-zero smoothed value which is easy to trigger. Filter the value by a certain threshold may increase the trend signal's accuracy.
  • Adding take profit/ stop loss level may be helpful to cut lost/gain earlier.
  • The MA period in this example is taken to be 60-day. Different smoothing period could be more appropriate.