Mean reversion basket

Discussion in 'Strategy Building' started by ajensen, Apr 13, 2017.

  1. ajensen

    ajensen

    For a group of M related stocks that I specify, I would like to backtest the strategy of buying at the close, each day, equal dollar amounts of the N stocks that are most oversold, defined as being the most below (on a percentage basis) their P-day moving averages. (For example, I could own the 5 stocks out 50 that are most below their 21-day MA, in which case M = 50, N = 5, and P = 21.)

    I would like to be able to test two versions -- on which assumes you can use the closing price to determine the basket, and a more realistic version that uses yesterday's closing price.

    What software would you use for this?
     
  2. Hey, I'm messing around with backtrader which could handle this pretty easily. Combine this with quandl's free closing data, you could probably get a long ways towards it.

    Edit: And of course, the 800 pound gorilla: Quantopian could do this easily.
     
    VPhantom, Gynesis and ajensen like this.
  3. ajensen

    ajensen

    Thanks. Backtrader https://github.com/mementum/backtrader looks especially useful to me since it's in Python, which I use.
     
    VPhantom, ab_trader and nooby_mcnoob like this.
  4. I'll second Quantopian. Python based, good data. I've used it in the past for an ETF rotation strategy I was developing.
     
  5. Zzzz1

    Zzzz1

    The finest granularity data are minute bars? And only US securities? Just curious. Thanks.

     
  6. i960

    i960

    If you want sub-minute granularity on US and things outside the US, I think it's a given you're gonna either pay $$$ up or have to come up with something on your own.

    Why would you need sub-minute anyway? It's not like it's possible to even compete within that space. If it's for precision reasons, I'd imagine it's very much following the rule of diminishing returns.
     
  7. Zzzz1

    Zzzz1

    Thanks. My expertise lies in order book analytics and higher frequency algorithm analysis and trading. I want to potentially expand into different asset classes and test existing strategies on data sets but am not prepared nor willing to shell out close 100,000 USD for data sets at the moment. Should the economics work out I have no problem to invest in collocation. My primary interest though at the moment lies in finding cloud based solutions to rent rather than own data sets I need for my backtest environment.

     
  8. Affordable rental for exactly your purpose here: https://www.quantgo.com/

    Only problem is you can't download the data, but you should be at least able to determine whether the data is worthwhile for you to purchase.
     
    ajensen likes this.
  9. Zzzz1

    Zzzz1

    Thanks a lot, I spoke with the guy at quant go and it did not meet my needs in terms of available data and contract specifics though pricing seemed OK. I don't intend to download data but only look to upload my testing environment and run the data on their aws instances. So, the quant go setup seems very interesting at first in terms of technology implementation but the data they advertise is either not available or not in the granularity advertised. That was a bit of a disappointment

     
  10. This is the strategy logic in *backtrader* to do it. There aren't many requirements in the original post (for example there is no exit criterion), so the assumption is that there is an unlimited supply of cash, which can be added to the broker for the new acquisitions.

    Cheat-on-Close: when a *Market* order is sent (default), *backtrader* tries to buy with the next incoming price, which for a daily bar situation is the next opening price. By activating *cheat-on-close*, one can buy with the current closing price.

    In all-in strategies the following can happen when cheat-on-close is false and the next day open price is used:

    • The size, which is calculated with the last known price (the close) may actually be too much for the current cash reserves in the broker if there is an opening gap to the upside. The order will be rejected. In this case, up to 5 orders can be sent and if opening gaps take place, the last order is the one that can undergo that behavior.
    Of course the strategy below needs to be executed inside a cerebro instance, which loads the data feeds.
    Code:
    class BuyMNP(bt.Strategy):
        params = dict(
            P=21,    # Average Period
            N=5,     # Number To buy
            A=1000,  # Monetary units to buy
            movav=bt.ind.SMA,  # Moving Average to apply
            cheat_on_close=False,
        )
    
        def __init__(self):
            # Decide if buy on today's close or next incoming price (open next)
            self.broker.set_coc(self.p.cheat_on_close)
    
            # Put the smas in a temporary dict indexed by data
            smas = {d: self.p.movav(d, period=self.p.P) for d in self.datas}
    
            # dict with the most oversold (%) indexed by data
            self.mos = {d: 1.0 - d.close / smas[d] for d in self.datas}
    
        def next(self):
            # Get a (data, %) list with the 5 which are most oversold today
            mos = sorted(self.mos.items(), key=lambda x: x[1], reverse=True)
    
            # Buy a max of N and only if below the moving average
            mosN = [d for d, perc in mos[:self.p.N] if perc > 0.0]
    
            # Unlimited cash supply ... add to the broker - to keep on buying
            self.broker.cash += self.p.A * len(mosN)
    
            # Execute the buy - no need to keep the order
            for d in mosN:
                self.buy(data=d, size=int(self.p.A / d.close))
                print(self.datetime.date(), d._name, d.close[0])  # some feedback
    
    The complete code (there is a placeholder for the strategy) to make this run (with a cash start of 100.000 to avoid running into the opening gap problem)

    Code:
    from __future__ import (absolute_import, division, print_function,
                            unicode_literals)
    
    import argparse
    import datetime
    
    import backtrader as bt
    
    TICKERS = ['IBM', 'MSFT', 'YHOO', 'ORCL', 'NVDA',
               'GOOG', 'AAPL', 'AMZN', 'INTC', 'MCD']
    
    ########################
    # PUT YOUR STRATEGY HERE
    ########################
    
    def runstrat(args=None):
        args = parse_args(args)
    
        cerebro = bt.Cerebro()
    
        # Data feed kwargs
        kwargs = dict()
    
        # Parse from/to-date
        dtfmt, tmfmt = '%Y-%m-%d', 'T%H:%M:%S'
        for a, d in ((getattr(args, x), x) for x in ['fromdate', 'todate']):
            if a:
                strpfmt = dtfmt + tmfmt * ('T' in a)
                kwargs[d] = datetime.datetime.strptime(a, strpfmt)
    
        # Data feed
        for dticker in args.datas:
            data = bt.feeds.YahooFinanceData(dataname=dticker, **kwargs)
            cerebro.adddata(data)
    
        # Broker
        cerebro.broker = bt.brokers.BackBroker(**eval('dict(' + args.broker + ')'))
    
        # Strategy
        cerebro.addstrategy(BuyMNP, **eval('dict(' + args.strat + ')'))
    
        # Execute
        cerebro.run(**eval('dict(' + args.cerebro + ')'))
    
        if args.plot:  # Plot if requested to
            cerebro.plot(**eval('dict(' + args.plot + ')'))
    
    
    def parse_args(pargs=None):
        parser = argparse.ArgumentParser(
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            description=('Mean Reversion Basket'))
    
        parser.add_argument('--datas', default=TICKERS,
                            required=False, help='Data tickers to read in')
    
        # Defaults for dates
        parser.add_argument('--fromdate', required=False, default='',
                            help='Date[time] in YYYY-MM-DD[THH:MM:SS] format')
    
        parser.add_argument('--todate', required=False, default='',
                            help='Date[time] in YYYY-MM-DD[THH:MM:SS] format')
    
        parser.add_argument('--cerebro', required=False, default='',
                            metavar='kwargs', help='kwargs in key=value format')
    
        parser.add_argument('--broker', required=False, default='cash=100000',
                            metavar='kwargs', help='kwargs in key=value format')
    
        parser.add_argument('--strat', required=False, default='',
                            metavar='kwargs', help='kwargs in key=value format')
    
        parser.add_argument('--plot', required=False, default='',
                            nargs='?', const='{}',
                            metavar='kwargs', help='kwargs in key=value format')
    
        return parser.parse_args(pargs)
    
    
    if __name__ == '__main__':
        runstrat()
    
     
    Last edited: Apr 14, 2017
    #10     Apr 14, 2017
    ajensen likes this.