Pairs Trading Script Example

Hello All,

I am wondering if someone has an example of how you would code a basic pairs trading strategy.

Specifically, I am wondering how can we have two symbols and create a long/short pairs between them.

Any help would be appreciated.

Thanks

Tagged:

Comments

  • shayneshayne Posts: 70

    these scripts can work for pairs and baskets. It's broken into two scripts (the entry and exit). Be sure to add the exit as a dependency to the entry.

    from cloudquant.interfaces import Strategy, Event
    
    ALGO_BUY = '{2b4fdc55-ff01-416e-a5ea-e1f1d4524c7d}'  # Buy Market ARCA
    ALGO_SELL = '{8fdee8fe-b772-46bd-b411-5544f7a0d917}'  # Sell Market ARCA
    
    class BasketEntry(Strategy):
        @classmethod
        def on_strategy_start(cls, md, service, account):
            symbol_dict = {'side': 'long',
                           'base_shares': 0,
                           'entry_shares': 0,
                           'exit_shares': 0,
                           'entry_price': 0,
                           'order_id': None,
                           'retries': 0
                           }
            cls.symbols = {'TQQQ': symbol_dict,
                           'AAPL': symbol_dict,
                           'AAL': symbol_dict,
                           'FB': symbol_dict,
                           'MSFT': symbol_dict,
                           'TGT': symbol_dict,
                           'WMT': symbol_dict,
                           'SPY': symbol_dict
                           }
            for count, symbol in enumerate(cls.symbols):
                if count % 2 == 0:
                    cls.symbols[symbol]["side"] = 'short'
    
            for symb in cls.symbols:
                print symb, cls.symbols[symb]
    
        @classmethod
        def is_symbol_qualified(cls, symbol, md, service, account):
            # create 1 instance
            return symbol == min(cls.symbols)
    
        @classmethod
        def backtesting_extra_symbols(cls, symbol, md, service, account):
            # load other symbols data
            return symbol in cls.symbols and symbol != min(cls.symbols)
    
        def entry_orders(self, event, md, order, service, account):
            for symbol in self.__class__.symbols:
                # self.state = 1
                # trade_data = {}
                order_quantity = abs(self.__class__.symbols[symbol]['base_shares'])
                if self.__class__.symbols[symbol]['side'] == 'long':
                    # -------------------- none --------------------
                    self.__class__.symbols[symbol]['order_id'] = order.algo_buy(symbol, ALGO_BUY, 'init',
                                                                                order_quantity=order_quantity)
                    # --------------------------------------------------
                elif self.__class__.symbols[symbol]['side'] == 'short':
                    # -------------------- none --------------------
                    self.__class__.symbols[symbol]['order_id'] = order.algo_sell(symbol, ALGO_SELL, 'init',
                                                                                 order_quantity=order_quantity)
                    # --------------------------------------------------
    
        def on_start(self, md, order, service, account):
            ###################################################################################
            #
            # The pair strategy runs as a single instance that funnels all events for desired symbols
            #
            ###################################################################################
            service.clear_event_triggers()
            # note the list of symbols
            service.add_event_trigger([symbol for symbol in self.__class__.symbols], [Event.TRADE, Event.FILL, Event.REJECT])
    
            self.state = 0
            dollar_of_capital = 100000
            for symbol in self.__class__.symbols:
                self.__class__.symbols[symbol]['base_shares'] = int(dollar_of_capital / md[symbol].stat.prev_close)
    
            # Sent entry basket orders at 9:35
            self.basket_entry_time = md.market_open_time + service.time_interval(minutes=5)
    
            # Exit Script Timer
            service.add_time_trigger(service.time(11,35))
    
        def on_trade(self, event, md, order, service, account):
            if self.state == 0 and event.timestamp >= self.basket_entry_time:
                self.state = 1
                self.entry_orders(event, md, order, service, account)
    
        def on_timer(self, event, md, order, service, account):
    
            # params = {}
            # params['symbols_dict'] = self.__class__.symbols
            service.start_strategy("BasketExit")
            print 'starting exit'#, params
    
        # called when an order is rejected (locally or other parts of the order processes e.g. the market)
        def on_reject(self, event, md, order, service, account):
            print event
            pass
    
    from cloudquant.interfaces import Strategy, Event
    
    ALGO_BUY = '{2b4fdc55-ff01-416e-a5ea-e1f1d4524c7d}'  # Buy Market ARCA
    ALGO_SELL = '{8fdee8fe-b772-46bd-b411-5544f7a0d917}'  # Sell Market ARCA
    
    class BasketExit(Strategy):
    
        @classmethod
        def on_strategy_start(cls, md, service, account):
            cls.symb_positions = account.positions
            print 'Inside BasketExit - on_strategy_start'
    
        @classmethod
        def is_symbol_qualified(cls, symbol, md, service, account):
            return symbol == min(cls.symb_positions)
    
        @classmethod
        def backtesting_extra_symbols(cls, symbol, md, service, account):
            # load other symbols data
            return symbol in cls.symb_positions and symbol != min(cls.symb_positions)
    
        def on_start(self, md, order, service, account):
            print 'Inside BasketExit - on_start for symbol ', self.symbol
            print account.positions
            ###################################################################################
            #
            # The pair strategy runs as a single instance that funnels all events for desired symbols
            #
            ###################################################################################
    
            for symbol in account.positions:
                if account[symbol].position.shares > 0:
                    self.sell_order_id = order.algo_sell(symbol, ALGO_SELL, 'exit')
    
                elif account[symbol].position.shares < 0:
                    self.buy_order_id = order.algo_buy(symbol, ALGO_BUY, 'exit')
    
            service.terminate()
    
  • jsmchangjsmchang Posts: 6
    edited February 2017

    Hi everyone,

    I am new to both cloudquant and Python, so please bear with me.

    I am trying to implement a strategy that involves trading a basket of stocks. The pair trading script could be a good initial template for me to start experimenting with the various features on cloudquant. So I gave it a try.

    I tried to backtest on a single day using the entry and exit scripts. However, I bumped into a couple of issues and hope that you can give me some guidance. The issues are:

    (1) The entry script seems to have finished running, trying to call the exit script. But that triggered an error. The message on the console is:

    Traceback (most recent call last):
      File "run_simulation.py", line 532, in <module>
        main(sys.argv[1:])
      File "run_simulation.py", line 501, in main
        results = simulator()
      File "/opt/anaconda/lib/python2.7/site-packages/tradesim/engine.py", line 494, in __call__
        return self.simulate()
      File "/opt/anaconda/lib/python2.7/site-packages/tradesim/engine.py", line 637, in simulate
        order_listener=self.order_listener)
      File "/opt/anaconda/lib/python2.7/site-packages/tradesim/engine.py", line 797, in _handle_new_strategies
        new_strategy = strategy_classes[new_config.strategy](**params)
    KeyError: 'BasketExit'
    

    I have set up the exit script as a dependency to the entry script. It is not clear to me what caused the error.

    (2) I noticed that the values inside the symbols dictionary are identical across all symbols. i.e. even though the entry script tries to assign half the symbols as long, and half the symbols as short, the 'side' of all symbols were set to short. Similarly for the 'base shares', which should be different for each symbol, but it ended up becoming identical across all symbols.

    I added some print statements to check how the content of the symbols dictionary changed when new values were assigned into individual fields. It appears that whenever a value was assign to a field for one symbol, the value got populated to the same field across all symbols. Not sure what caused this behavior.

    I wonder if there may be an updated version of this pair trading script that I can take a look. Thanks for your help.

  • David_BelvedereDavid_Belvedere Posts: 19
    edited February 2017

    Hey James,

    I also had some issues with the scripts provided, but I managed to work through the bugs and have a working script. The PairEntry and PairExit scripts are below. To use them, I created third script called PairScript, which handles all the logic in terms of when to enter. When I have a pair, I just run this code:

    params['long_symbol'] =  self.
    params['short_symbol'] = self.symbol 
    service.start_strategy("pairentry", parameters=params)
    

    For the example posted below, I hardcoded a pair into PairEntry so you can run it, and have the code commented that I use when running PairScript.

    I found it much more useful to use the account object to track positions instead of self.variables

    Hope this helps!

    from cloudquant.interfaces import Strategy, Event
    
    class PairEntry(Strategy):
        __script_name__ = 'PairEntry'
    
        @classmethod
        def backtesting_extra_symbols(cls, symbol, md, service, account):
            #return symbol == params['short_symbol']
            # to ensure hedge is loaded
            return symbol == 'SPY'
    
        @classmethod
        def is_symbol_qualified(cls, symbol, md, service, account):
            #return symbol == params['long_symbol'] 
            return symbol == 'TQQQ'
    
        def __init__(self, **params):
            self.long_symbol = 'TQQQ'
            if 'long_symbol' in params:
                self.long_symbol = params['long_symbol']
            self.short_symbol = 'SPY'
            if 'short_symbol' in params:
                self.short_symbol = params['short_symbol']
    
    
    
        def on_start(self, md, order, service, account):
            ###################################################################################
            #
            # The pair strategy runs as a single instance that funnels all events for desired symbols
            #
            ###################################################################################
            service.clear_event_triggers()
            # note the list of symbols
            service.add_event_trigger([self.long_symbol, self.short_symbol], [Event.TRADE, Event.CANCEL, Event.FILL, Event.REJECT])
    
    
            self.state = 0
            dollar_of_capital = 50000
            self.long_desired_shares = int(dollar_of_capital/md[self.long_symbol].stat.prev_close)
            self.short_desired_shares = -int(dollar_of_capital/md[self.short_symbol].stat.prev_close)
            self.long_shares = 0
            self.short_shares = 0
            self.long_order_id = None
            self.short_order_id = None
            self.long_entry_price = 0
            self.short_entry_price = 0
            #-------------------- none --------------------
    
    
    
        def on_trade(self, event, md, order, service, account):
            if (self.long_order_id is None and self.short_order_id is None and self.state==0):
                self.state=1 # only run once
                orderShares = abs(self.long_desired_shares)
                voodooGuid = '{2b4fdc55-ff01-416e-a5ea-e1f1d4524c7d}' # Buy Market ARCA
                self.long_order_id = order.algo_buy( self.long_symbol, "market", 'init', order_quantity=orderShares)
                print "entering long"
    
                orderShares = abs(self.short_desired_shares)
                voodooGuid = '{8fdee8fe-b772-46bd-b411-5544f7a0d917}' # Sell Market ARCA
                self.short_order_id = order.algo_sell( self.short_symbol, "market", 'init', order_quantity=orderShares)
                print "entering short"
    
    
        def on_fill(self, event, md, order, service, account):
            print event
            #self.long_shares += event.shares
            self.long_entry_price = account[self.long_symbol].position.entry_price
            #print "long proce", self.long_entry_price#, event.price
    
            #self.short_shares += event.shares
            self.short_entry_price = account[self.short_symbol].position.entry_price
            #print "short price", self.short_entry_price# event.price
    
            #print account[self.long_symbol].position.shares, self.long_desired_shares, account[self.short_symbol].position.shares, self.short_desired_shares
            ###################################################################################
            #
            # When both orders are filled to the desired position, start the exit script
            #
            ###################################################################################
            #print account[self.long_symbol].position.shares, (self.long_desired_shares), account[self.short_symbol].position.shares ,(self.short_desired_shares)
            if ((account[self.long_symbol].position.shares == (self.long_desired_shares)) and (account[self.short_symbol].position.shares == (self.short_desired_shares))) :
                params = {}
                params['long_symbol'] =  self.long_symbol
                params['long_shares'] = self.long_desired_shares
                params['long_entry_price'] = self.long_entry_price
                params['short_symbol'] = self.short_symbol
                params['short_shares'] = self.short_desired_shares
                params['short_entry_price'] = self.short_entry_price
    
    
                service.start_strategy("pairexit", parameters=params)
                print 'starting exit', params
                service.terminate()
    
        def on_cancel(self, event, md, order, service, account):
            print service.time_to_string(event.timestamp), 'cancel', event.symbol
    
            ###################################################################################
            #
            # in the event that one of the side cancels, a couple of times to cover the position
            #
            ###################################################################################
            if ((event.intent == 'init' or event.intent == 'increase') and account[self.long_symbol].position.shares != (self.long_desired_shares)) :
                voodooGuid = '{2b4fdc55-ff01-416e-a5ea-e1f1d4524c7d}' # Buy Market ARCA
                orderShares = abs(self.long_desired_shares)
                #print abs(self.long_desired_shares), self.symbol
                self.long_order_id = order.algo_buy( self.long_symbol, "market", 'increase', position_size=orderShares)
    
            if ((event.intent == 'init' or event.intent == 'increase') and account[self.short_symbol].position.shares != (self.short_desired_shares)) :
                voodooGuid = '{8fdee8fe-b772-46bd-b411-5544f7a0d917}' # Sell Market ARCA
                orderShares = abs(self.short_desired_shares)
                self.short_order_id = order.algo_sell( self.short_symbol, "market", 'increase', position_size=orderShares)
    
        def on_reject(self,event,md,order,service,account):
            print event.reason, self.state
    
  • David_BelvedereDavid_Belvedere Posts: 19
    edited February 2017
    from cloudquant.interfaces import Strategy, Event
    
    class PairExit(Strategy):
        __script_name__ = 'pairexit'
    
        @classmethod
        def is_symbol_qualified(cls, symbol, md, service, account):
            return False 
    
        def pair_pl_calc(self, md, account):
            long_pl = (md[self.long_symbol].L1.bid - self.long_entry_price) * self.long_shares 
            short_pl = (self.short_entry_price - md[self.short_symbol].L1.ask) * abs(self.short_shares)
            #print 'long\t%s\t%s\t%s\t%s\t%s' % (self.long_symbol, md[self.long_symbol].L1.bid, self.long_entry_price, self.long_shares, long_pl)
            #print 'short\t%s\t%s\t%s\t%s\t%s' % (self.short_symbol, self.short_entry_price, md[self.short_symbol].L1.ask, self.short_shares, short_pl)
            return long_pl + short_pl
    
        def cover_long(self, event, md, order, service, account):
            #-------------------- none --------------------
            orderShares = abs(self.long_shares)
            voodooGuid = '{8fdee8fe-b772-46bd-b411-5544f7a0d917}' # Sell Market ARCA
            self.sell_order_id = order.algo_sell( self.long_symbol, "market", 'exit')
            #--------------------------------------------------
    
        def cover_short(self, event, md, order, service, account):  
            #-------------------- none --------------------
            orderShares = abs(self.short_shares)
            voodooGuid = '{2b4fdc55-ff01-416e-a5ea-e1f1d4524c7d}' # Buy Market ARCA
            self.buy_order_id = order.algo_buy( self.short_symbol, "market", 'exit')
            #--------------------------------------------------
    
        def __init__(self, **params):
            print params
            ###################################################################################
            #
            # parameters passed from the entry
            #
            ###################################################################################
            self.long_symbol = params['long_symbol']
            self.long_shares = params['long_shares'] 
            self.long_entry_price = params['long_entry_price']
            self.short_symbol = params['short_symbol']
            self.short_shares = params['short_shares']
            self.short_entry_price = params['short_entry_price']
    
    
    
    
        def on_start(self, md, order, service, account):
            ###################################################################################
            #
            # The pair strategy runs as a single instance that funnels all events for desired symbols
            #
            ###################################################################################
            service.clear_event_triggers()
            service.add_event_trigger([self.long_symbol, self.short_symbol], [Event.TRADE, Event.CANCEL, Event.FILL, Event.REJECT])
    
            self.state = 0
            self.sell_order_id = None
            self.buy_order_id = None
            self.retries_long = 0
            self.retries_short = 0
            print self.long_shares*self.long_entry_price + abs(self.short_shares)*self.short_entry_price
    
            # cover at the end of the day if still open
            service.add_time_trigger(md.market_close_time - service.time_interval(minutes=5))
    
        def on_trade(self, event, md, order, service, account):
            pl = self.pair_pl_calc(md, account)
            #print service.time_to_string(event.timestamp), pl
            #
    
            if ((((pl) > (self.long_shares*self.long_entry_price + abs(self.short_shares)*self.short_entry_price)*0.005)
                or ((pl) < (self.long_shares*self.long_entry_price + abs(self.short_shares)*self.short_entry_price)*-0.005))
                and self.state == 0):
                self.state=1
                print self.long_symbol, self.short_symbol, pl
                self.cover_long(event, md, order, service, account)
                self.cover_short(event, md, order, service, account)
    
        def on_cancel(self, event, md, order, service, account):
            print service.time_to_string(event.timestamp), 'cancel', event.symbol, event.reason
    
            ###################################################################################
            #
            # in the event that one of the side cancels, a couple of times to cover the position
            #
            ###################################################################################
            if (account[self.symbol].position.shares > 0 and event.intent == 'exit') :
                print "from cancel, cover long"
                self.cover_long(event, md, order, service, account)
    
            if (account[self.symbol].position.shares < 0 and event.intent == 'exit') :
                print "from cancel, cover short"
                self.cover_short(event, md, order, service, account)
    
        def on_fill(self, event, md, order, service, account):
            #print event.order_id, self.sell_order_id, self.buy_order_id
            print event.shares, event.symbol
            if (event.shares<0 and event.intent == 'exit') :
                #some code
                self.long_shares += event.shares
    
            if (event.shares>0 and event.intent == 'exit') :
                #some code
                self.short_shares += event.shares
    
            #print self.long_shares
            #print self.short_shares
            ###################################################################################
            #
            # Terminate script when both positions are flat
            #
            ###################################################################################
            if (account[self.short_symbol].position.shares == (0)) and (account[self.long_symbol].position.shares == (0)) :
                self.entry_time = None
                service.terminate()
    
        def on_reject(self, event, md, order, service, account):
            print service.time_to_string(event.timestamp), 'reject', event.symbol, event.reason
            #print self.long_shares, self.short_shares
    
            ###################################################################################
            #
            # in the event that one of the side rejects, a couple of times to cover the position
            #
            ###################################################################################
            if ((event.order_id) == (self.sell_order_id)) and ((self.retries_long) < (10) and abs(self.long_shares) > 0) :
                self.retries_long += 1
                self.cover_long(event, md, order, service, account)
    
            if ((event.order_id) == (self.buy_order_id)) and ((self.retries_short) < (10) and abs(self.short_shares) > 0) :
                self.retries_short += 1
                self.cover_short(event, md, order, service, account)
    
        def on_timer(self, event, md, order, service, account):
            if ((self.state) == (0)) :
                self.state = 1
                self.cover_long(event, md, order, service, account)
                self.cover_short(event, md, order, service, account)
    
  • Hi David,

    Thank you so much. You made my day.

    James

  • David,

    Thanks for the scripts you posted above! I cannot add the exit script as a dependency. Is there something special I have to do (like a path name or directory)? Alternatively, is there some way to combine your scripts into one?

    My apologies for the naive questions. I'm new to CloudQuant and new to Python. Any help would be much appreciated! All the best.

    Dave

  • carcarcarcar Posts: 5

    Would anyone be willing to help me one on one? I am learning how to program. willing to pay if it makes sense

Sign In or Register to comment.