async def get_user_trade_by_id(self, mode: Literal["to_list", "by_id"], trdMatchID: str, symbol: Optional[PAIR] = None, retries: int = 1): """Get info on a single trade """ try: data = self.request_parser.user_trades(trdMatchID=trdMatchID) except Exception as e: msg = repr(e) logger.error(msg) await log_exc_to_db(logger, e) return ErrorResponse(status_code=400, value=msg) result = await self.query_private(method="trades_info", data=data, retries=retries) if not result.is_ok: return ErrorResponse(status_code=result.status_code, value=result.value) else: parsed_response = self.response_parser.user_trades( response=result.value, mode=mode) # not all pydantic models have <data> as only field # so we need to specify the field here to make it work for all models return self.user_trade_validate_and_serialize( mode, {"data": parsed_response})
async def get_instrument( self, symbol: PAIR, retries: int = 1, ) -> NoobitResponse: """Get data for instrument. Depending on exchange this will aggregate ticker, spread data """ params = self.validate_params(model=InstrumentRequest, symbol=symbol) if params.is_error: return ErrorResponse(status_code=400, value=params.value) try: data = self.request_parser.instrument(params.value["symbol"]) except Exception as e: msg = repr(e) logger.error(msg) await log_exc_to_db(logger, e) return ErrorResponse(status_code=400, value=msg) result = await self.query_public(method="instrument", data=data, retries=retries) if not result.is_ok: return ErrorResponse(status_code=result.status_code, value=result.value) else: parsed_response = self.response_parser.instrument( response=result.value) return self.instrument_validate_and_serialize(parsed_response)
async def get_open_positions(self, mode: Literal["to_list", "by_id"], symbol: Optional[PAIR] = None, retries: int = 1) -> NoobitResponse: """For kraken there is no <closed positions> endpoint, but we can simulate it by querying <closed orders> and then filtering for margin orders Or even better filter out <trades history> and <type==closed position> """ if symbol is not None: symbol = symbol.upper() try: data = self.request_parser.open_positions(symbol) except Exception as e: msg = repr(e) logger.error(msg) await log_exc_to_db(logger, e) return ErrorResponse(status_code=400, value=msg) result = await self.query_private(method="open_positions", data=data, retries=retries) if not result.is_ok: return ErrorResponse(status_code=result.status_code, value=result.value) else: parsed_response = self.response_parser.open_positions( response=result.value, mode=mode) # not all pydantic models have <data> as only field # so we need to specify the field here to make it work for all models return self.positions_validate_and_serialize( mode, {"data": parsed_response})
async def get_orderbook( self, symbol: PAIR, retries: int = 1, ) -> NoobitResponse: """ """ params = self.validate_params(model=OrderBookRequest, symbol=symbol) if params.is_error: return ErrorResponse(status_code=400, value=params.value) try: data = self.request_parser.orderbook(params.value["symbol"]) except Exception as e: msg = repr(e) logger.error(msg) await log_exc_to_db(logger, e) return ErrorResponse(status_code=400, value=msg) result = await self.query_public(method="orderbook", data=data, retries=retries) if not result.is_ok: return ErrorResponse(status_code=result.status_code, value=result.value) else: parsed_response = self.response_parser.orderbook( response=result.value) return self.orderbook_validate_and_serialize(parsed_response)
async def get_closed_positions(self, mode: Literal["to_list", "by_id"], symbol: Optional[PAIR] = None, retries: int = 1) -> NoobitResponse: if symbol is not None: symbol = symbol.upper() try: data = self.request_parser.closed_positions(symbol) except Exception as e: msg = repr(e) logger.error(msg) await log_exc_to_db(logger, e) return ErrorResponse(status_code=400, value=msg) result = await self.query_private(method="closed_positions", data=data, retries=retries) if not result.is_ok: return ErrorResponse(status_code=result.status_code, value=result.value) else: parsed_response = self.response_parser.closed_positions( response=result.value, mode=mode) return self.positions_validate_and_serialize( mode, {"data": parsed_response})
async def publish_data_orderbook(self, msg, redis_pool): try: # with current logic parser needs to return a dict # that has bool values for keys is_snapshot and is_update parsed = self.stream_parser.orderbook(msg) # should return dict that we validates vs Trade Model validated = OrderBook(**parsed) # then we want to return a response if validated.is_snapshot: self.full_orderbook["asks"] = Counter(validated.asks) self.full_orderbook["bids"] = Counter(validated.bids) update_chan = f"ws:public:data:orderbook:snapshot:{self.exchange}:{validated.symbol}" else: self.full_orderbook["asks"] += Counter(validated.asks) self.full_orderbook["bids"] += Counter(validated.bids) update_chan = f"ws:public:data:orderbook:update:{self.exchange}:{validated.symbol}" resp = OKResponse(status_code=200, value=self.full_orderbook) await redis_pool.publish(update_chan, ujson.dumps(resp.value)) except ValidationError as e: logger.error(e) return ErrorResponse(status_code=404, value=str(e)) except Exception as e: log_exception(logger, e)
async def publish_data_trade(self, msg, redis_pool): # public trades # no snapshots try: parsed = self.stream_parser.trade(msg) # should return dict that we validates vs Trade Model validated = TradesList(data=parsed, last=None) # then we want to return a response value = validated.data resp = OKResponse(status_code=200, value=value) # resp value is a list of pydantic Trade Models # we need to check symbol for each item of list and dispatch accordingly for item in resp.value: # logger.info(ujson.dumps(item.dict())) update_chan = f"ws:public:data:trade:update:{self.exchange}:{item.symbol}" await redis_pool.publish(update_chan, ujson.dumps(item.dict())) except ValidationError as e: logger.error(e) return ErrorResponse(status_code=404, value=str(e)) except Exception as e: log_exception(logger, e)
def validate_model_from_mode( self, parsed_response: Union[dict, list, str], mode: Optional[str], mode_to_model: Dict[str, BaseModel]) -> Union[OKResponse, ErrorResponse]: """Handle validation for all possible modes present in mode_to_model dict Args: parsed_response: object to validate mode: mode of request mode_to_model: dict mapping mode to pydantic model to validate against Returns: Union[OKResponse, ErrorResponse]: according to success/error """ pydantic_model = mode_to_model[mode] try: validated = pydantic_model(**parsed_response) defined_fields = list(validated.dict().keys()) # handle different cases, where we define data only, or multiple fields if defined_fields == ["data"]: value = validated.dict()["data"] else: value = validated.dict() return OKResponse(status_code=status.HTTP_200_OK, value=value) except ValidationError as e: logger.error(str(e)) return ErrorResponse( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, value=e.errors())
async def publish_data_trade(self, msg, redis_pool): # public trades # ignore snapshot as it will only give us past 50 trades (useless) try: try: self.feed_counters["trade"] += 1 parsed = self.stream_parser.trade(msg) except KeyError: self.feed_counters["trade"] = 0 parsed = self.stream_parser.user_trade(msg) # should return dict that we validates vs Trade Model validated = TradesList(data=parsed) # then we want to return a response value = validated.data resp = OKResponse(status_code=200, value=value) # resp value is a list of pydantic Trade Models # we need to check symbol for each item of list and dispatch accordingly for item in resp.value: logger.info(item) update_chan = f"ws:private:data:trade:update:{self.exchange}:{item.symbol}" await redis_pool.publish(update_chan, ujson.dumps(item.dict())) except ValidationError as e: logger.error(e) await log_exc_to_db(logger, e) return ErrorResponse(status_code=404, value=str(e)) except Exception as e: log_exception(logger, e) await log_exc_to_db(logger, e)
async def get_ohlc(self, symbol: PAIR, timeframe: TIMEFRAME, retries: int = 1) -> NoobitResponse: """ """ #TODO validate kwargs by passing them to OhlcParameters Pydantic Model params = self.validate_params(model=OhlcRequest, symbol=symbol, timeframe=timeframe) if params.is_error: return ErrorResponse(status_code=400, value=params.value) # TODO Handle request errors (for ex if we pass invalid symbol, or pair that does not exist) try: data = self.request_parser.ohlc( symbol=params.value["symbol"], timeframe=params.value["timeframe"]) except Exception as e: msg = repr(e) logger.error(msg) await log_exc_to_db(logger, e) return ErrorResponse(status_code=400, value=msg) # data = self.request_parser.ohlc(symbol=symbol.upper(), timeframe=timeframe) # if isinstance(data, BaseException): # return ErrorResponse(status_code=400, value=data) #! vvvvvvvvvvvvvvvvvvvvvvv HANDLE REQUEST PARSING ERRORS # make request parser return a RequestResult object # but this leaves responsability to the user, which is bad # ==> Basic Example: # if request.is_error: # return ErrorResponse(status_code=bad_request, value=request.value) result = await self.query_public(method="ohlc", data=data, retries=retries) # parse to order response model and validate if not result.is_ok: return ErrorResponse(status_code=result.status_code, value=result.value) else: parsed_response = self.response_parser.ohlc(response=result.value) # not all pydantic models have <data> as only field # so we need to specify the field here to make it work for all models return self.ohlc_validate_and_serialize({"data": parsed_response})
async def get_closed_orders(self, mode: Literal["to_list", "by_id"], symbol: Optional[PAIR] = None, clOrdID: Optional[int] = None, orderID=None, retries: int = 1): """Get closed orders. Args: symbol (str): Instrument symbol clOrdID (str): Restrict results to given ID mode (str): Parse response to list or index by order id Returns: closed orders """ if symbol is not None: symbol = symbol.upper() try: data = self.request_parser.orders("closed", symbol=symbol, clOrdID=clOrdID) except Exception as e: msg = repr(e) logger.error(msg) await log_exc_to_db(logger, e) return ErrorResponse(status_code=400, value=msg) result = await self.query_private(method="closed_orders", data=data, retries=retries) # parse to order response model and validate if not result.is_ok: return ErrorResponse(status_code=result.status_code, value=result.value) else: parsed_response = self.response_parser.orders( response=result.value, symbol=symbol, mode=mode) # not all pydantic models have <data> as only field # so we need to specify the field here to make it work for all models return self.order_validate_and_serialize(mode, {"data": parsed_response})
async def get_public_trades(self, symbol: PAIR, since: TIMESTAMP = None, retries: int = 1) -> NoobitResponse: """Get data on public trades. Response value is a list with each item being a dict that corresponds to the data for a single trade. """ params = self.validate_params(model=TradesRequest, symbol=symbol, since=since) if params.is_error: return ErrorResponse(status_code=400, value=params.value) try: data = self.request_parser.public_trades( symbol=params.value["symbol"], since=params.value["since"]) except Exception as e: msg = repr(e) logger.error(msg) await log_exc_to_db(logger, e) return ErrorResponse(status_code=400, value=msg) result = await self.query_public(method="trades", data=data, retries=retries) if not result.is_ok: return ErrorResponse(status_code=result.status_code, value=result.value) else: parsed_response = self.response_parser.trades( response=result.value) # not all pydantic models have <data> as only field # so we need to specify the field here to make it work for all models return self.public_trade_validate_and_serialize({ "data": parsed_response["data"], "last": parsed_response["last"] })
async def get_order(self, mode: Literal["to_list", "by_id"], orderID: str, clOrdID: Optional[int] = None, symbol=None, retries: int = 1) -> Union[list, dict, str]: """Get a single order mode (str): Parse response to list, or index by order id orderID: ID of the order to query (ID as assigned by broker) clOrdID (str): Restrict results to given ID """ try: data = self.request_parser.orders(mode="by_id", orderID=orderID, clOrdID=clOrdID) except Exception as e: msg = repr(e) logger.error(msg) await log_exc_to_db(logger, e) return ErrorResponse(status_code=400, value=msg) # returns ErrorHandlerResult object (OkResult or ErrorResult) result = await self.query_private(method="order_info", data=data, retries=retries) # if its an error we just want the error message with no parsing if not result.is_ok: return ErrorResponse(status_code=result.status_code, value=result.value) else: # parse to order response model if error handler has not returned None or ValidationError parsed_response = self.response_parser.orders( response=result.value, mode=mode) # not all pydantic models have <data> as only field # so we need to specify the field here to make it work for all models return self.order_validate_and_serialize(mode, {"data": parsed_response})
async def get_exposure(self, retries: int = 1): data = {} result = await self.query_private(method="exposure", data=data, retries=retries) if not result.is_ok: return ErrorResponse(status_code=result.status_code, value=result.value) else: parsed_response = self.response_parser.exposure( response=result.value) return self.exposure_validate_and_serialize(parsed_response)
async def publish_data_order(self, msg, redis_pool): try: #! we need to sort between snapshot / new order update / order status update #! like for orderbook we want to return a full image of current orders, not just a single status update for ex #! ==> ideally we want to publish to two redis channels, one with the full list of current orders, one with just the incoming updates try: self.feed_counters["order"] += 1 # this should return a dict with 2 keys: # new_orders and status_changes each being a dict parsed = self.stream_parser.order_update(msg) # updating the dict will override the value if the key is already present self.all_orders.update(parsed["insert"]) for order_id, info in parsed["update"].items(): # e.g info = {"status": "filled", "leavesQty": 0} for key, value in info.items(): self.all_orders[order_id][key] = value # first message == it's a snapshot (!! this is true for kraken but we have not checked for other exchanges) except KeyError: self.feed_counters["order"] = 0 parsed = self.stream_parser.order_snapshot(msg) self.all_orders.update(parsed) # should return dict that we validates vs order Model validated = OrdersByID(data=self.all_orders) # then we want to return a response value = validated.data resp = OKResponse(status_code=200, value=value) # resp value is a dict of pydantic Order Models # we need to check symbol for each item of dict and dispatch accordingly for key, value in resp.value.items(): logger.info(value) update_chan = f"ws:private:data:order:update:{self.exchange}:{value.symbol}" await redis_pool.publish(update_chan, ujson.dumps(value.dict())) except ValidationError as e: logger.error(e) await log_exc_to_db(logger, e) return ErrorResponse(status_code=404, value=str(e)) except Exception as e: log_exception(logger, e) await log_exc_to_db(logger, e)
async def publish_data_spread(self, msg, redis_pool): try: parsed = self.stream_parser.spread(msg) validated = Spread(**parsed) resp = OKResponse(status_code=200, value=validated) logger.info(resp.value) update_chan = f"ws:public:data:spread:update:{self.exchange}:{resp.value.symbol}" await redis_pool.publish(update_chan, ujson.dumps(resp.value.dict())) except ValidationError as e: logger.error(e) return ErrorResponse(status_code=404, value=str(e)) except Exception as e: log_exception(logger, e)
async def publish_data_instrument(self, msg, redis_pool): # no snapshots try: parsed = self.stream_parser.instrument(msg) # should return dict that we validates vs Trade Model validated = Instrument(**parsed) # then we want to return a response resp = OKResponse(status_code=200, value=validated) logger.info(resp.value) update_chan = f"ws:public:data:instrument:update:{self.exchange}:{resp.value.symbol}" await redis_pool.publish(update_chan, ujson.dumps(resp.value.dict())) except ValidationError as e: logger.error(e) return ErrorResponse(status_code=404, value=str(e)) except Exception as e: log_exception(logger, e)
async def get_balances(self, symbol: Optional[PAIR] = None, retries: int = 1) -> NoobitResponse: if symbol is not None: symbol = symbol.upper() data = {} result = await self.query_private(method="balances", data=data, retries=retries) if not result.is_ok: return ErrorResponse(status_code=result.status_code, value=result.value) else: parsed_response = self.response_parser.balances( response=result.value) return self.balances_validate_and_serialize( {"data": parsed_response})
async def get_websocket_auth_token(self, validity: int = None, permissions: list = None, retries: int = 0): """Get auth token to subscribe to private websocket feed. Args: validity (int) : number of minutes that token is valid (optional / default (max): 60 minutes) permissions (list) : comma separated list of allowed feeds (optional / default: all) Returns: dict keys: token (str) : token to authenticate private websocket subscription expires (int) : time to expiry Note: The API client must request an authentication "token" via the following REST API endpoint "GetWebSocketsToken" to connect to WebSockets Private endpoints. The token should be used within 15 minutes of creation. The token does not expire once a connection to a WebSockets API private message (openOrders or ownTrades) is maintained. This should be called at startup, we get a token, then subscribe to a ws feed We receive all the updates for our orders and send it to redis That way we can track position changes almost at tick speed without needing to make rest calls We will need to check the token creation time on every tick and a get new token every 30/40 minutes """ data = {"validity": validity, "permissions": permissions} result = await self.query_private(method="ws_token", data=data, retries=retries) if result.is_ok: return OKResponse(status_code=status.HTTP_200_OK, value=result.value) else: return ErrorResponse(status_code=result.status_code, value=result.value)