├── alpaca_pairs_trading.py ├── readme.rst └── requirements.txt /alpaca_pairs_trading.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from time import sleep 3 | 4 | import alpaca_trade_api as tradeapi 5 | import pandas as pd 6 | 7 | base_url = 'https://paper-api.alpaca.markets' 8 | data_url = 'wss://data.alpaca.markets' 9 | trade_taken = False 10 | 11 | # instantiate REST API 12 | api = tradeapi.REST(base_url=base_url, api_version='v2') 13 | 14 | # init WebSocket 15 | conn = tradeapi.stream2.StreamConn( 16 | base_url=base_url, data_url=data_url, data_stream='alpacadatav1' 17 | ) 18 | 19 | 20 | def wait_for_market_open(): 21 | clock = api.get_clock() 22 | if not clock.is_open: 23 | time_to_open = clock.next_open - clock.timestamp 24 | sleep_time = round(time_to_open.total_seconds()) 25 | sleep(sleep_time) 26 | return clock 27 | 28 | 29 | # define websocket callbacks 30 | data_df = None 31 | 32 | 33 | @conn.on(r'^T.ENZL$') 34 | async def on_second_bars_EWN(conn, channel, bar): 35 | if data_df is not None: 36 | data_df.enzl[-1] = bar.price 37 | 38 | 39 | @conn.on(r'^T.EWA$') 40 | async def on_second_bars_ENZL(conn, channel, bar): 41 | if data_df is not None: 42 | data_df.ewa[-1] = bar.price 43 | 44 | 45 | # start WebSocket in a thread 46 | streams = ['T.ENZL', 'T.EWA'] 47 | ws_thread = threading.Thread(target=conn.run, daemon=True, args=(streams,)) 48 | ws_thread.start() 49 | 50 | 51 | # main 52 | while True: 53 | 54 | clock = wait_for_market_open() 55 | 56 | ewa = api.get_barset('EWA', 'day', limit=25) 57 | enzl = api.get_barset('ENZL', 'day', limit=25) 58 | 59 | data_df = pd.concat( 60 | [ewa.df.EWA.close, enzl.df.ENZL.close], 61 | axis=1, 62 | join='inner', 63 | keys=['ewa', 'enzl'], 64 | ) 65 | data_df.enzl[-1] = 0 66 | data_df.ewa[-1] = 0 67 | 68 | spread_df = data_df.pct_change() 69 | spread_df = spread_df[:-1] 70 | spread_df['spread'] = spread_df.ewa - spread_df.enzl 71 | max_divergence = spread_df.spread.tail(20).abs().max() 72 | 73 | while data_df.enzl[-1] == 0 or data_df.ewa[-1] == 0: 74 | sleep(1) 75 | 76 | 77 | while not trade_taken: 78 | # check for trade opportunities 79 | spread_df = data_df.pct_change()[:2] 80 | spread_df['spread'] = spread_df.ewa - spread_df.enzl 81 | 82 | if abs(spread_df.spread[-1]) > max_divergence: 83 | # there is a trade - calculate position sizing 84 | acct = api.get_account() 85 | acct_size = float(acct.equity) 86 | ewa_size = round(acct_size / data_df.ewa[-1]) 87 | enzl_size = round(acct_size / data_df.enzl[-1]) 88 | 89 | if spread_df.spread[-1] < 0: 90 | # EWA - ENZL is negative -> Long EWA short ENZL 91 | long_ewa = True 92 | ewa_side = 'buy' 93 | enzl_side = 'sell' 94 | 95 | else: 96 | # EWA - ENZL is positive -> Short EWA long ENZL 97 | long_ewa = False 98 | ewa_side = 'sell' 99 | enzl_side = 'buy' 100 | 101 | # submit order 102 | api.submit_order( 103 | symbol='EWA', 104 | qty=ewa_size, 105 | side=ewa_side, 106 | time_in_force='gtc', 107 | type='market', 108 | ) 109 | 110 | api.submit_order( 111 | symbol='ENZL', 112 | qty=enzl_size, 113 | side=enzl_side, 114 | time_in_force='gtc', 115 | type='market', 116 | ) 117 | 118 | trade_taken = True 119 | stop_loss = acct_size * 0.02 * -1 120 | take_profit = acct_size * max_divergence 121 | break 122 | 123 | sleep(1) 124 | 125 | # check if the market is still open 126 | if pd.Timestamp.now(tz='America/New_York') > clock.next_close: 127 | trade_taken = False 128 | break 129 | 130 | 131 | while trade_taken: 132 | # In a trade - check for exit 133 | pnl = data_df.ewa[-1] * ewa_size - data_df.enzl[-1] * enzl_size 134 | 135 | if not long_ewa: 136 | pnl *= -1 # inverse the p&l calculation 137 | 138 | if pnl < stop_loss or pnl > take_profit: 139 | # Either stop or take profit hit - close trade 140 | api.close_position('EWA') 141 | api.close_position('ENZL') 142 | trade_taken = False 143 | break 144 | 145 | if pd.Timestamp.now(tz='America/New_York') > clock.next_close: 146 | break 147 | 148 | -------------------------------------------------------------------------------- /readme.rst: -------------------------------------------------------------------------------- 1 | ================================================================ 2 | Pairs Trading - Strategy Deployment Guide - alpaca.markets/learn 3 | ================================================================ 4 | 5 | This is the code used in the article: **Pairs Trading - Strategy Deployment Guide** published on the `alpaca.markets/learn `_ blog. The article can be found here - https://alpaca.markets/learn/pairs-trading/ 6 | 7 | This guide was created on behalf of AlgoTrading101.com 8 | 9 | 10 | ----------------- 11 | Table of Contents 12 | ----------------- 13 | 14 | * `What Is Pairs Trading? `_ 15 | * `Why should I run a pairs trading strategy? `_ 16 | * `Why shouldn’t I run a pairs trading strategy? `_ 17 | * `How do I get started with Alpaca? `_ 18 | * `How to code a pairs trading strategy with the Alpaca API? `_ 19 | * `How do we size our trades? `_ 20 | * `How do I set a stop loss or take profit? `_ 21 | * `How can I build a successful pairs trading strategy? `_ 22 | 23 | 24 | 25 | 26 | 27 | ------------ 28 | Requirements 29 | ------------ 30 | 31 | * `python `_ >= 3.6+ 32 | * `alpaca_trade_api `_ (tested to work with >= 0.51 ) 33 | * `pandas `_ >= 1.1.2+ 34 | 35 | ----------- 36 | Author Info 37 | ----------- 38 | 39 | :author: Jignesh Davda 40 | :author page: https://algotrading101.com/learn/author/jdavda/ 41 | :published: 2020-12-02 42 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pandas==1.1.2 2 | alpaca_trade_api==0.51.0 3 | --------------------------------------------------------------------------------