Quantitative Analysis, Risk Management, Modelling, Algo Trading, and Big Data Analysis

Gap-on-Open Profitable Trading Strategy


After a longer while, QuantAtRisk is back to business. As an algo trader I have been always tempted to test a gap-on-open trading strategy. There were various reasons standing behind it but the most popular one was always omni-discussed: good/bad news on the stock. And what? The stock price skyrocketed/dropped down on the following days. When we approach such price patterns, we talk about triggers or triggered events. The core of the algorithm’s activity is the trigger identification and taking proper actions: to go long or short. That’s it. In both cases we want to make money.

In this post we will design the initial conditions for our gap-on-open trading strategy acting as the triggers and we will backtest a realistic scenario of betting our money on those stocks that opened higher on the next trading day. Our goal is to find the most optimal holding period for such trades closed with a profit.

Portfolio

Our strategy can be backtested using any $N$-asset portfolio. Here, for simplicity, let us use a random subset of 10 stocks (portfolio.lst) being a part of a current Dow Jones Index:

AXP   CSCO   DIS   IBM   JNJ   KO   NKE   PG   UTX   XOM

In Matlab, we fetch the stock prices from Google Finance data provider accessible via Quandl.com’s Matlab API (see this post for its setup in Matlab). We commence writing our main backtesting code as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
% Gap on Open Trading Strategy
%  Fetching stock prices via Quandl and Strategy Backtesting
%
% (c) 2014 by Pawel Lachowicz, QuantAtRisk.com
 
 
clear all; close all; clc;
 
fname=['portfolio.lst'];
 
% Model's parameter #1 (years)
parm1=1;  
ndays=parm1*365;
lday=datenum('2014-08-05');
% fetching stock data
[Top,Thp,Tlp,Tcp,N,ntdays]=FetchQuandl(fname,ndays,lday);

where we use a pre-designed function of FetchQuandl to import 4 separate price-series of each stock’s open (Top), high (Thp), low (Tlp), and close (Tcp) daily prices:

function [Top,Thp,Tlp,Tcp,N,ntdays]=FetchQuandl(fname,ndays,lday)
    % Read the list of Dow Jones components
    fileID = fopen(fname);
    tmp = textscan(fileID,'%s');
    fclose(fileID);
    components=tmp{1};  % a list as a cell array
 
    % Read in the list of tickers and internal codes from Quandl.com
    [~,text,~] = xlsread('QuandlStockCodeListUS.xlsx');
    quandlc=text(:,1);    % again, as a list in a cell array
    quandlcode=text(:,3); % corresponding Quandl's Price Code
 
    % fetch stock data for last ‘ndays’
    date2=datestr(lday,'yyyy-mm-dd');       % from
    date1=datestr(lday-ndays,'yyyy-mm-dd'); % to
 
    Rop={}; Tcp={};
    % scan all tickers and fetch the data from Quandl.com
    for i=1:length(components)
        for j=1:length(quandlc)
            if(strcmp(components{i},quandlc{j}))
                fprintf('%4.0f %s\n',i,quandlc{j});
                fts=0;
                [fts,headers]=Quandl.get(quandlcode{j},'type','fints', ...
                              'authcode','PutHereYourQuandlCode',...
                              'start_date',date1,'end_date',date2);
                cp=fts2mat(fts.Close,1); Tcp{i}=cp;     % close price-series
                op=fts2mat(fts.Open,1);  Top{i}=op;     % open price-series
                hp=fts2mat(fts.High,1);  Thp{i}=hp;     % high price
                lp=fts2mat(fts.Low,1);   Tlp{i}=lp;     % low price
                %Rcp{i}=cp(2:end,2)./cp(1:end-1,2)-1;   % return-series cp
            end
        end
    end
    N=length(components);
    ntdays=length(Tcp{1});
end

Please note that in line #12 we specified number of years, i.e. how far our backtest should be extended backward in time (or number of calendar days; see line #13) from the day specified in line #14 (last day).

Trading Model

First, let us design the trading strategy. We scan concurrently four price-series for each stock separately. We define the strategy’s trigger as follows:
triggers-2 i.e. if a stock open price on day $t$ was higher than the close price on the day $t-1$ and the lowest prices on day $t$ was higher than the highest price on day $t-1$. Having that, we make a BUY LONG decision! We buy that stock on the next day at its market price (close price). This approach should remove the slippage bias effectively (see more on slippage in stock trading here).

Now, we run the backtest on each stock and each open trade. We select the second parameter (parm2) to be a number of days, i.e. how long we hold the stock. In the following piece of code, let us allow to sell the stock after/between 1 to 21 calendar days ($\pm$ weekend or public holidays time period):

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
% pre-defined matrix for backtest final results
results=[];
 
for parm2=0:20
 
    cR=[];
    for i=1:N
        % just for a purpose of plotting of price-series
        if(i==1)
            % open (blue color)
            plot(Top{i}(:,1),Top{i}(:,2),'')
            hold on
            % close (red color)
            plot(Tcp{i}(:,1),Tcp{i}(:,2),'r')
            hold on
            % high (green color)
            plot(Thp{i}(:,1),Thp{i}(:,2),'g')
            %
            xlabel('Days');
            ylabel('AXP Stock Prices [US$]');
        end
 
        Tbuy=[];
        for t=2:ntdays
            % define indicators 
            ind1=Tcp{i}(t-1,2);  % cp on (t-1)day
            ind2=Thp{i}(t-1,2);  % hp on (t-1)day
            ind3=Top{i}(t,2);    % op on (t)day
            ind4=Tlp{i}(t,2);    % lp on (t)day
            % detect trigger
            if(ind1<ind3)&&(ind2<ind4)
                % plotting only for AXP
                if(i==1)
                    hold on;
                    plot(Top{i}(t,1),Top{i}(t,2),'o');
                end
                % date of a trigger
                tday=Top{i}(t,1);
                nextbusdate=busdate(tday,1); % find next trading date
                Tbuy=[Tbuy; nextbusdate];
            end
        end
        Tsell=busdate(Tbuy+parm2,1);

Here, in lines #57 and #60 we constructed time array storing physical information on those days. Now, we will use them to check the price on trade’s open and close and derive profit and loss for each stock:

62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
        R=[];
        for k=1:length(Tbuy)
            j=find(Tbuy(k)==Tcp{i}(:,1));
            pbuy=Tcp{i}(j,2);
            j=find(Tsell(k)==Tcp{i}(:,1));
            psell=Tcp{i}(j,2);
            ret=(psell/pbuy-1); % return per trade
            R=[R; ret];
        end
 
        compR=prod(R+1)-1;  % compound return per stock
        cR=[cR; compR];
 
    end
 
    results=[results cR];
 
end

In the inner loop (lines #24 to #75, i.e. tracking a number of stocks in portfolio; index $i$, here 1 to 10) we capture all trades per stock (lines #63-70) and calculate a multi-period compound return (line #72) as if we were trading that stock solely using our model.

For instance, for stock $i=1$ (AXP) from our portfolio, our code displays 1-year price-series:
post05082014-fig01 where days meeting our trigger criteria have been denoted by open-circle markers. If you now re-run the backtest making a gentle substitution in line #24 now to be:

24
    for i=1:1

we can find that by running through some extra lines of code as defined:

81
82
83
84
figure(2)
stem((0:20),100*results)
xlabel('Holding Period [days]');
ylabel('AXP: Compound Return [%]');

we obtain an appealing result:
post05082014-fig02
The chart reveals that for AXP, over past 251 days (since Aug/4 2014 backwards), we had 16 triggers therefore 16 trades and, surprisingly, regardless of holding period, the compound return from all closed trades was highly positive (profitable).

This is not the case if we consider, for example, $i=4$, IBM stock:
post05082014-fig03 This result points that for different holding periods (and different stocks of course) certain extra trading indicators should be applied to limit the losses (e.g. profit targets).

If we traded a whole portfolio using our gap-on-open model, we would end up with very encouraging result:
post05082014-fig04 where for each holding period we displayed the averaged over 10 stocks compound return. Taking into account the global up-trend in the US stock markets between August of 2013 and 2014, this strategy is worth its consideration with any further modifications (e.g. considering short or both long and short triggers, FX time-series, etc.).

Someone wise once said: Sometimes you win. Sometimes you learn. In algo trading we all learn to win.

In next post…
Marginal Value-at-Risk for Portfolio Managers

WANT TO LEARN MORE ON PORTFOLIOS in MATLAB!?
Click here!

 

Contact Form Powered By : XYZScripts.com