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
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 _get_order_collateral_token(self, exchange: 'ExchangeBase') -> Optional[str]: trading_pair = self.trading_pair base, quote = split_hb_trading_pair(trading_pair) if self.order_side == TradeType.BUY: oc_token = quote else: oc_token = base return oc_token
def get_size_token_and_order_size(self) -> TokenAmount: trading_pair = self.trading_pair base, quote = split_hb_trading_pair(trading_pair) if self.order_side == TradeType.BUY: order_size = self.amount * self.price size_token = quote else: order_size = self.amount size_token = base return TokenAmount(size_token, order_size)
def get_assets(self, connector_name: str) -> List[str]: """ Returns a unique list of unique of token names sorted alphabetically :param connector_name: The name of the connector :return: A list of token names """ result: Set = set() for trading_pair in self.markets[connector_name]: result.update(split_hb_trading_pair(trading_pair)) return sorted(result)
def _market_trading_pair_tuple( self, connector_name: str, trading_pair: str) -> MarketTradingPairTuple: """ Creates and returns a new MarketTradingPairTuple :param connector_name: The name of the connector :param trading_pair: The trading pair :return: A new MarketTradingPairTuple object. """ base, quote = split_hb_trading_pair(trading_pair) return MarketTradingPairTuple(self.connectors[connector_name], trading_pair, base, quote)
def _get_fee(self, exchange: 'ExchangeBase') -> TradeFeeBase: base, quote = split_hb_trading_pair(self.trading_pair) position_action = PositionAction.CLOSE if self.position_close else PositionAction.OPEN fee = build_perpetual_trade_fee( exchange.name, self.is_maker, position_action, base, quote, self.order_type, self.order_side, self.amount, self.price, ) return fee
def _get_fee(self, exchange: 'ExchangeBase') -> TradeFeeBase: trading_pair = self.trading_pair price = self.price base, quote = split_hb_trading_pair(trading_pair) fee = build_trade_fee( exchange.name, self.is_maker, base, quote, self.order_type, self.order_side, self.amount, price, ) return fee
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 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
async def _initialize_metrics(self, exchange: str, trading_pair: str, trades: List[Any], current_balances: Dict[str, Decimal]): """ Calculates PnL, fees, Return % and etc... :param exchange: the exchange or connector name :param trading_pair: the trading market to get performance metrics :param trades: the list of TradeFill or Trade object :param current_balances: current user account balance """ base, quote = split_hb_trading_pair(trading_pair) buys, sells = self._preprocess_trades_and_group_by_type(trades) self.num_buys = len(buys) self.num_sells = len(sells) self.num_trades = self.num_buys + self.num_sells self.cur_base_bal = current_balances.get(base, 0) self.cur_quote_bal = current_balances.get(quote, 0) self.start_base_bal = self.cur_base_bal - self.tot_vol_base self.start_quote_bal = self.cur_quote_bal - self.tot_vol_quote self.start_price = Decimal(str(trades[0].price)) self.cur_price = await get_last_price( exchange.replace("_paper_trade", ""), trading_pair) if self.cur_price is None: self.cur_price = Decimal(str(trades[-1].price)) self.start_base_ratio_pct = self.divide( self.start_base_bal * self.start_price, (self.start_base_bal * self.start_price) + self.start_quote_bal) self.cur_base_ratio_pct = self.divide( self.cur_base_bal * self.cur_price, (self.cur_base_bal * self.cur_price) + self.cur_quote_bal) self.hold_value = (self.start_base_bal * self.cur_price) + self.start_quote_bal self.cur_value = (self.cur_base_bal * self.cur_price) + self.cur_quote_bal self._calculate_trade_pnl(buys, sells) await self._calculate_fees(exchange, quote, trades) self.total_pnl = self.trade_pnl - self.fee_in_quote self.return_pct = self.divide(self.total_pnl, self.hold_value)
async def asset_ratio_maintenance_prompt( self, # type: HummingbotApplication config_map: BaseTradingStrategyConfigMap, input_value: Any = None, ): # pragma: no cover if input_value: config_map.inventory_target_base_pct = input_value else: exchange = config_map.exchange market = config_map.market base, quote = split_hb_trading_pair(market) balances = await UserBalances.instance().balances( exchange, base, quote) if balances is None: return base_ratio = await UserBalances.base_amount_ratio( exchange, market, balances) if base_ratio is None: return base_ratio = round(base_ratio, 3) quote_ratio = 1 - base_ratio cvar = ConfigVar( key="temp_config", prompt= f"On {exchange}, you have {balances.get(base, 0):.4f} {base} and " f"{balances.get(quote, 0):.4f} {quote}. By market value, " f"your current inventory split is {base_ratio:.1%} {base} " f"and {quote_ratio:.1%} {quote}." f" Would you like to keep this ratio? (Yes/No) >>> ", required_if=lambda: True, type_str="bool", validator=validate_bool) await self.prompt_a_config_legacy(cvar) if cvar.value: config_map.inventory_target_base_pct = round( base_ratio * Decimal('100'), 1) elif self.app.to_stop_config: self.app.to_stop_config = False else: await self.prompt_a_config(config_map, config="inventory_target_base_pct")
class BuyDipExample(ScriptStrategyBase): """ THis strategy buys ETH (with BTC) when the ETH-BTC drops 5% below 50 days moving average (of a previous candle) This example demonstrates: - How to call Binance REST API for candle stick data - How to incorporate external pricing source (Coingecko) into the strategy - How to listen to order filled event - How to structure order execution on a more complex strategy Before running this example, make sure you run `config rate_oracle_source coingecko` """ connector_name: str = "binance_paper_trade" trading_pair: str = "ETH-BTC" base_asset, quote_asset = split_hb_trading_pair(trading_pair) conversion_pair: str = f"{quote_asset}-USD" buy_usd_amount: Decimal = Decimal("100") moving_avg_period: int = 50 dip_percentage: Decimal = Decimal("0.05") #: A cool off period before the next buy (in seconds) cool_off_interval: float = 10. #: The last buy timestamp last_ordered_ts: float = 0. markets = {connector_name: {trading_pair}} @property def connector(self) -> ExchangeBase: """ The only connector in this strategy, define it here for easy access """ return self.connectors[self.connector_name] def on_tick(self): """ Runs every tick_size seconds, this is the main operation of the strategy. - Create proposal (a list of order candidates) - Check the account balance and adjust the proposal accordingly (lower order amount if needed) - Lastly, execute the proposal on the exchange """ proposal: List[OrderCandidate] = self.create_proposal() proposal = self.connector.budget_checker.adjust_candidates(proposal, all_or_none=False) if proposal: self.execute_proposal(proposal) def create_proposal(self) -> List[OrderCandidate]: """ Creates and returns a proposal (a list of order candidate), in this strategy the list has 1 element at most. """ daily_closes = self._get_daily_close_list(self.trading_pair) start_index = (-1 * self.moving_avg_period) - 1 # Calculate the average of the 50 element prior to the last element avg_close = mean(daily_closes[start_index:-1]) proposal = [] # If the current price (the last close) is below the dip, add a new order candidate to the proposal if daily_closes[-1] < avg_close * (Decimal("1") - self.dip_percentage): order_price = self.connector.get_price(self.trading_pair, False) * Decimal("0.9") usd_conversion_rate = RateOracle.get_instance().rate(self.conversion_pair) amount = (self.buy_usd_amount / usd_conversion_rate) / order_price proposal.append(OrderCandidate(self.trading_pair, False, OrderType.LIMIT, TradeType.BUY, amount, order_price)) return proposal def execute_proposal(self, proposal: List[OrderCandidate]): """ Places the order candidates on the exchange, if it is not within cool off period and order candidate is valid. """ if self.last_ordered_ts > time.time() - self.cool_off_interval: return for order_candidate in proposal: if order_candidate.amount > Decimal("0"): self.buy(self.connector_name, self.trading_pair, order_candidate.amount, order_candidate.order_type, order_candidate.price) self.last_ordered_ts = time.time() def did_fill_order(self, event: OrderFilledEvent): """ Listens to fill order event to log it and notify the hummingbot application. If you set up Telegram bot, you will get notification there as well. """ msg = (f"({event.trading_pair}) {event.trade_type.name} order (price: {event.price}) of {event.amount} " f"{split_hb_trading_pair(event.trading_pair)[0]} is filled.") self.log_with_clock(logging.INFO, msg) self.notify_hb_app_with_timestamp(msg) def _get_daily_close_list(self, trading_pair: str) -> List[Decimal]: """ Fetches binance candle stick data and returns a list daily close This is the API response data structure: [ [ 1499040000000, // Open time "0.01634790", // Open "0.80000000", // High "0.01575800", // Low "0.01577100", // Close "148976.11427815", // Volume 1499644799999, // Close time "2434.19055334", // Quote asset volume 308, // Number of trades "1756.87402397", // Taker buy base asset volume "28.46694368", // Taker buy quote asset volume "17928899.62484339" // Ignore. ] ] :param trading_pair: A market trading pair to :return: A list of daily close """ url = "https://api.binance.com/api/v3/klines" params = {"symbol": trading_pair.replace("-", ""), "interval": "1d"} records = requests.get(url=url, params=params).json() return [Decimal(str(record[4])) for record in records]
def is_linear_perpetual(trading_pair: str) -> bool: """ Returns True if trading_pair is in USDT(Linear) Perpetual """ _, quote_asset = split_hb_trading_pair(trading_pair) return quote_asset == "USDT"
def order_amount_prompt( cls, model_instance: 'AvellanedaMarketMakingConfigMap') -> str: trading_pair = model_instance.market base_asset, quote_asset = split_hb_trading_pair(trading_pair) return f"What is the amount of {base_asset} per order?"
def get_sell_collateral_token(self, trading_pair: str) -> str: _, quote = split_hb_trading_pair(trading_pair) return quote