├── renovate.json ├── .gitignore ├── Pipfile ├── README.md ├── tick_taker.py └── Pipfile.lock /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .envrc 3 | .zipline 4 | .pyversion 5 | .vscode 6 | .coverage 7 | .python-version 8 | .pytest_cache 9 | algo-state.pkl 10 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | alpaca-trade-api = ">=0.24" 8 | 9 | [dev-packages] 10 | pytest = "*" 11 | pytest-cov = "*" 12 | 13 | [requires] 14 | python_version = "3.6" 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Example HFT-ish Algorithm for Alpaca Trading API 2 | 3 | The aim of this algorithm is to capture slight moves in the bid/ask spread 4 | as they happen. It is only intended to work for high-volume stocks where there 5 | are frequent moves of 1 cent exactly. It is one of the trading strategies 6 | based on order book imbalance. For more details about it, please refer to 7 | [Darryl Shen, 2015](http://eprints.maths.ox.ac.uk/1895/1/Darryl%20Shen%20%28for%20archive%29.pdf) 8 | or other online articles. 9 | 10 | This algorithm will make many trades on the same security each day, so any 11 | account running it will quickly encounter PDT rules. Please make sure your 12 | account balance is well above $25,000 before running this script in a live 13 | environment. 14 | 15 | This script also presents a basic framework for streaming-based algorithms. 16 | You can learn how to write your algorithm based on the real-time price updates. 17 | 18 | ## Setup 19 | 20 | This algorithm runs with Python 3.6 or above. It uses 21 | [Alpaca Python SDK](https://pypi.org/project/alpaca-trade-api/) so make 22 | sure install it beforehand, or if you have [pipenv](https://pipenv.readthedocs.io), 23 | you can install it by 24 | 25 | ```sh 26 | $ pipenv install 27 | ``` 28 | 29 | in this directory. 30 | 31 | ## API Key 32 | 33 | In order to run this algorithm, you have to have Alpaca Trading API key. 34 | Please obtain it from the dashboard and set it in enviroment variables. 35 | 36 | ```sh 37 | export APCA_API_KEY_ID= 38 | export APCA_API_SECRET_KEY= 39 | ``` 40 | 41 | ## Run 42 | 43 | ``` 44 | $ python ./tick_taker.py 45 | ``` 46 | 47 | The parameters are following. 48 | 49 | - `--symbol`: the stock to trade (defaults to "SNAP") 50 | - `--quantity`: the maximum number of shares to hold at once. Note that this does not account for any existing position; the algorithm only tracks what is bought as part of its execution. (Default 500, minimum 100.) 51 | - `--key-id`: your API key ID. (Can also be set via the APCA_API_KEY_ID environment variable.) 52 | - `--secret-key`: your API key secret. (Can also be set via the APCA_API_SECRET_KEY environment variable.) 53 | - `--base-url`: the URL to connect to. (Can also be set via the APCA_API_BASE_URL environment variable. Defaults to "https://paper-api.alpaca.markets" if using a paper account key, "https://api.alpaca.markets" otherwise.) 54 | 55 | The algorithm can be stopped at any time by sending a keyboard interrupt `CTRL+C` to the console. (You may need to send two `CTRL+C` commands to kill the process depending where in the execution you catch it.) 56 | 57 | ## Note 58 | 59 | Please also note that this algorithm uses the Polygon streaming API with Alpaca API key, 60 | so you have to have a live trading account setup. For more details about the data 61 | requirements, please see 62 | [Alpaca documentation](https://docs.alpaca.markets/web-api/market-data/). 63 | -------------------------------------------------------------------------------- /tick_taker.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import pandas as pd 3 | import numpy as np 4 | import alpaca_trade_api as tradeapi 5 | 6 | 7 | class Quote(): 8 | """ 9 | We use Quote objects to represent the bid/ask spread. When we encounter a 10 | 'level change', a move of exactly 1 penny, we may attempt to make one 11 | trade. Whether or not the trade is successfully filled, we do not submit 12 | another trade until we see another level change. 13 | 14 | Note: Only moves of 1 penny are considered eligible because larger moves 15 | could potentially indicate some newsworthy event for the stock, which this 16 | algorithm is not tuned to trade. 17 | """ 18 | 19 | def __init__(self): 20 | self.prev_bid = 0 21 | self.prev_ask = 0 22 | self.prev_spread = 0 23 | self.bid = 0 24 | self.ask = 0 25 | self.bid_size = 0 26 | self.ask_size = 0 27 | self.spread = 0 28 | self.traded = True 29 | self.level_ct = 1 30 | self.time = 0 31 | 32 | def reset(self): 33 | # Called when a level change happens 34 | self.traded = False 35 | self.level_ct += 1 36 | 37 | def update(self, data): 38 | # Update bid and ask sizes and timestamp 39 | self.bid_size = data.bidsize 40 | self.ask_size = data.asksize 41 | 42 | # Check if there has been a level change 43 | if ( 44 | self.bid != data.bidprice 45 | and self.ask != data.askprice 46 | and round(data.askprice - data.bidprice, 2) == .01 47 | ): 48 | # Update bids and asks and time of level change 49 | self.prev_bid = self.bid 50 | self.prev_ask = self.ask 51 | self.bid = data.bidprice 52 | self.ask = data.askprice 53 | self.time = data.timestamp 54 | # Update spreads 55 | self.prev_spread = round(self.prev_ask - self.prev_bid, 3) 56 | self.spread = round(self.ask - self.bid, 3) 57 | print( 58 | 'Level change:', self.prev_bid, self.prev_ask, 59 | self.prev_spread, self.bid, self.ask, self.spread, flush=True 60 | ) 61 | # If change is from one penny spread level to a different penny 62 | # spread level, then initialize for new level (reset stale vars) 63 | if self.prev_spread == 0.01: 64 | self.reset() 65 | 66 | 67 | class Position(): 68 | """ 69 | The position object is used to track how many shares we have. We need to 70 | keep track of this so our position size doesn't inflate beyond the level 71 | we're willing to trade with. Because orders may sometimes be partially 72 | filled, we need to keep track of how many shares are "pending" a buy or 73 | sell as well as how many have been filled into our account. 74 | """ 75 | 76 | def __init__(self): 77 | self.orders_filled_amount = {} 78 | self.pending_buy_shares = 0 79 | self.pending_sell_shares = 0 80 | self.total_shares = 0 81 | 82 | def update_pending_buy_shares(self, quantity): 83 | self.pending_buy_shares += quantity 84 | 85 | def update_pending_sell_shares(self, quantity): 86 | self.pending_sell_shares += quantity 87 | 88 | def update_filled_amount(self, order_id, new_amount, side): 89 | old_amount = self.orders_filled_amount[order_id] 90 | if new_amount > old_amount: 91 | if side == 'buy': 92 | self.update_pending_buy_shares(old_amount - new_amount) 93 | self.update_total_shares(new_amount - old_amount) 94 | else: 95 | self.update_pending_sell_shares(old_amount - new_amount) 96 | self.update_total_shares(old_amount - new_amount) 97 | self.orders_filled_amount[order_id] = new_amount 98 | 99 | def remove_pending_order(self, order_id, side): 100 | old_amount = self.orders_filled_amount[order_id] 101 | if side == 'buy': 102 | self.update_pending_buy_shares(old_amount - 100) 103 | else: 104 | self.update_pending_sell_shares(old_amount - 100) 105 | del self.orders_filled_amount[order_id] 106 | 107 | def update_total_shares(self, quantity): 108 | self.total_shares += quantity 109 | 110 | 111 | def run(args): 112 | symbol = args.symbol 113 | max_shares = args.quantity 114 | opts = {} 115 | if args.key_id: 116 | opts['key_id'] = args.key_id 117 | if args.secret_key: 118 | opts['secret_key'] = args.secret_key 119 | if args.base_url: 120 | opts['base_url'] = args.base_url 121 | elif 'key_id' in opts and opts['key_id'].startswith('PK'): 122 | opts['base_url'] = 'https://paper-api.alpaca.markets' 123 | # Create an API object which can be used to submit orders, etc. 124 | api = tradeapi.REST(**opts) 125 | 126 | symbol = symbol.upper() 127 | quote = Quote() 128 | qc = 'Q.%s' % symbol 129 | tc = 'T.%s' % symbol 130 | position = Position() 131 | 132 | # Establish streaming connection 133 | conn = tradeapi.StreamConn(**opts) 134 | 135 | # Define our message handling 136 | @conn.on(r'Q\.' + symbol) 137 | async def on_quote(conn, channel, data): 138 | # Quote update received 139 | quote.update(data) 140 | 141 | @conn.on(r'T\.' + symbol) 142 | async def on_trade(conn, channel, data): 143 | if quote.traded: 144 | return 145 | # We've received a trade and might be ready to follow it 146 | if ( 147 | data.timestamp <= ( 148 | quote.time + pd.Timedelta(np.timedelta64(50, 'ms')) 149 | ) 150 | ): 151 | # The trade came too close to the quote update 152 | # and may have been for the previous level 153 | return 154 | if data.size >= 100: 155 | # The trade was large enough to follow, so we check to see if 156 | # we're ready to trade. We also check to see that the 157 | # bid vs ask quantities (order book imbalance) indicate 158 | # a movement in that direction. We also want to be sure that 159 | # we're not buying or selling more than we should. 160 | if ( 161 | data.price == quote.ask 162 | and quote.bid_size > (quote.ask_size * 1.8) 163 | and ( 164 | position.total_shares + position.pending_buy_shares 165 | ) < max_shares - 100 166 | ): 167 | # Everything looks right, so we submit our buy at the ask 168 | try: 169 | o = api.submit_order( 170 | symbol=symbol, qty='100', side='buy', 171 | type='limit', time_in_force='day', 172 | limit_price=str(quote.ask) 173 | ) 174 | # Approximate an IOC order by immediately cancelling 175 | api.cancel_order(o.id) 176 | position.update_pending_buy_shares(100) 177 | position.orders_filled_amount[o.id] = 0 178 | print('Buy at', quote.ask, flush=True) 179 | quote.traded = True 180 | except Exception as e: 181 | print(e) 182 | elif ( 183 | data.price == quote.bid 184 | and quote.ask_size > (quote.bid_size * 1.8) 185 | and ( 186 | position.total_shares - position.pending_sell_shares 187 | ) >= 100 188 | ): 189 | # Everything looks right, so we submit our sell at the bid 190 | try: 191 | o = api.submit_order( 192 | symbol=symbol, qty='100', side='sell', 193 | type='limit', time_in_force='day', 194 | limit_price=str(quote.bid) 195 | ) 196 | # Approximate an IOC order by immediately cancelling 197 | api.cancel_order(o.id) 198 | position.update_pending_sell_shares(100) 199 | position.orders_filled_amount[o.id] = 0 200 | print('Sell at', quote.bid, flush=True) 201 | quote.traded = True 202 | except Exception as e: 203 | print(e) 204 | 205 | @conn.on(r'trade_updates') 206 | async def on_trade_updates(conn, channel, data): 207 | # We got an update on one of the orders we submitted. We need to 208 | # update our position with the new information. 209 | event = data.event 210 | if event == 'fill': 211 | if data.order['side'] == 'buy': 212 | position.update_total_shares( 213 | int(data.order['filled_qty']) 214 | ) 215 | else: 216 | position.update_total_shares( 217 | -1 * int(data.order['filled_qty']) 218 | ) 219 | position.remove_pending_order( 220 | data.order['id'], data.order['side'] 221 | ) 222 | elif event == 'partial_fill': 223 | position.update_filled_amount( 224 | data.order['id'], int(data.order['filled_qty']), 225 | data.order['side'] 226 | ) 227 | elif event == 'canceled' or event == 'rejected': 228 | position.remove_pending_order( 229 | data.order['id'], data.order['side'] 230 | ) 231 | 232 | conn.run( 233 | ['trade_updates', tc, qc] 234 | ) 235 | 236 | 237 | if __name__ == '__main__': 238 | parser = argparse.ArgumentParser() 239 | parser.add_argument( 240 | '--symbol', type=str, default='SNAP', 241 | help='Symbol you want to trade.' 242 | ) 243 | parser.add_argument( 244 | '--quantity', type=int, default=500, 245 | help='Maximum number of shares to hold at once. Minimum 100.' 246 | ) 247 | parser.add_argument( 248 | '--key-id', type=str, default=None, 249 | help='API key ID', 250 | ) 251 | parser.add_argument( 252 | '--secret-key', type=str, default=None, 253 | help='API secret key', 254 | ) 255 | parser.add_argument( 256 | '--base-url', type=str, default=None, 257 | help='set https://paper-api.alpaca.markets if paper trading', 258 | ) 259 | args = parser.parse_args() 260 | assert args.quantity >= 100 261 | run(args) 262 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "b8f19302adc8833906939f083f9b7c9db3de517788c876204d89caa707b70263" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "alpaca-trade-api": { 20 | "hashes": [ 21 | "sha256:308100a49579e1890c2ef3e73e335fd4f0c63e82adf1e5ee88fb98d3d895d26c", 22 | "sha256:42fd823d7c881abdc9f011b1f324287e2a9fe0afeadbdf70c7e121c250d5d2dd" 23 | ], 24 | "index": "pypi", 25 | "version": "==0.24" 26 | }, 27 | "asyncio-nats-client": { 28 | "hashes": [ 29 | "sha256:c36e464a33e2d1bb59437b68ad74051f9f3113969108e4f8008b1e3fb5a2969f" 30 | ], 31 | "version": "==0.9.2" 32 | }, 33 | "certifi": { 34 | "hashes": [ 35 | "sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939", 36 | "sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695" 37 | ], 38 | "version": "==2019.6.16" 39 | }, 40 | "chardet": { 41 | "hashes": [ 42 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 43 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 44 | ], 45 | "version": "==3.0.4" 46 | }, 47 | "idna": { 48 | "hashes": [ 49 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", 50 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" 51 | ], 52 | "version": "==2.8" 53 | }, 54 | "numpy": { 55 | "hashes": [ 56 | "sha256:03e311b0a4c9f5755da7d52161280c6a78406c7be5c5cc7facfbcebb641efb7e", 57 | "sha256:0cdd229a53d2720d21175012ab0599665f8c9588b3b8ffa6095dd7b90f0691dd", 58 | "sha256:312bb18e95218bedc3563f26fcc9c1c6bfaaf9d453d15942c0839acdd7e4c473", 59 | "sha256:464b1c48baf49e8505b1bb754c47a013d2c305c5b14269b5c85ea0625b6a988a", 60 | "sha256:5adfde7bd3ee4864536e230bcab1c673f866736698724d5d28c11a4d63672658", 61 | "sha256:7724e9e31ee72389d522b88c0d4201f24edc34277999701ccd4a5392e7d8af61", 62 | "sha256:8d36f7c53ae741e23f54793ffefb2912340b800476eb0a831c6eb602e204c5c4", 63 | "sha256:910d2272403c2ea8a52d9159827dc9f7c27fb4b263749dca884e2e4a8af3b302", 64 | "sha256:951fefe2fb73f84c620bec4e001e80a80ddaa1b84dce244ded7f1e0cbe0ed34a", 65 | "sha256:9588c6b4157f493edeb9378788dcd02cb9e6a6aeaa518b511a1c79d06cbd8094", 66 | "sha256:9ce8300950f2f1d29d0e49c28ebfff0d2f1e2a7444830fbb0b913c7c08f31511", 67 | "sha256:be39cca66cc6806652da97103605c7b65ee4442c638f04ff064a7efd9a81d50a", 68 | "sha256:c3ab2d835b95ccb59d11dfcd56eb0480daea57cdf95d686d22eff35584bc4554", 69 | "sha256:eb0fc4a492cb896346c9e2c7a22eae3e766d407df3eb20f4ce027f23f76e4c54", 70 | "sha256:ec0c56eae6cee6299f41e780a0280318a93db519bbb2906103c43f3e2be1206c", 71 | "sha256:f4e4612de60a4f1c4d06c8c2857cdcb2b8b5289189a12053f37d3f41f06c60d0" 72 | ], 73 | "version": "==1.17.0" 74 | }, 75 | "pandas": { 76 | "hashes": [ 77 | "sha256:074a032f99bb55d178b93bd98999c971542f19317829af08c99504febd9e9b8b", 78 | "sha256:20f1728182b49575c2f6f681b3e2af5fac9e84abdf29488e76d569a7969b362e", 79 | "sha256:2745ba6e16c34d13d765c3657bb64fa20a0e2daf503e6216a36ed61770066179", 80 | "sha256:32c44e5b628c48ba17703f734d59f369d4cdcb4239ef26047d6c8a8bfda29a6b", 81 | "sha256:3b9f7dcee6744d9dcdd53bce19b91d20b4311bf904303fa00ef58e7df398e901", 82 | "sha256:544f2033250980fb6f069ce4a960e5f64d99b8165d01dc39afd0b244eeeef7d7", 83 | "sha256:58f9ef68975b9f00ba96755d5702afdf039dea9acef6a0cfd8ddcde32918a79c", 84 | "sha256:9023972a92073a495eba1380824b197ad1737550fe1c4ef8322e65fe58662888", 85 | "sha256:914341ad2d5b1ea522798efa4016430b66107d05781dbfe7cf05eba8f37df995", 86 | "sha256:9d151bfb0e751e2c987f931c57792871c8d7ff292bcdfcaa7233012c367940ee", 87 | "sha256:b932b127da810fef57d427260dde1ad54542c136c44b227a1e367551bb1a684b", 88 | "sha256:cfb862aa37f4dd5be0730731fdb8185ac935aba8b51bf3bd035658111c9ee1c9", 89 | "sha256:de7ecb4b120e98b91e8a2a21f186571266a8d1faa31d92421e979c7ca67d8e5c", 90 | "sha256:df7e1933a0b83920769611c5d6b9a1bf301e3fa6a544641c6678c67621fe9843" 91 | ], 92 | "version": "==0.25.0" 93 | }, 94 | "python-dateutil": { 95 | "hashes": [ 96 | "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", 97 | "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" 98 | ], 99 | "version": "==2.8.0" 100 | }, 101 | "pytz": { 102 | "hashes": [ 103 | "sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32", 104 | "sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7" 105 | ], 106 | "version": "==2019.2" 107 | }, 108 | "requests": { 109 | "hashes": [ 110 | "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", 111 | "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" 112 | ], 113 | "version": "==2.22.0" 114 | }, 115 | "six": { 116 | "hashes": [ 117 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 118 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 119 | ], 120 | "version": "==1.12.0" 121 | }, 122 | "urllib3": { 123 | "hashes": [ 124 | "sha256:4c291ca23bbb55c76518905869ef34bdd5f0e46af7afe6861e8375643ffee1a0", 125 | "sha256:9a247273df709c4fedb38c711e44292304f73f39ab01beda9f6b9fc375669ac3" 126 | ], 127 | "index": "pypi", 128 | "version": "==1.24.2" 129 | }, 130 | "websocket-client": { 131 | "hashes": [ 132 | "sha256:1151d5fb3a62dc129164292e1227655e4bbc5dd5340a5165dfae61128ec50aa9", 133 | "sha256:1fd5520878b68b84b5748bb30e592b10d0a91529d5383f74f4964e72b297fd3a" 134 | ], 135 | "version": "==0.56.0" 136 | }, 137 | "websockets": { 138 | "hashes": [ 139 | "sha256:049e694abe33f8a1d99969fee7bfc0ae6761f7fd5f297c58ea933b27dd6805f2", 140 | "sha256:73ce69217e4655783ec72ce11c151053fcbd5b837cc39de7999e19605182e28a", 141 | "sha256:83e63aa73331b9ca21af61df8f115fb5fbcba3f281bee650a4ad16a40cd1ef15", 142 | "sha256:882a7266fa867a2ebb2c0baaa0f9159cabf131cf18c1b4270d79ad42f9208dc5", 143 | "sha256:8c77f7d182a6ea2a9d09c2612059f3ad859a90243e899617137ee3f6b7f2b584", 144 | "sha256:8d7a20a2f97f1e98c765651d9fb9437201a9ccc2c70e94b0270f1c5ef29667a3", 145 | "sha256:a7affaeffbc5d55681934c16bb6b8fc82bb75b175e7fd4dcca798c938bde8dda", 146 | "sha256:c82e286555f839846ef4f0fdd6910769a577952e1e26aa8ee7a6f45f040e3c2b", 147 | "sha256:e906128532a14b9d264a43eb48f9b3080d53a9bda819ab45bf56b8039dc606ac", 148 | "sha256:e9102043a81cdc8b7c8032ff4bce39f6229e4ac39cb2010946c912eeb84e2cb6", 149 | "sha256:f5cb2683367e32da6a256b60929a3af9c29c212b5091cf5bace9358d03011bf5" 150 | ], 151 | "version": "==8.0.2" 152 | } 153 | }, 154 | "develop": { 155 | "atomicwrites": { 156 | "hashes": [ 157 | "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", 158 | "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" 159 | ], 160 | "version": "==1.3.0" 161 | }, 162 | "attrs": { 163 | "hashes": [ 164 | "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", 165 | "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" 166 | ], 167 | "version": "==19.1.0" 168 | }, 169 | "coverage": { 170 | "hashes": [ 171 | "sha256:08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", 172 | "sha256:0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", 173 | "sha256:141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", 174 | "sha256:19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", 175 | "sha256:23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", 176 | "sha256:245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", 177 | "sha256:331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", 178 | "sha256:386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", 179 | "sha256:3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", 180 | "sha256:60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", 181 | "sha256:63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", 182 | "sha256:6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", 183 | "sha256:6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", 184 | "sha256:7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", 185 | "sha256:826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", 186 | "sha256:93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", 187 | "sha256:9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", 188 | "sha256:af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", 189 | "sha256:bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", 190 | "sha256:bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", 191 | "sha256:c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", 192 | "sha256:dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", 193 | "sha256:df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", 194 | "sha256:e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", 195 | "sha256:e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", 196 | "sha256:e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", 197 | "sha256:eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", 198 | "sha256:eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", 199 | "sha256:ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", 200 | "sha256:efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", 201 | "sha256:fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", 202 | "sha256:ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025" 203 | ], 204 | "version": "==4.5.4" 205 | }, 206 | "importlib-metadata": { 207 | "hashes": [ 208 | "sha256:23d3d873e008a513952355379d93cbcab874c58f4f034ff657c7a87422fa64e8", 209 | "sha256:80d2de76188eabfbfcf27e6a37342c2827801e59c4cc14b0371c56fed43820e3" 210 | ], 211 | "version": "==0.19" 212 | }, 213 | "more-itertools": { 214 | "hashes": [ 215 | "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", 216 | "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" 217 | ], 218 | "version": "==7.2.0" 219 | }, 220 | "pluggy": { 221 | "hashes": [ 222 | "sha256:0825a152ac059776623854c1543d65a4ad408eb3d33ee114dff91e57ec6ae6fc", 223 | "sha256:b9817417e95936bf75d85d3f8767f7df6cdde751fc40aed3bb3074cbcb77757c" 224 | ], 225 | "version": "==0.12.0" 226 | }, 227 | "py": { 228 | "hashes": [ 229 | "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", 230 | "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" 231 | ], 232 | "version": "==1.8.0" 233 | }, 234 | "pytest": { 235 | "hashes": [ 236 | "sha256:41568ea7ecb4a68d7f63837cf65b92ce8d0105e43196ff2b26622995bb3dc4b2", 237 | "sha256:c3c573a29d7c9547fb90217ece8a8843aa0c1328a797e200290dc3d0b4b823be" 238 | ], 239 | "index": "pypi", 240 | "version": "==4.1.1" 241 | }, 242 | "pytest-cov": { 243 | "hashes": [ 244 | "sha256:0ab664b25c6aa9716cbf203b17ddb301932383046082c081b9848a0edf5add33", 245 | "sha256:230ef817450ab0699c6cc3c9c8f7a829c34674456f2ed8df1fe1d39780f7c87f" 246 | ], 247 | "index": "pypi", 248 | "version": "==2.6.1" 249 | }, 250 | "six": { 251 | "hashes": [ 252 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 253 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 254 | ], 255 | "version": "==1.12.0" 256 | }, 257 | "zipp": { 258 | "hashes": [ 259 | "sha256:4970c3758f4e89a7857a973b1e2a5d75bcdc47794442f2e2dd4fe8e0466e809a", 260 | "sha256:8a5712cfd3bb4248015eb3b0b3c54a5f6ee3f2425963ef2a0125b8bc40aafaec" 261 | ], 262 | "version": "==0.5.2" 263 | } 264 | } 265 | } 266 | --------------------------------------------------------------------------------