async def collect_metrics(self, events: List[OrderFilledEvent]): try: total_volume = Decimal("0") for fill_event in events: trade_base, trade_quote = split_hb_trading_pair( fill_event.trading_pair) from_quote_conversion_pair = combine_to_hb_trading_pair( base=trade_quote, quote=self._valuation_token) rate = await self._rate_provider.stored_or_live_rate( from_quote_conversion_pair) if rate is not None: total_volume += fill_event.amount * fill_event.price * rate else: from_base_conversion_pair = combine_to_hb_trading_pair( base=trade_base, quote=self._valuation_token) rate = await self._rate_provider.stored_or_live_rate( from_base_conversion_pair) if rate is not None: total_volume += fill_event.amount * rate else: self.logger().debug( f"Could not find a conversion rate rate using Rate Oracle for any of " f"the pairs {from_quote_conversion_pair} or {from_base_conversion_pair}" ) if total_volume > Decimal("0"): self._dispatch_trade_volume(total_volume) except asyncio.CancelledError: raise except Exception: self._collected_events.extend(events)
def fee_amount_in_token(self, trading_pair: str, price: Decimal, order_amount: Decimal, token: str, exchange: Optional['ExchangeBase'] = None, rate_source: Optional[Any] = None) -> Decimal: base, quote = split_hb_trading_pair(trading_pair) fee_amount = Decimal("0") if self.percent != 0: amount_from_percentage = (price * order_amount) * self.percent if self._are_tokens_interchangeable(quote, token): fee_amount += amount_from_percentage else: conversion_pair = combine_to_hb_trading_pair(base=quote, quote=token) conversion_rate = self._get_exchange_rate( conversion_pair, exchange, rate_source) fee_amount += amount_from_percentage * conversion_rate for flat_fee in self.flat_fees: if self._are_tokens_interchangeable(flat_fee.token, token): # No need to convert the value fee_amount += flat_fee.amount elif (self._are_tokens_interchangeable(flat_fee.token, base) and (self._are_tokens_interchangeable(quote, token))): # In this case instead of looking for the rate we use directly the price in the parameters fee_amount += flat_fee.amount * price else: conversion_pair = combine_to_hb_trading_pair( base=flat_fee.token, quote=token) conversion_rate = self._get_exchange_rate( conversion_pair, exchange, rate_source) fee_amount += (flat_fee.amount * conversion_rate) return fee_amount
def _get_fee(self, base_currency: str, quote_currency: str, order_type: OrderType, order_side: TradeType, amount: Decimal, price: Decimal = s_decimal_NaN, is_maker: Optional[bool] = None) -> AddedToCostTradeFee: is_maker = is_maker or (order_type is OrderType.LIMIT_MAKER) trading_pair = combine_to_hb_trading_pair(base=base_currency, quote=quote_currency) if trading_pair in self._trading_fees: fees_data = self._trading_fees[trading_pair] fee_value = Decimal( fees_data["makerFeeRate"]) if is_maker else Decimal( fees_data["takerFeeRate"]) fee = AddedToCostTradeFee(percent=fee_value) else: fee = build_trade_fee( self.name, is_maker, base_currency=base_currency, quote_currency=quote_currency, order_type=order_type, order_side=order_side, amount=amount, price=price, ) return fee
async def init_trading_pair_symbols( cls, domain: str = CONSTANTS.DOMAIN, throttler: Optional[AsyncThrottler] = None, api_factory: WebAssistantsFactory = None, time_synchronizer: Optional[TimeSynchronizer] = None): """Initialize _trading_pair_symbol_map class variable""" mapping = bidict() try: data = await web_utils.api_request( path=CONSTANTS.EXCHANGE_INFO_URL, api_factory=api_factory, throttler=throttler, time_synchronizer=time_synchronizer, domain=domain, method=RESTMethod.GET, timeout=10) for symbol_data in filter(utils.is_exchange_information_valid, data["symbols"]): try: mapping[symbol_data["pair"]] = combine_to_hb_trading_pair( symbol_data["baseAsset"], symbol_data["quoteAsset"]) except ValueDuplicationError: continue except Exception as ex: cls.logger().exception( f"There was an error requesting exchange info ({str(ex)})") cls._trading_pair_symbol_map[domain] = mapping
def setUp(self) -> None: super().setUp() self.strategy = None self.markets = {"binance": ExchangeBase(client_config_map=ClientConfigAdapter(ClientConfigMap()))} self.notifications = [] self.log_records = [] self.base = "ETH" self.quote = "BTC" self.strategy_config_map = ClientConfigAdapter( AvellanedaMarketMakingConfigMap( exchange="binance", market=combine_to_hb_trading_pair(self.base, self.quote), execution_timeframe_mode=FromDateToDateModel( start_datetime="2021-11-18 15:00:00", end_datetime="2021-11-18 16:00:00", ), order_amount=60, order_refresh_time=60, hanging_orders_mode=TrackHangingOrdersModel( hanging_orders_cancel_pct=1, ), order_levels_mode=MultiOrderLevelModel( order_levels=4, level_distances=1, ), min_spread=2, risk_factor=1.11, order_amount_shape_factor=0.33, ) ) self.raise_exception_for_market_initialization = False self._logger = None
def get_fee_impact_on_order_cost( self, order_candidate: 'OrderCandidate', exchange: 'ExchangeBase') -> Optional[TokenAmount]: """ WARNING: Do not use this method for sizing. Instead, use the `BudgetChecker`. Returns the impact of the fee on the cost requirements for the candidate order. """ ret = None if self.percent != Decimal("0"): fee_token = self.percent_token or order_candidate.order_collateral.token if order_candidate.order_collateral is None or fee_token != order_candidate.order_collateral.token: token, size = order_candidate.get_size_token_and_order_size() if fee_token == token: exchange_rate = Decimal("1") else: exchange_pair = combine_to_hb_trading_pair( token, fee_token) # buy order token w/ pf token exchange_rate = exchange.get_price(exchange_pair, is_buy=True) fee_amount = size * exchange_rate * self.percent else: # self.percent_token == order_candidate.order_collateral.token fee_amount = order_candidate.order_collateral.amount * self.percent ret = TokenAmount(fee_token, fee_amount) return ret
async def init_trading_pair_symbols( cls, domain: str = CONSTANTS.DOMAIN, throttler: Optional[AsyncThrottler] = None, api_factory: WebAssistantsFactory = None): """Initialize _trading_pair_symbol_map class variable""" mapping = bidict() api_factory = api_factory or web_utils.build_api_factory() throttler = throttler or cls._get_throttler_instance() params = {"filter": json.dumps({"typ": "IFXXXP"})} try: data = await web_utils.api_request(CONSTANTS.EXCHANGE_INFO_URL, api_factory, throttler, domain, params) for symbol_data in data: try: mapping[ symbol_data["symbol"]] = combine_to_hb_trading_pair( symbol_data["rootSymbol"], symbol_data["quoteCurrency"]) except ValueDuplicationError: continue except Exception as ex: cls.logger().error( f"There was an error requesting exchange info ({str(ex)})") cls._trading_pair_symbol_map[domain] = mapping
async def _init_trading_pair_symbols( cls, domain: str = CONSTANTS.DEFAULT_DOMAIN, api_factory: Optional[WebAssistantsFactory] = None, throttler: Optional[AsyncThrottler] = None, time_synchronizer: Optional[TimeSynchronizer] = None): """ Initialize mapping of trade symbols in exchange notation to trade symbols in client notation """ mapping = bidict() try: data = await web_utils.api_request( path=CONSTANTS.EXCHANGE_INFO_PATH_URL, api_factory=api_factory, throttler=throttler, time_synchronizer=time_synchronizer, domain=domain, method=RESTMethod.GET, ) for symbol_data in data["result"]: mapping[symbol_data["name"]] = combine_to_hb_trading_pair( base=symbol_data["baseCurrency"], quote=symbol_data["quoteCurrency"]) except Exception as ex: cls.logger().error( f"There was an error requesting exchange info ({str(ex)})") cls._trading_pair_symbol_map[domain] = mapping
def test_adjust_candidate_insufficient_funds_for_flat_fees_and_percent_fees_third_token( self): fc_token = "PFC" trade_fee_schema = TradeFeeSchema( percent_fee_token=fc_token, maker_percent_fee_decimal=Decimal("0.01"), taker_percent_fee_decimal=Decimal("0.01"), maker_fixed_fees=[TokenAmount(fc_token, Decimal("1"))]) exchange = MockPaperExchange(client_config_map=ClientConfigAdapter( ClientConfigMap()), trade_fee_schema=trade_fee_schema) pfc_quote_pair = combine_to_hb_trading_pair(self.quote_asset, fc_token) exchange.set_balanced_order_book( # the quote to pfc price will be 1:2 trading_pair=pfc_quote_pair, mid_price=1.5, min_price=1, max_price=2, price_step_size=1, volume_step_size=1, ) budget_checker: BudgetChecker = exchange.budget_checker exchange.set_balance(self.quote_asset, Decimal("20")) exchange.set_balance( fc_token, Decimal("1.2") ) # 0.2 less than required; will result in 50% order reduction order_candidate = OrderCandidate( trading_pair=self.trading_pair, is_maker=True, order_type=OrderType.LIMIT, order_side=TradeType.BUY, amount=Decimal("10"), price=Decimal("2"), ) adjusted_candidate = budget_checker.adjust_candidate(order_candidate, all_or_none=False) self.assertEqual(Decimal("5"), adjusted_candidate.amount) self.assertEqual(self.quote_asset, adjusted_candidate.order_collateral.token) self.assertEqual(Decimal("10"), adjusted_candidate.order_collateral.amount) self.assertEqual(fc_token, adjusted_candidate.percent_fee_collateral.token) self.assertEqual(Decimal("0.2"), adjusted_candidate.percent_fee_collateral.amount) self.assertEqual(fc_token, adjusted_candidate.percent_fee_value.token) self.assertEqual(Decimal("0.2"), adjusted_candidate.percent_fee_value.amount) self.assertEqual(1, len(adjusted_candidate.fixed_fee_collaterals)) fixed_fee_collateral = adjusted_candidate.fixed_fee_collaterals[0] self.assertEqual(fc_token, fixed_fee_collateral.token) self.assertEqual(Decimal("1"), fixed_fee_collateral.amount) self.assertEqual(self.base_asset, adjusted_candidate.potential_returns.token) self.assertEqual(Decimal("5"), adjusted_candidate.potential_returns.amount)
def _initialize_trading_pair_symbols_from_exchange_info( self, exchange_info: Dict[str, Any]): mapping = bidict() for symbol_data in filter(okx_utils.is_exchange_information_valid, exchange_info["data"]): mapping[symbol_data["instId"]] = combine_to_hb_trading_pair( base=symbol_data["baseCcy"], quote=symbol_data["quoteCcy"]) self._set_trading_pair_symbol_map(mapping)
def _initialize_trading_pair_symbols_from_exchange_info( self, exchange_info: Dict[str, Any]): mapping = bidict() for symbol_data in filter(utils.is_pair_information_valid, exchange_info.get("data", [])): mapping[symbol_data["symbol"]] = combine_to_hb_trading_pair( base=symbol_data["baseCurrency"], quote=symbol_data["quoteCurrency"]) self._set_trading_pair_symbol_map(mapping)
def test_populate_collateral_fields_percent_fees_in_third_token(self): pfc_token = "PFC" trade_fee_schema = TradeFeeSchema( percent_fee_token=pfc_token, maker_percent_fee_decimal=Decimal("0.01"), taker_percent_fee_decimal=Decimal("0.01"), ) exchange = MockPaperExchange(client_config_map=ClientConfigAdapter( ClientConfigMap()), trade_fee_schema=trade_fee_schema) pfc_quote_pair = combine_to_hb_trading_pair(self.quote_asset, pfc_token) exchange.set_balanced_order_book( # the quote to pfc price will be 1:2 trading_pair=pfc_quote_pair, mid_price=1.5, min_price=1, max_price=2, price_step_size=1, volume_step_size=1, ) budget_checker: BudgetChecker = exchange.budget_checker order_candidate = OrderCandidate( trading_pair=self.trading_pair, is_maker=True, order_type=OrderType.LIMIT, order_side=TradeType.BUY, amount=Decimal("10"), price=Decimal("2"), ) populated_candidate = budget_checker.populate_collateral_entries( order_candidate) self.assertEqual(self.quote_asset, populated_candidate.order_collateral.token) self.assertEqual(Decimal("20"), populated_candidate.order_collateral.amount) self.assertEqual(pfc_token, populated_candidate.percent_fee_collateral.token) self.assertEqual(Decimal("0.4"), populated_candidate.percent_fee_collateral.amount) self.assertEqual(pfc_token, populated_candidate.percent_fee_value.token) self.assertEqual(Decimal("0.4"), populated_candidate.percent_fee_value.amount) self.assertEqual(0, len(populated_candidate.fixed_fee_collaterals)) self.assertEqual(self.base_asset, populated_candidate.potential_returns.token) self.assertEqual(Decimal("10"), populated_candidate.potential_returns.amount)
def _initialize_trading_pair_symbols_from_exchange_info( self, exchange_info: Dict[str, Any]): mapping = bidict() if not exchange_info: return for ticker in exchange_info: exchange_trading_pair = f"{ticker['baseCurrency']}/{ticker['quoteCurrency']}" if 'symbol' not in ticker: continue # don't update with trading_rules data, check format_trading_rules for workaround base, quote = ticker["symbol"].split('/') mapping[exchange_trading_pair] = combine_to_hb_trading_pair( base=base, quote=quote) if mapping: self._set_trading_pair_symbol_map(mapping)
def fee_amount_in_quote( self, trading_pair: str, price: Decimal, order_amount: Decimal, exchange: Optional['ExchangeBase'] = None ) -> Decimal: fee_amount = Decimal("0") if self.percent > 0: fee_amount = (price * order_amount) * self.percent base, quote = split_hb_trading_pair(trading_pair) for flat_fee in self.flat_fees: if interchangeable(flat_fee.token, base): fee_amount += (flat_fee.amount * price) elif interchangeable(flat_fee.token, quote): fee_amount += flat_fee.amount else: conversion_pair = combine_to_hb_trading_pair(base=flat_fee.token, quote=quote) conversion_rate = self._get_exchange_rate(conversion_pair, exchange) fee_amount = (flat_fee.amount * conversion_rate) return fee_amount
def _get_size_collateral_price( self, exchange: 'ExchangeBase', order_collateral_token: str ) -> Decimal: size_token, _ = self.get_size_token_and_order_size() base, quote = split_hb_trading_pair(self.trading_pair) if order_collateral_token == size_token: price = Decimal("1") elif order_collateral_token == base: # size_token == quote price = Decimal("1") / self.price elif order_collateral_token == quote: # size_token == base price = self.price else: size_collateral_pair = combine_to_hb_trading_pair(size_token, order_collateral_token) price = exchange.get_price(size_collateral_pair, is_buy=True) # we are buying return price
def _get_fee(self, base_currency: str, quote_currency: str, order_type: OrderType, order_side: TradeType, amount: Decimal, price: Decimal = s_decimal_NaN, is_maker: Optional[bool] = None) -> TradeFeeBase: """ Calculates the estimated fee an order would pay based on the connector configuration :param base_currency: the order base currency :param quote_currency: the order quote currency :param order_type: the type of order (MARKET, LIMIT, LIMIT_MAKER) :param order_side: if the order is for buying or selling :param amount: the order amount :param price: the order price :param is_maker: if we take into account maker fee (True) or taker fee (None, False) :return: the estimated fee for the order """ trading_pair = combine_to_hb_trading_pair(base=base_currency, quote=quote_currency) fee_schema = self._trading_fees.get(trading_pair, None) if fee_schema is None: self.logger().warning( f"For trading pair = {trading_pair} there is no fee schema loaded, using presets!" ) fee = build_trade_fee(exchange=self.name, is_maker=is_maker, base_currency=base_currency, quote_currency=quote_currency, order_type=order_type, order_side=order_side, amount=amount, price=price) else: if fee_schema.type == LatokenTakeType.PROPORTION or fee_schema.take == LatokenCommissionType.PERCENT: pass # currently not implemented but is nice to have in next release(s) percent = fee_schema.maker_fee if order_type is OrderType.LIMIT_MAKER or ( is_maker is not None and is_maker) else fee_schema.taker_fee fee = AddedToCostTradeFee( percent=percent ) if order_side == TradeType.BUY else DeductedFromReturnsTradeFee( percent=percent) return fee
async def _calculate_fees(self, quote: str, trades: List[Any]): for trade in trades: fee_percent = None trade_price = None trade_amount = None if self._is_trade_fill(trade): if trade.trade_fee.get("percent") is not None and Decimal( trade.trade_fee["percent"]) > 0: trade_price = Decimal(str(trade.price)) trade_amount = Decimal(str(trade.amount)) fee_percent = Decimal(str(trade.trade_fee["percent"])) flat_fees = [ TokenAmount(token=flat_fee["token"], amount=Decimal(flat_fee["amount"])) for flat_fee in trade.trade_fee.get("flat_fees", []) ] else: # assume this is Trade object if trade.trade_fee.percent is not None and trade.trade_fee.percent > 0: trade_price = Decimal(trade.price) trade_amount = Decimal(trade.amount) fee_percent = Decimal(trade.trade_fee.percent) flat_fees = trade.trade_fee.flat_fees if fee_percent is not None and fee_percent > 0: self.fees[quote] += trade_price * trade_amount * fee_percent for flat_fee in flat_fees: self.fees[flat_fee.token] += flat_fee.amount for fee_token, fee_amount in self.fees.items(): if fee_token == quote: self.fee_in_quote += fee_amount else: rate_pair: str = combine_to_hb_trading_pair(fee_token, quote) last_price = await RateOracle.get_instance( ).stored_or_live_rate(rate_pair) if last_price is not None: self.fee_in_quote += fee_amount * last_price else: self.logger().warning( f"Could not find exchange rate for {rate_pair} " f"using {RateOracle.get_instance()}. PNL value will be inconsistent." )
def get_fee(self, base_currency: str, quote_currency: str, order_type: OrderType, order_side: TradeType, amount: Decimal, price: Decimal = s_decimal_NaN, is_maker: Optional[bool] = None) -> AddedToCostTradeFee: """ Calculates the fee to pay based on the fee information provided by the exchange for the account and the token pair. If exchange info is not available it calculates the estimated fee an order would pay based on the connector configuration :param base_currency: the order base currency :param quote_currency: the order quote currency :param order_type: the type of order (MARKET, LIMIT, LIMIT_MAKER) :param order_side: if the order is for buying or selling :param amount: the order amount :param price: the order price :param is_maker: True if the order is a maker order, False if it is a taker order :return: the calculated or estimated fee """ is_maker = is_maker or (order_type is OrderType.LIMIT_MAKER) trading_pair = combine_to_hb_trading_pair(base=base_currency, quote=quote_currency) if trading_pair in self._trading_fees: fees_data = self._trading_fees[trading_pair] fee_value = Decimal(fees_data["makerFeeRate"]) if is_maker else Decimal(fees_data["takerFeeRate"]) fee = AddedToCostTradeFee(percent=fee_value) else: fee = build_trade_fee( self.name, is_maker, base_currency=base_currency, quote_currency=quote_currency, order_type=order_type, order_side=order_side, amount=amount, price=price, ) return fee
async def _format_trading_rules( self, exchange_info_list: List[Dict[str, Any]]) -> List[TradingRule]: """ Queries the necessary API endpoint and initialize the TradingRule object for each trading pair being traded. Parameters ---------- exchange_info_dict: Trading rules dictionary response from the exchange """ return_val: list = [] for rule in exchange_info_list: try: trading_pair = combine_to_hb_trading_pair( rule["rootSymbol"], rule["quoteCurrency"]) if trading_pair in self._trading_pairs: trading_pair_multipliers = await utils.get_trading_pair_multipliers( rule['symbol']) self._trading_pair_to_multipliers[ trading_pair] = trading_pair_multipliers max_order_size = Decimal(str(rule.get("maxOrderQty"))) min_order_size = Decimal(str(rule.get( "lotSize"))) / trading_pair_multipliers.base_multiplier tick_size = Decimal(str(rule.get("tickSize"))) return_val.append( TradingRule( trading_pair, min_order_size=min_order_size, min_price_increment=Decimal(tick_size), min_base_amount_increment=Decimal(min_order_size), max_order_size=max_order_size, )) except Exception as e: self.logger().error( f"Error parsing the trading pair rule {rule}. Error: {e}. Skipping...", exc_info=True) return return_val
async def init_trading_pair_symbols( cls, domain: str = CONSTANTS.DOMAIN, throttler: Optional[AsyncThrottler] = None, api_factory: WebAssistantsFactory = None ): """Initialize _trading_pair_symbol_map class variable""" mapping = bidict() api_factory = api_factory or utils.build_api_factory() rest_assistant = await api_factory.get_rest_assistant() url = utils.rest_url(path_url=CONSTANTS.EXCHANGE_INFO_URL, domain=domain) throttler = throttler or cls._get_throttler_instance() try: async with throttler.execute_task(limit_id=CONSTANTS.EXCHANGE_INFO_URL): request = RESTRequest( method=RESTMethod.GET, url=url, ) response = await rest_assistant.call(request=request, timeout=10) if response.status == 200: data = await response.json() # fetch d["pair"] for binance perpetual for symbol_data in filter(utils.is_exchange_information_valid, data["symbols"]): try: mapping[symbol_data["pair"]] = combine_to_hb_trading_pair( symbol_data["baseAsset"], symbol_data["quoteAsset"]) except ValueDuplicationError: continue except Exception as ex: cls.logger().error(f"There was an error requesting exchange info ({str(ex)})") cls._trading_pair_symbol_map[domain] = mapping