MACD Settings

In the Algotrading site of Reddit a thread about optimizing MACD settings was found.

Having started the backtrader quest because of Trading Your Way To Financial Freedom - Amazon Link I had no other choice but to post an answer and craft a sample.

The strategy approach is loosely based on some of the ideas presented in the book. Nothing new under the sun. And the parameters have been quickly set. No overfitting, no optimization, no nothing. Roughly:

  • Enter if the macd line crosses the signal line to the upside and a control Simple Moving Average has had a net negative direction in the last x periods (current SMA value below the value x periods ago)
  • Set a stop price N x ATR times away from the close price
  • Exit the market if the close price goes below the stop price
  • If still in the market, update the stop price only if it’s greater than the actual one

The staking is done by:

  • Having a Sizer allocate a percentage of the available cash to the operation. The more the strategy wins, the more it stakes ... and the more it loses the less it stakes.

There is a commision included. Because the tests will be done with stocks a percentage commission has been chosen with a value 0.0033 (aka 0.33% per round trip.

To put this into play, there will be 3 runs with 3 datas (for a total of 9 runs)

  1. The standard parameters which have been chosen manually
  2. Increase cash allocation perentage from 0.20 to 0.50
  3. Increase ATR distance for the stop from 3.0 to 4.0 to try to avoid being whipped

The manually chosen parameters for the strategy:

params = (
    # Standard MACD Parameters
    ('macd1', 12),
    ('macd2', 26),
    ('macdsig', 9),
    ('atrperiod', 14),  # ATR Period (standard)
    ('atrdist', 3.0),   # ATR distance for stop price
    ('smaperiod', 30),  # SMA Period (pretty standard)
    ('dirperiod', 10),  # Lookback period to consider SMA trend direction
)

And system wide:

parser.add_argument('--cash', required=False, action='store',
                    type=float, default=50000,
                    help=('Cash to start with'))

parser.add_argument('--cashalloc', required=False, action='store',
                    type=float, default=0.20,
                    help=('Perc (abs) of cash to allocate for ops'))

parser.add_argument('--commperc', required=False, action='store',
                    type=float, default=0.0033,
                    help=('Perc (abs) commision in each operation. '
                          '0.001 -> 0.1%%, 0.01 -> 1%%'))

The date range will be from 2005-01-01 to 2014-12-31 for a total of 10 years.

Analyzers

To get some objective data 3 analyzers will be added to the system:

  • Two TimeReturn for the entire period

    1. For the strategy itself
    2. For the data being operated upon as benchmark

    Effectively benchmarking the strategy against the asset

  • A TimeReturn to measure annual returns

  • A SharpeRatio to see how the strategy performs against a risk-free asset

    The value is set at 1%, can be changed with the sample options

  • A SQN (System Quality Number) to analyze the quality of the trades using this performance indicator defined by Van K. Tharp

Additonally a DrawDown observer will be added to the mix.

Run 1: Standard Parameters

YHOO

$ ./macdsystem.py --plot --dataset yhoo
===============================================================================
TimeReturn:
  - 9999-12-31 23:59:59.999999: -0.07118518868
===============================================================================
TimeReturn:
  - 9999-12-31 23:59:59.999999: 0.316736183525
===============================================================================
TimeReturn:
  - 2005-12-31: 0.02323119024
  - 2006-12-31: -0.0813678018166
  - 2007-12-31: -0.0144802141955
  - 2008-12-31: -0.142301023804
  - 2009-12-31: 0.0612152927491
  - 2010-12-31: 0.00898269987778
  - 2011-12-31: -0.00845048588578
  - 2012-12-31: 0.0541362123146
  - 2013-12-31: 0.0158705967774
  - 2014-12-31: 0.0281978956007
===============================================================================
SharpeRatio:
  - sharperatio: -0.261214264357
===============================================================================
SQN:
  - sqn: -0.784558216044
  - trades: 45

ORCL

$ ./macdsystem.py --plot --dataset orcl
===============================================================================
TimeReturn:
  - 9999-12-31 23:59:59.999999: 0.24890384718
===============================================================================
TimeReturn:
  - 9999-12-31 23:59:59.999999: 2.23991354467
===============================================================================
TimeReturn:
  - 2005-12-31: -0.02372911952
  - 2006-12-31: 0.0692563226579
  - 2007-12-31: 0.0551086853554
  - 2008-12-31: -0.026707886256
  - 2009-12-31: 0.0786118091383
  - 2010-12-31: 0.037571919146
  - 2011-12-31: 0.00846519206845
  - 2012-12-31: 0.0402937469005
  - 2013-12-31: -0.0147124502187
  - 2014-12-31: 0.00710131291379
===============================================================================
SharpeRatio:
  - sharperatio: 0.359712552054
===============================================================================
SQN:
  - sqn: 1.76240859868
  - trades: 37

NVDA

$ ./macdsystem.py --plot --dataset nvda
===============================================================================
TimeReturn:
  - 9999-12-31 23:59:59.999999: -0.0178507058999
===============================================================================
TimeReturn:
  - 9999-12-31 23:59:59.999999: -0.177604931253
===============================================================================
TimeReturn:
  - 2005-12-31: 0.0773031957141
  - 2006-12-31: 0.105007457325
  - 2007-12-31: 0.015286423657
  - 2008-12-31: -0.109130552525
  - 2009-12-31: 0.14716076542
  - 2010-12-31: -0.0891638005423
  - 2011-12-31: -0.0788216550171
  - 2012-12-31: -0.0498231953066
  - 2013-12-31: -0.0119166712361
  - 2014-12-31: 0.00940493597076
===============================================================================
SharpeRatio:
  - sharperatio: -0.102967601564
===============================================================================
SQN:
  - sqn: -0.0700412395071
  - trades: 38

Analysis of Run 1

  • YHOO

    The 1st and 2nd TimeReturn analyzers (the strategy and the asset itself) show that The strategy has lost 7.11% whilst the asset in question has appreciated 31.67%.

    Not even worth having a look at the other analyzers

  • ORCL

    A 24.89% for the strategy, but paled by the 223.99% return of the asset itself.

    The SharpeRatio at 0.35 is still away from the usual minimum target of 1.

    The SQN returns a 1.76 which at least gets a grading in the Van K. Tharp scale: 1.6 - 1.9 Below Average.

  • NVDA

    In this case a -1.78% for the strategy and a -17.76% for the asset. The SharpeRatio, with -0.10 shows that even if the strategy has outperformed the asset, it would have been better to go with a 1% bank account.

    The SQN is obviously not even at the bottom of the scale.

Conclusions of Run 1

  • 1 great loss, a tie and a win which underperforms the asset. Not that successful.

Run 2: CashAlloc to 0.50

YHOO

$ ./macdsystem.py --plot --dataset yhoo --cashalloc 0.50
===============================================================================
TimeReturn:
  - 9999-12-31 23:59:59.999999: -0.20560369198
===============================================================================
TimeReturn:
  - 9999-12-31 23:59:59.999999: 0.316736183525
===============================================================================
TimeReturn:
  - 2005-12-31: 0.05517338686
  - 2006-12-31: -0.195123836162
  - 2007-12-31: -0.0441556438731
  - 2008-12-31: -0.32426212721
  - 2009-12-31: 0.153876836394
  - 2010-12-31: 0.0167157437151
  - 2011-12-31: -0.0202891373759
  - 2012-12-31: 0.13289763017
  - 2013-12-31: 0.0408192946307
  - 2014-12-31: 0.0685527133815
===============================================================================
SharpeRatio:
  - sharperatio: -0.154427699146
===============================================================================
SQN:
  - sqn: -0.97846453428
  - trades: 45

ORCL

$ ./macdsystem.py --plot --dataset orcl --cashalloc 0.50
===============================================================================
TimeReturn:
  - 9999-12-31 23:59:59.999999: 0.69016747856
===============================================================================
TimeReturn:
  - 9999-12-31 23:59:59.999999: 2.23991354467
===============================================================================
TimeReturn:
  - 2005-12-31: -0.0597533502
  - 2006-12-31: 0.176988400688
  - 2007-12-31: 0.140268851352
  - 2008-12-31: -0.0685193675128
  - 2009-12-31: 0.195760054561
  - 2010-12-31: 0.0956386594392
  - 2011-12-31: 0.018709882089
  - 2012-12-31: 0.100122407053
  - 2013-12-31: -0.0375741196261
  - 2014-12-31: 0.017570390931
===============================================================================
SharpeRatio:
  - sharperatio: 0.518921692742
===============================================================================
SQN:
  - sqn: 1.68844251174
  - trades: 37

NVDA

$ ./macdsystem.py --plot --dataset nvda --cashalloc 0.50
===============================================================================
TimeReturn:
  - 9999-12-31 23:59:59.999999: -0.128845648113
===============================================================================
TimeReturn:
  - 9999-12-31 23:59:59.999999: -0.177604931253
===============================================================================
TimeReturn:
  - 2005-12-31: 0.200593209479
  - 2006-12-31: 0.219254906522
  - 2007-12-31: 0.0407793562989
  - 2008-12-31: -0.259567975403
  - 2009-12-31: 0.380971100974
  - 2010-12-31: -0.208860409742
  - 2011-12-31: -0.189068154062
  - 2012-12-31: -0.122095056225
  - 2013-12-31: -0.0296495770432
  - 2014-12-31: 0.0232050942344
===============================================================================
SharpeRatio:
  - sharperatio: -0.0222780544339
===============================================================================
SQN:
  - sqn: -0.190661428812
  - trades: 38

Conclusions of Run 2

  • Increasing the percentage cash allocation in each operation from 20% to 50% has increased the effect of the previous results
    • The strategy on YHOO and NVDA has lost more than before
    • And the strategy on ORCL has won more than before, still not close to the over 220% of the asset.

Run 3: ATR Distance to 4.0

Still keeping the previous increase in cash allocation to 50%. The idea is to avoid getting too early out of the market.

YHOO

$ ./macdsystem.py --plot --dataset yhoo --cashalloc 0.50 --atrdist 4.0
===============================================================================
TimeReturn:
  - 9999-12-31 23:59:59.999999: 0.01196310622
===============================================================================
TimeReturn:
  - 9999-12-31 23:59:59.999999: 0.316736183525
===============================================================================
TimeReturn:
  - 2005-12-31: 0.06476232676
  - 2006-12-31: -0.220219327475
  - 2007-12-31: -0.0525484648039
  - 2008-12-31: -0.314772526784
  - 2009-12-31: 0.179631995594
  - 2010-12-31: 0.0579495723922
  - 2011-12-31: -0.0248948026947
  - 2012-12-31: 0.10922621981
  - 2013-12-31: 0.406711050602
  - 2014-12-31: -0.0113108751022
===============================================================================
SharpeRatio:
  - sharperatio: 0.0495181271704
===============================================================================
SQN:
  - sqn: -0.211652416441
  - trades: 33

ORCL

$ ./macdsystem.py --plot --dataset orcl --cashalloc 0.50 --atrdist 4.0
===============================================================================
TimeReturn:
  - 9999-12-31 23:59:59.999999: 0.21907748452
===============================================================================
TimeReturn:
  - 9999-12-31 23:59:59.999999: 2.23991354467
===============================================================================
TimeReturn:
  - 2005-12-31: -0.06660102614
  - 2006-12-31: 0.169334910265
  - 2007-12-31: 0.10620478758
  - 2008-12-31: -0.167615289704
  - 2009-12-31: 0.17616784045
  - 2010-12-31: 0.0591200431984
  - 2011-12-31: -0.100238247103
  - 2012-12-31: 0.135096589522
  - 2013-12-31: -0.0630483842399
  - 2014-12-31: 0.0175914485158
===============================================================================
SharpeRatio:
  - sharperatio: 0.144210776122
===============================================================================
SQN:
  - sqn: 0.646519270815
  - trades: 30

NVDA

$ ./macdsystem.py --plot --dataset nvda --cashalloc 0.50 --atrdist 4.0
===============================================================================
TimeReturn:
  - 9999-12-31 23:59:59.999999: 0.48840287049
===============================================================================
TimeReturn:
  - 9999-12-31 23:59:59.999999: -0.177604931253
===============================================================================
TimeReturn:
  - 2005-12-31: 0.246510998277
  - 2006-12-31: 0.194958106054
  - 2007-12-31: -0.123140650516
  - 2008-12-31: -0.246174938322
  - 2009-12-31: 0.33121185861
  - 2010-12-31: -0.0442212647256
  - 2011-12-31: 0.0368388717861
  - 2012-12-31: -0.048473112136
  - 2013-12-31: 0.10657587649
  - 2014-12-31: 0.0883112536534
===============================================================================
SharpeRatio:
  - sharperatio: 0.264551262551
===============================================================================
SQN:
  - sqn: 0.564151633588
  - trades: 29

Conclusions of Run 3

  • Chicken, Chicken, Winner Dinner!!

    The strategy makes money on the 3 assets

    • YHOO: 1.19% vs the 31.67% return of the asset itself

    • ORCL: 21.90% vs the 223.99% of the asset

      In this case increasing the ATRDist parameter has decreased the previous returns of Run 2 which were at 69.01%

    • NVDA: 48.84% vs -17.76% of the asset.

      The surprising thing here is that the SharpRatio and SQN are telling that

Usage of the sample

$ ./macdsystem.py --help
usage: macdsystem.py [-h] (--data DATA | --dataset {yhoo,orcl,nvda})
                     [--fromdate FROMDATE] [--todate TODATE] [--cash CASH]
                     [--cashalloc CASHALLOC] [--commperc COMMPERC]
                     [--macd1 MACD1] [--macd2 MACD2] [--macdsig MACDSIG]
                     [--atrperiod ATRPERIOD] [--atrdist ATRDIST]
                     [--smaperiod SMAPERIOD] [--dirperiod DIRPERIOD]
                     [--riskfreerate RISKFREERATE] [--plot [kwargs]]

Sample for Tharp example with MACD

optional arguments:
  -h, --help            show this help message and exit
  --data DATA           Specific data to be read in (default: None)
  --dataset {yhoo,orcl,nvda}
                        Choose one of the predefined data sets (default: None)
  --fromdate FROMDATE   Starting date in YYYY-MM-DD format (default:
                        2005-01-01)
  --todate TODATE       Ending date in YYYY-MM-DD format (default: None)
  --cash CASH           Cash to start with (default: 50000)
  --cashalloc CASHALLOC
                        Perc (abs) of cash to allocate for ops (default: 0.2)
  --commperc COMMPERC   Perc (abs) commision in each operation. 0.001 -> 0.1%,
                        0.01 -> 1% (default: 0.0033)
  --macd1 MACD1         MACD Period 1 value (default: 12)
  --macd2 MACD2         MACD Period 2 value (default: 26)
  --macdsig MACDSIG     MACD Signal Period value (default: 9)
  --atrperiod ATRPERIOD
                        ATR Period To Consider (default: 14)
  --atrdist ATRDIST     ATR Factor for stop price calculation (default: 3.0)
  --smaperiod SMAPERIOD
                        Period for the moving average (default: 30)
  --dirperiod DIRPERIOD
                        Period for SMA direction calculation (default: 10)
  --riskfreerate RISKFREERATE
                        Risk free rate in Perc (abs) of the asset for the
                        Sharpe Ratio (default: 0.01)
  --plot [kwargs], -p [kwargs]
                        Plot the read data applying any kwargs passed For
                        example: --plot style="candle" (to plot candles)
                        (default: None)

And the code itself

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)


import argparse
import datetime
import random

import backtrader as bt

BTVERSION = tuple(int(x) for x in bt.__version__.split('.'))


class FixedPerc(bt.Sizer):
    '''This sizer simply returns a fixed size for any operation

    Params:
      - ``perc`` (default: ``0.20``) Perc of cash to allocate for operation
    '''

    params = (
        ('perc', 0.20),  # perc of cash to use for operation
    )

    def _getsizing(self, comminfo, cash, data, isbuy):
        cashtouse = self.p.perc * cash
        if BTVERSION > (1, 7, 1, 93):
            size = comminfo.getsize(data.close[0], cashtouse)
        else:
            size = cashtouse // data.close[0]
        return size


class TheStrategy(bt.Strategy):
    '''
    This strategy is loosely based on some of the examples from the Van
    K. Tharp book: *Trade Your Way To Financial Freedom*. The logic:

      - Enter the market if:
        - The MACD.macd line crosses the MACD.signal line to the upside
        - The Simple Moving Average has a negative direction in the last x
          periods (actual value below value x periods ago)

     - Set a stop price x times the ATR value away from the close

     - If in the market:

       - Check if the current close has gone below the stop price. If yes,
         exit.
       - If not, update the stop price if the new stop price would be higher
         than the current
    '''

    params = (
        # Standard MACD Parameters
        ('macd1', 12),
        ('macd2', 26),
        ('macdsig', 9),
        ('atrperiod', 14),  # ATR Period (standard)
        ('atrdist', 3.0),   # ATR distance for stop price
        ('smaperiod', 30),  # SMA Period (pretty standard)
        ('dirperiod', 10),  # Lookback period to consider SMA trend direction
    )

    def notify_order(self, order):
        if order.status == order.Completed:
            pass

        if not order.alive():
            self.order = None  # indicate no order is pending

    def __init__(self):
        self.macd = bt.indicators.MACD(self.data,
                                       period_me1=self.p.macd1,
                                       period_me2=self.p.macd2,
                                       period_signal=self.p.macdsig)

        # Cross of macd.macd and macd.signal
        self.mcross = bt.indicators.CrossOver(self.macd.macd, self.macd.signal)

        # To set the stop price
        self.atr = bt.indicators.ATR(self.data, period=self.p.atrperiod)

        # Control market trend
        self.sma = bt.indicators.SMA(self.data, period=self.p.smaperiod)
        self.smadir = self.sma - self.sma(-self.p.dirperiod)

    def start(self):
        self.order = None  # sentinel to avoid operrations on pending order

    def next(self):
        if self.order:
            return  # pending order execution

        if not self.position:  # not in the market
            if self.mcross[0] > 0.0 and self.smadir < 0.0:
                self.order = self.buy()
                pdist = self.atr[0] * self.p.atrdist
                self.pstop = self.data.close[0] - pdist

        else:  # in the market
            pclose = self.data.close[0]
            pstop = self.pstop

            if pclose < pstop:
                self.close()  # stop met - get out
            else:
                pdist = self.atr[0] * self.p.atrdist
                # Update only if greater than
                self.pstop = max(pstop, pclose - pdist)


DATASETS = {
    'yhoo': '../../datas/yhoo-1996-2014.txt',
    'orcl': '../../datas/orcl-1995-2014.txt',
    'nvda': '../../datas/nvda-1999-2014.txt',
}


def runstrat(args=None):
    args = parse_args(args)

    cerebro = bt.Cerebro()
    cerebro.broker.set_cash(args.cash)
    comminfo = bt.commissions.CommInfo_Stocks_Perc(commission=args.commperc,
                                                   percabs=True)

    cerebro.broker.addcommissioninfo(comminfo)

    dkwargs = dict()
    if args.fromdate is not None:
        fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d')
        dkwargs['fromdate'] = fromdate

    if args.todate is not None:
        todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d')
        dkwargs['todate'] = todate

    # if dataset is None, args.data has been given
    dataname = DATASETS.get(args.dataset, args.data)
    data0 = bt.feeds.YahooFinanceCSVData(dataname=dataname, **dkwargs)
    cerebro.adddata(data0)

    cerebro.addstrategy(TheStrategy,
                        macd1=args.macd1, macd2=args.macd2,
                        macdsig=args.macdsig,
                        atrperiod=args.atrperiod,
                        atrdist=args.atrdist,
                        smaperiod=args.smaperiod,
                        dirperiod=args.dirperiod)

    cerebro.addsizer(FixedPerc, perc=args.cashalloc)

    # Add TimeReturn Analyzers for self and the benchmark data
    cerebro.addanalyzer(bt.analyzers.TimeReturn, _name='alltime_roi',
                        timeframe=bt.TimeFrame.NoTimeFrame)

    cerebro.addanalyzer(bt.analyzers.TimeReturn, data=data0, _name='benchmark',
                        timeframe=bt.TimeFrame.NoTimeFrame)

    # Add TimeReturn Analyzers fot the annuyl returns
    cerebro.addanalyzer(bt.analyzers.TimeReturn, timeframe=bt.TimeFrame.Years)
    # Add a SharpeRatio
    cerebro.addanalyzer(bt.analyzers.SharpeRatio, timeframe=bt.TimeFrame.Years,
                        riskfreerate=args.riskfreerate)

    # Add SQN to qualify the trades
    cerebro.addanalyzer(bt.analyzers.SQN)
    cerebro.addobserver(bt.observers.DrawDown)  # visualize the drawdown evol

    results = cerebro.run()
    st0 = results[0]

    for alyzer in st0.analyzers:
        alyzer.print()

    if args.plot:
        pkwargs = dict(style='bar')
        if args.plot is not True:  # evals to True but is not True
            npkwargs = eval('dict(' + args.plot + ')')  # args were passed
            pkwargs.update(npkwargs)

        cerebro.plot(**pkwargs)


def parse_args(pargs=None):

    parser = argparse.ArgumentParser(
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
        description='Sample for Tharp example with MACD')

    group1 = parser.add_mutually_exclusive_group(required=True)
    group1.add_argument('--data', required=False, default=None,
                        help='Specific data to be read in')

    group1.add_argument('--dataset', required=False, action='store',
                        default=None, choices=DATASETS.keys(),
                        help='Choose one of the predefined data sets')

    parser.add_argument('--fromdate', required=False,
                        default='2005-01-01',
                        help='Starting date in YYYY-MM-DD format')

    parser.add_argument('--todate', required=False,
                        default=None,
                        help='Ending date in YYYY-MM-DD format')

    parser.add_argument('--cash', required=False, action='store',
                        type=float, default=50000,
                        help=('Cash to start with'))

    parser.add_argument('--cashalloc', required=False, action='store',
                        type=float, default=0.20,
                        help=('Perc (abs) of cash to allocate for ops'))

    parser.add_argument('--commperc', required=False, action='store',
                        type=float, default=0.0033,
                        help=('Perc (abs) commision in each operation. '
                              '0.001 -> 0.1%%, 0.01 -> 1%%'))

    parser.add_argument('--macd1', required=False, action='store',
                        type=int, default=12,
                        help=('MACD Period 1 value'))

    parser.add_argument('--macd2', required=False, action='store',
                        type=int, default=26,
                        help=('MACD Period 2 value'))

    parser.add_argument('--macdsig', required=False, action='store',
                        type=int, default=9,
                        help=('MACD Signal Period value'))

    parser.add_argument('--atrperiod', required=False, action='store',
                        type=int, default=14,
                        help=('ATR Period To Consider'))

    parser.add_argument('--atrdist', required=False, action='store',
                        type=float, default=3.0,
                        help=('ATR Factor for stop price calculation'))

    parser.add_argument('--smaperiod', required=False, action='store',
                        type=int, default=30,
                        help=('Period for the moving average'))

    parser.add_argument('--dirperiod', required=False, action='store',
                        type=int, default=10,
                        help=('Period for SMA direction calculation'))

    parser.add_argument('--riskfreerate', required=False, action='store',
                        type=float, default=0.01,
                        help=('Risk free rate in Perc (abs) of the asset for '
                              'the Sharpe Ratio'))
    # Plot options
    parser.add_argument('--plot', '-p', nargs='?', required=False,
                        metavar='kwargs', const=True,
                        help=('Plot the read data applying any kwargs passed\n'
                              '\n'
                              'For example:\n'
                              '\n'
                              '  --plot style="candle" (to plot candles)\n'))

    if pargs is not None:
        return parser.parse_args(pargs)

    return parser.parse_args()


if __name__ == '__main__':
    runstrat()

Comments

Fork me on GitHub