def instruments(self) -> List[Instrument]: """construct a list of instruments from the coinbase-returned json list of instruments""" ret = [] # This will fetch a list of pairs products = self._products() for product in products: # separate pair into base and quote first = product["base_currency"] second = product["quote_currency"] # for each pair, construct both underlying currencies as well # as the pair object ret.append( Instrument( name="{}-{}".format(first, second), type=InstrumentType.PAIR, exchange=self.exchange, brokerId=product["id"], leg1=self.currency(first), leg2=self.currency(second), leg1_side=Side.BUY, leg2_side=Side.SELL, )) return ret
async def onStart(self, event: Event) -> None: # Create an instrument inst = Instrument(name=self._symbol, type=InstrumentType(self._symbol_type)) # Subscribe await self.subscribe(inst) print("Subscribing to {}".format(inst))
def accounts(self): '''fetch a list of coinbase accounts. These store quantities of InstrumentType.CURRENCY''' ret = [] # fetch all accounts accounts = self._accounts() # if unauthorized or invalid api key, raise if accounts == { 'message': 'Unauthorized.' } or accounts == { 'message': 'Invalid API Key' }: raise Exception('Coinbase auth failed') # for each account for account in accounts: # grab the id to lookup info acc_data = self._account(account['id']) # if tradeable and positive balance if acc_data['trading_enabled'] and float(acc_data['balance']) > 0: # construct a position representing the balance # acc = Account(account['id'], exchange, [ pos = Position( float(acc_data['balance']), 0., datetime.now(), Instrument(acc_data['currency'], InstrumentType.CURRENCY, exchange=self.exchange), self.exchange, []) ret.append(pos) # ] # ) # ret.append(acc) return ret
async def onStart(self, event: Event) -> None: # Get available instruments from exchange await self.subscribe( Instrument( name=self._symbol, type=InstrumentType.EQUITY, exchange=ExchangeType("iex"), ))
async def onStart(self, event: Event) -> None: # Create an instrument inst = Instrument( name=self._symbol, type=InstrumentType.EQUITY, exchange=ExchangeType("iex") ) # Subscribe await self.subscribe(inst) print("Subscribing to {}".format(inst))
async def onStart(self, event: Event) -> None: # Create an instrument inst = Instrument(name=self._symbol, type=InstrumentType.EQUITY) # Check that its available if inst not in self.instruments(): raise Exception("Not available on exchange: {}".format( self._symbol)) # Subscribe await self.subscribe(inst) print("Subscribing to {}".format(inst))
async def onStart(self, event: Event) -> None: # Get available instruments from exchange for name in ('MSFT', 'AAPL'): # Create an instrument inst = Instrument(name=name, type=InstrumentType.EQUITY) insts = await self.lookup(inst) # Check that its available if inst not in insts: raise Exception('Not available on exchange: {}'.format(name)) # Subscribe self.subscribe(inst) print('Subscribing to {}'.format(inst))
async def onStart(self, event: Event) -> None: # Get available instruments from exchange insts = self.instruments() print('Available Instruments:\n{}'.format(insts)) for name in ('FB', 'AMZN', 'NFLX', 'GOOG', 'MSFT', 'AAPL', 'NVDA'): # Create an instrument inst = Instrument(name=name, type=InstrumentType.EQUITY) # Check that its available if inst not in insts: raise Exception('Not available on exchange: {}'.format(name)) # Subscribe self.subscribe(inst) print('Subscribing to {}'.format(inst))
def _get_instruments(public_client, exchange): ret = [] products = public_client.get_products() for product in products: first = product['base_currency'] second = product['quote_currency'] ret.append( Instrument(name='{}/{}'.format(first, second), type=InstrumentType.PAIR, exchange=exchange, leg1=_get_currency(first), leg2=_get_currency(second), leg1_side=Side.BUY, leg2_side=Side.SELL)) return ret
async def accounts(self) -> List[Position]: """fetch a list of coinbase accounts. These store quantities of InstrumentType.CURRENCY""" ret = [] # fetch all accounts accounts = self._accounts() # if unauthorized or invalid api key, raise if accounts == { "message": "Unauthorized." } or accounts == { "message": "Invalid API Key" }: raise Exception("Coinbase auth failed") # for each account for account in accounts: # grab the id to lookup info acc_data = self._account(account["id"]) # if tradeable and positive balance if acc_data["trading_enabled"] and float(acc_data["balance"]) > 0: # construct a position representing the balance # acc = Account(account['id'], exchange, [ pos = Position( float(acc_data["balance"]), 0.0, datetime.now(), Instrument( acc_data["currency"], InstrumentType.CURRENCY, exchange=self.exchange, ), self.exchange, [], ) ret.append(pos) # ] # ) # ret.append(acc) return ret
async def onStart(self, event: Event): # Get available instruments from exchange self.subscribe( Instrument(name=self._symbol, type=InstrumentType.EQUITY))
async def websocket(self, subscriptions: List[Instrument]): # type: ignore # copy the base subscription template subscription = _SUBSCRIPTION.copy() # for each subcription, add symbol to product_ids for sub in subscriptions: subscription["product_ids"].append(sub.brokerId) # type: ignore # sign the message in a similar way to the rest api, but # using the message of GET/users/self/verify timestamp = str(time.time()) message = timestamp + "GET/users/self/verify" hmac_key = base64.b64decode(self.secret_key) signature = hmac.new(hmac_key, message.encode(), hashlib.sha256) signature_b64 = base64.b64encode(signature.digest()).decode() # update the subscription message with the signing info subscription.update({ "signature": signature_b64, "timestamp": timestamp, "key": self.api_key, "passphrase": self.passphrase, }) # construct a new websocket session session = aiohttp.ClientSession() # connect to the websocket async with session.ws_connect(self.ws_url) as ws: # send the subscription await ws.send_str(json.dumps(subscription)) # for each message returned async for msg in ws: # only handle text messages if msg.type == aiohttp.WSMsgType.TEXT: # load the data as json x = json.loads(msg.data) # skip earlier messages that our order book # already reflects if "sequence" in x: inst = Instrument(x["product_id"], InstrumentType.PAIR, self.exchange) if x.get("sequence", float("inf")) < self.seqnum.get( inst, 0): # if msg has a sequence number, and that number is < the last sequence number # of the order book snapshot, ignore continue # ignore subscription and heartbeat messages if x["type"] in ("subscriptions", "heartbeat"): # TODO yield heartbeats? continue elif x["type"] == "received": # generate new Open events # A valid order has been received and is now active. # This message is emitted for every single valid order as # soon as the matching engine receives it whether it fills # immediately or not. # # The received message does not indicate a resting order on # the order book. It simply indicates a new incoming order # which as been accepted by the matching engine for processing. # Received orders may cause match message to follow if they # are able to begin being filled (taker behavior). Self-trade # prevention may also trigger change messages to follow if the # order size needs to be adjusted. Orders which are not fully # filled or canceled due to self-trade prevention result in an # open message and become resting orders on the order book. # # Market orders (indicated by the order_type field) may have # an optional funds field which indicates how much quote currency # will be used to buy or sell. For example, a funds field of # 100.00 for the BTC-USD product would indicate a purchase of # up to 100.00 USD worth of bitcoin. # # { # "type": "received", # "time": "2014-11-07T08:19:27.028459Z", # "product_id": "BTC-USD", # "sequence": 10, # "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", # "size": "1.34", # "price": "502.1", # "side": "buy", # "order_type": "limit" # } # { # "type": "received", # "time": "2014-11-09T08:19:27.028459Z", # "product_id": "BTC-USD", # "sequence": 12, # "order_id": "dddec984-77a8-460a-b958-66f114b0de9b", # "funds": "3000.234", # "side": "buy", # "order_type": "market" # } id = x["order_id"] # FIXME make sure we dont need this # if id in self._order_map: # # yield a received event and get order from dict # o = self._order_map[id] # yield Event(type=EventType.RECEIVED, target=o) if x["order_type"] == "market": if "size" in x and float(x["size"]) <= 0: # ignore zero size orders # TODO why do we even get these? continue elif "size" not in x and "funds" in x: print("TODO: funds") # TODO can't handle these yet, no mapping from funds to size/price continue # create a market data order from the event data # TODO set something for price? float('inf') ? o = Order( float(x["size"]), 0.0, Side(x["side"].upper()), Instrument(x["product_id"], InstrumentType.PAIR, self.exchange), self.exchange, id=id, ) else: # create limit order from the event data o = Order( float(x["size"]), float(x["price"]), Side(x["side"].upper()), Instrument(x["product_id"], InstrumentType.PAIR, self.exchange), self.exchange, ) # yield an open event for the new order e = Event(type=EventType.OPEN, target=o) yield e elif x["type"] == "done": # The order is no longer on the order book. Sent for # all orders for which there was a received message. # This message can result from an order being canceled # or filled. There will be no more messages for this # order_id after a done message. remaining_size indicates # how much of the order went unfilled; this will # be 0 for filled orders. # # market orders will not have a remaining_size or price # field as they are never on the open order book at a # given price. # # { # "type": "done", # "time": "2014-11-07T08:19:27.028459Z", # "product_id": "BTC-USD", # "sequence": 10, # "price": "200.2", # "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", # "reason": "filled", // or "canceled" # "side": "sell", # "remaining_size": "0" # } if x["reason"] == "canceled": id = x["order_id"] # if cancelled if "price" not in x: # cancel this event if we have a full local order book # where we can determine the original order print("TODO: noprice") continue # FIXME don't use remaining_size, lookup original size in order book o = Order( float(x["remaining_size"]), float(x["price"]), Side(x["side"].upper()), Instrument( x["product_id"], InstrumentType.PAIR, self.exchange, ), self.exchange, id=id, ) e = Event(type=EventType.CANCEL, target=o) yield e elif x["reason"] == "filled": # Will have a match event # TODO route these to full local order book continue else: # TODO unhandled # this should never print print("TODO: unhandled", x) elif x["type"] == "match": # A trade occurred between two orders. The aggressor # or taker order is the one executing immediately # after being received and the maker order is a # resting order on the book. The side field indicates # the maker order side. If the side is sell this # indicates the maker was a sell order and the match # is considered an up-tick. A buy side match is a down-tick. # # If authenticated, and you were the taker, the message # would also have the following fields: # taker_user_id: "5844eceecf7e803e259d0365", # user_id: "5844eceecf7e803e259d0365", # taker_profile_id: "765d1549-9660-4be2-97d4-fa2d65fa3352", # profile_id: "765d1549-9660-4be2-97d4-fa2d65fa3352", # taker_fee_rate: "0.005" # # Similarly, if you were the maker, the message would have the following: # maker_user_id: "5f8a07f17b7a102330be40a3", # user_id: "5f8a07f17b7a102330be40a3", # maker_profile_id: "7aa6b75c-0ff1-11eb-adc1-0242ac120002", # profile_id: "7aa6b75c-0ff1-11eb-adc1-0242ac120002", # maker_fee_rate: "0.001" # { # "type": "match", # "trade_id": 10, # "sequence": 50, # "maker_order_id": "ac928c66-ca53-498f-9c13-a110027a60e8", # "taker_order_id": "132fb6ae-456b-4654-b4e0-d681ac05cea1", # "time": "2014-11-07T08:19:27.028459Z", # "product_id": "BTC-USD", # "size": "5.23512", # "price": "400.23", # "side": "sell" # } # Generate a trade event # First, create an order from the event if str(x.get("taker_order_id", "")) in self._order_map: o = self._order_map[str(x.get("taker_order_id"))] o.filled = float(x["size"]) # my order mine = True elif str(x.get("maker_order_id", "")) in self._order_map: o = self._order_map[str(x.get("maker_order_id"))] # TODO filled? # my order mine = True else: o = Order( float(x["size"]), float(x["price"]), Side(x["side"].upper()), Instrument(x["product_id"], InstrumentType.PAIR, self.exchange), self.exchange, ) # set filled to volume so we see it as "done" o.filled = o.volume # not my order mine = False # create a trader with this order as the taker # makers would be accumulated via the # `elif x['reason'] == 'filled'` block above t = Trade( float(x["size"]), float(x["price"]), taker_order=o, maker_orders=[], ) if mine: t.my_order = o e = Event(type=EventType.TRADE, target=t) yield e elif x["type"] == "open": # The order is now open on the order book. # This message will only be sent for orders # which are not fully filled immediately. # remaining_size will indicate how much of # the order is unfilled and going on the book. # { # "type": "open", # "time": "2014-11-07T08:19:27.028459Z", # "product_id": "BTC-USD", # "sequence": 10, # "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", # "price": "200.2", # "remaining_size": "1.00", # "side": "sell" # } # TODO how are these differentiated from received? o = Order( float(x["remaining_size"]), float(x["price"]), Side(x["side"].upper()), Instrument(x["product_id"], InstrumentType.PAIR, self.exchange), self.exchange, ) e = Event(type=EventType.OPEN, target=o) yield e elif x["type"] == "change": # TODO # An order has changed. This is the result # of self-trade prevention adjusting the # order size or available funds. Orders can # only decrease in size or funds. change # messages are sent anytime an order changes # in size; this includes resting orders (open) # as well as received but not yet open. # change messages are also sent when a new # market order goes through self trade prevention # and the funds for the market order have changed. # { # "type": "change", # "time": "2014-11-07T08:19:27.028459Z", # "sequence": 80, # "order_id": "ac928c66-ca53-498f-9c13-a110027a60e8", # "product_id": "BTC-USD", # "new_size": "5.23512", # "old_size": "12.234412", # "price": "400.23", # "side": "sell" # } # { # "type": "change", # "time": "2014-11-07T08:19:27.028459Z", # "sequence": 80, # "order_id": "ac928c66-ca53-498f-9c13-a110027a60e8", # "product_id": "BTC-USD", # "new_funds": "5.23512", # "old_funds": "12.234412", # "price": "400.23", # "side": "sell" # } print("TODO: change", x) else: # TODO unhandled # this should never print print("TODO: unhandled2", x)
def currency(self, symbol: str) -> Instrument: # construct a base currency from the symbol return Instrument(name=symbol, type=InstrumentType.CURRENCY)
def _get_currency(symbol): return Instrument(name=symbol, type=InstrumentType.CURRENCY)
async def onStart(self, event: Event) -> None: pprint(self.instruments()) pprint(self.positions()) await self.subscribe(Instrument("BTC-USD", InstrumentType.PAIR))
def __init__(self, symbol: str, *args: Any, **kwargs: Any) -> None: super(ReadOnlyStrategy, self).__init__(*args, **kwargs) self._inst = Instrument(name=symbol, type=InstrumentType.EQUITY, exchange=ExchangeType("iex"))