def create_trade_fill_records( self, trade_price_amount_list, market_trading_pair_tuple: MarketTradingPairTuple, order_type, start_time, strategy): for i, trade_data in enumerate(trade_price_amount_list): yield { "config_file_path": "path", "strategy": strategy, "market": market_trading_pair_tuple.market.display_name, "symbol": market_trading_pair_tuple.trading_pair, "base_asset": market_trading_pair_tuple.base_asset, "quote_asset": market_trading_pair_tuple.quote_asset, "timestamp": start_time + i + 1, "order_id": f"{i}_{market_trading_pair_tuple.trading_pair}", "trade_type": trade_data[0], "order_type": order_type, "price": float(trade_data[1]), "amount": float(trade_data[2]), "trade_fee": TradeFee.to_json(TradeFee(0.01)), "exchange_trade_id": f"{i}_{market_trading_pair_tuple.trading_pair}" }
def estimate_fee(exchange: str, is_maker: bool) -> TradeFee: if exchange not in CONNECTOR_SETTINGS: raise Exception(f"Invalid connector. {exchange} does not exist in CONNECTOR_SETTINGS") use_gas = CONNECTOR_SETTINGS[exchange].use_eth_gas_lookup if use_gas: gas_amount = get_gas_price(in_gwei=False) * CONNECTOR_SETTINGS[exchange].gas_limit return TradeFee(percent=0, flat_fees=[("ETH", gas_amount)]) fee_type = CONNECTOR_SETTINGS[exchange].fee_type fee_token = CONNECTOR_SETTINGS[exchange].fee_token default_fees = CONNECTOR_SETTINGS[exchange].default_fees fee_side = "maker" if is_maker else "taker" override_key = f"{exchange}_{fee_side}" if fee_type is TradeFeeType.FlatFee: override_key += "_fee_amount" elif fee_type is TradeFeeType.Percent: override_key += "_fee" fee = default_fees[0] if is_maker else default_fees[1] fee_config = fee_overrides_config_map.get(override_key) if fee_config is not None and fee_config.value is not None: fee = fee_config.value fee = Decimal(str(fee)) if fee_type is TradeFeeType.Percent: return TradeFee(percent=fee / Decimal("100"), flat_fees=[]) elif fee_type is TradeFeeType.FlatFee: return TradeFee(percent=0, flat_fees=[(fee_token, fee)])
def estimate_fee(exchange: str, is_maker: bool) -> TradeFee: """ Estimate the fee of a transaction on any blockchain. exchange is the name of the exchange to query. is_maker if true look at fee from maker side, otherwise from taker side. """ if exchange not in CONNECTOR_SETTINGS: raise Exception( f"Invalid connector. {exchange} does not exist in CONNECTOR_SETTINGS" ) fee_type = CONNECTOR_SETTINGS[exchange].fee_type fee_token = CONNECTOR_SETTINGS[exchange].fee_token default_fees = CONNECTOR_SETTINGS[exchange].default_fees fee_side = "maker" if is_maker else "taker" override_key = f"{exchange}_{fee_side}" if fee_type is TradeFeeType.FlatFee: override_key += "_fee_amount" elif fee_type is TradeFeeType.Percent: override_key += "_fee" fee = default_fees[0] if is_maker else default_fees[1] fee_config = fee_overrides_config_map.get(override_key) if fee_config is not None and fee_config.value is not None: fee = fee_config.value fee = Decimal(str(fee)) if fee_type is TradeFeeType.Percent: return TradeFee(percent=fee / Decimal("100"), flat_fees=[]) elif fee_type is TradeFeeType.FlatFee: return TradeFee(percent=0, flat_fees=[(fee_token, fee)])
def simulate_limit_order_fill(market: Market, limit_order: LimitOrder): quote_currency_traded: float = float( float(limit_order.price) * float(limit_order.quantity)) base_currency_traded: float = float(limit_order.quantity) quote_currency: str = limit_order.quote_currency base_currency: str = limit_order.base_currency config: MarketConfig = market.config if limit_order.is_buy: market.set_balance( quote_currency, market.get_balance(quote_currency) - quote_currency_traded) market.set_balance( base_currency, market.get_balance(base_currency) + base_currency_traded) market.trigger_event( MarketEvent.OrderFilled, OrderFilledEvent( market.current_timestamp, limit_order.client_order_id, limit_order.symbol, TradeType.BUY, OrderType.LIMIT, float(limit_order.price), float(limit_order.quantity), TradeFee(0.0) # No fee in backtest market )) market.trigger_event( MarketEvent.BuyOrderCompleted, BuyOrderCompletedEvent( market.current_timestamp, limit_order.client_order_id, base_currency, quote_currency, base_currency if config.buy_fees_asset is AssetType.BASE_CURRENCY else quote_currency, base_currency_traded, quote_currency_traded, 0.0, OrderType.LIMIT)) else: market.set_balance( quote_currency, market.get_balance(quote_currency) + quote_currency_traded) market.set_balance( base_currency, market.get_balance(base_currency) - base_currency_traded) market.trigger_event( MarketEvent.OrderFilled, OrderFilledEvent(market.current_timestamp, limit_order.client_order_id, limit_order.symbol, TradeType.SELL, OrderType.LIMIT, float(limit_order.price), float(limit_order.quantity), TradeFee(0.0))) market.trigger_event( MarketEvent.SellOrderCompleted, SellOrderCompletedEvent( market.current_timestamp, limit_order.client_order_id, base_currency, quote_currency, base_currency if config.sell_fees_asset is AssetType.BASE_CURRENCY else quote_currency, base_currency_traded, quote_currency_traded, 0.0, OrderType.LIMIT))
def simulate_limit_order_fill(market: Market, limit_order: LimitOrder): quote_currency_traded: Decimal = limit_order.price * limit_order.quantity base_currency_traded: Decimal = limit_order.quantity quote_currency: str = limit_order.quote_currency base_currency: str = limit_order.base_currency config: MarketConfig = market.config if limit_order.is_buy: market.set_balance(quote_currency, market.get_balance(quote_currency) - quote_currency_traded) market.set_balance(base_currency, market.get_balance(base_currency) + base_currency_traded) market.trigger_event(MarketEvent.OrderFilled, OrderFilledEvent( market.current_timestamp, limit_order.client_order_id, limit_order.trading_pair, TradeType.BUY, OrderType.LIMIT, limit_order.price, limit_order.quantity, TradeFee(Decimal("0")) )) market.trigger_event(MarketEvent.BuyOrderCompleted, BuyOrderCompletedEvent( market.current_timestamp, limit_order.client_order_id, base_currency, quote_currency, base_currency if config.buy_fees_asset is AssetType.BASE_CURRENCY else quote_currency, base_currency_traded, quote_currency_traded, Decimal("0"), OrderType.LIMIT )) else: market.set_balance(quote_currency, market.get_balance(quote_currency) + quote_currency_traded) market.set_balance(base_currency, market.get_balance(base_currency) - base_currency_traded) market.trigger_event(MarketEvent.OrderFilled, OrderFilledEvent( market.current_timestamp, limit_order.client_order_id, limit_order.trading_pair, TradeType.SELL, OrderType.LIMIT, limit_order.price, limit_order.quantity, TradeFee(Decimal("0")) )) market.trigger_event(MarketEvent.SellOrderCompleted, SellOrderCompletedEvent( market.current_timestamp, limit_order.client_order_id, base_currency, quote_currency, base_currency if config.sell_fees_asset is AssetType.BASE_CURRENCY else quote_currency, base_currency_traded, quote_currency_traded, Decimal("0"), OrderType.LIMIT ))
def simulate_limit_order_fill(market: MockPaperExchange, limit_order: LimitOrder): quote_currency_traded: Decimal = limit_order.price * limit_order.quantity base_currency_traded: Decimal = limit_order.quantity quote_currency: str = limit_order.quote_currency base_currency: str = limit_order.base_currency if limit_order.is_buy: market.set_balance( quote_currency, market.get_balance(quote_currency) - quote_currency_traded) market.set_balance( base_currency, market.get_balance(base_currency) + base_currency_traded) market.trigger_event( MarketEvent.OrderFilled, OrderFilledEvent(market.current_timestamp, limit_order.client_order_id, limit_order.trading_pair, TradeType.BUY, OrderType.LIMIT, limit_order.price, limit_order.quantity, TradeFee(Decimal("0")))) market.trigger_event( MarketEvent.BuyOrderCompleted, BuyOrderCompletedEvent(market.current_timestamp, limit_order.client_order_id, base_currency, quote_currency, quote_currency, base_currency_traded, quote_currency_traded, Decimal("0"), OrderType.LIMIT)) else: market.set_balance( quote_currency, market.get_balance(quote_currency) + quote_currency_traded) market.set_balance( base_currency, market.get_balance(base_currency) - base_currency_traded) market.trigger_event( MarketEvent.OrderFilled, OrderFilledEvent(market.current_timestamp, limit_order.client_order_id, limit_order.trading_pair, TradeType.SELL, OrderType.LIMIT, limit_order.price, limit_order.quantity, TradeFee(Decimal("0")))) market.trigger_event( MarketEvent.SellOrderCompleted, SellOrderCompletedEvent(market.current_timestamp, limit_order.client_order_id, base_currency, quote_currency, quote_currency, base_currency_traded, quote_currency_traded, Decimal("0"), OrderType.LIMIT))
def test_estimate_fee(self): """ test the estimate_fee function """ # test against centralized exchanges self.assertEqual(estimate_fee("kucoin", True), TradeFee(percent=Decimal('0.001'), flat_fees=[])) self.assertEqual(estimate_fee("kucoin", False), TradeFee(percent=Decimal('0.001'), flat_fees=[])) self.assertEqual(estimate_fee("binance", True), TradeFee(percent=Decimal('0.001'), flat_fees=[])) self.assertEqual(estimate_fee("binance", False), TradeFee(percent=Decimal('0.001'), flat_fees=[])) # test against decentralized exchanges self.assertEqual(estimate_fee("beaxy", True), TradeFee(percent=Decimal('0.0015'), flat_fees=[])) self.assertEqual(estimate_fee("beaxy", False), TradeFee(percent=Decimal('0.0025'), flat_fees=[])) # test against an exchange with flat fees self.assertEqual( estimate_fee("bamboo_relay", True), TradeFee(percent=Decimal(0), flat_fees=[('ETH', Decimal('0'))])) self.assertEqual( estimate_fee("bamboo_relay", False), TradeFee(percent=Decimal(0), flat_fees=[('ETH', Decimal('0.00001'))])) # test against exchanges that do not exist in hummingbot.client.settings.CONNECTOR_SETTINGS self.assertRaisesRegex(Exception, "^Invalid connector", estimate_fee, "does_not_exist", True) self.assertRaisesRegex(Exception, "Invalid connector", estimate_fee, "does_not_exist", False)
def estimate_fee(exchange, is_maker): override_config_name_suffix = "_maker_fee" if is_maker else "_taker_fee" override_config_name = exchange + override_config_name_suffix if exchange in default_dex_estimate: override_config_name += "_amount" s_decimal_0 = Decimal(0) s_decimal_100 = Decimal(100) if is_maker: if exchange in default_cex_estimate: if fee_overrides_config_map[ override_config_name].value is not None: return TradeFee( percent=fee_overrides_config_map[override_config_name]. value / s_decimal_100) else: return TradeFee( percent=Decimal(default_cex_estimate[exchange][0]) / s_decimal_100) else: if fee_overrides_config_map[ override_config_name].value is not None: return TradeFee( percent=s_decimal_0, flat_fees=[ ("ETH", fee_overrides_config_map[override_config_name].value) ]) else: return TradeFee( percent=s_decimal_0, flat_fees=[("ETH", Decimal(default_dex_estimate[exchange][0]))]) else: if exchange in default_cex_estimate: if fee_overrides_config_map[ override_config_name].value is not None: return TradeFee( percent=fee_overrides_config_map[override_config_name]. value / s_decimal_100) else: return TradeFee( percent=Decimal(default_cex_estimate[exchange][1]) / s_decimal_100) else: if fee_overrides_config_map[ override_config_name].value is not None: return TradeFee( percent=s_decimal_0, flat_fees=[ ("ETH", fee_overrides_config_map[override_config_name].value) ]) else: return TradeFee( percent=s_decimal_0, flat_fees=[("ETH", Decimal(default_dex_estimate[exchange][1]))])
def _process_trade_message(self, order_msg: Dict[str, Any]): """ Updates in-flight order and trigger order filled event for trade message received. Triggers order completed event if the total executed amount equals to the specified order amount. """ # Only process trade when trade fees have been accounted for; when trade status is "settled". if order_msg["status"] != "settled": return ex_order_id = order_msg["order_id"] client_order_id = None for track_order in self.in_flight_orders.values(): if track_order.exchange_order_id == ex_order_id: client_order_id = track_order.client_order_id break if client_order_id is None: return tracked_order = self.in_flight_orders[client_order_id] updated = tracked_order.update_with_trade_update(order_msg) if not updated: return self.trigger_event( MarketEvent.OrderFilled, OrderFilledEvent( self.current_timestamp, tracked_order.client_order_id, tracked_order.trading_pair, tracked_order.trade_type, tracked_order.order_type, Decimal(str(order_msg["price"])), Decimal(str(order_msg["quantity"])), TradeFee(0.0, [(order_msg["fee_currency_id"], Decimal(str(order_msg["fee_amount"])))]), exchange_trade_id=order_msg["id"])) if math.isclose(tracked_order.executed_amount_base, tracked_order.amount) or \ tracked_order.executed_amount_base >= tracked_order.amount: tracked_order.last_state = "filled" self.logger().info( f"The {tracked_order.trade_type.name} order " f"{tracked_order.client_order_id} has completed " f"according to order status API.") event_tag = MarketEvent.BuyOrderCompleted if tracked_order.trade_type is TradeType.BUY \ else MarketEvent.SellOrderCompleted event_class = BuyOrderCompletedEvent if tracked_order.trade_type is TradeType.BUY \ else SellOrderCompletedEvent self.trigger_event( event_tag, event_class(self.current_timestamp, tracked_order.client_order_id, tracked_order.base_asset, tracked_order.quote_asset, tracked_order.fee_asset, tracked_order.executed_amount_base, tracked_order.executed_amount_quote, tracked_order.fee_paid, tracked_order.order_type, tracked_order.exchange_order_id)) self.stop_tracking_order(tracked_order.client_order_id)
def test_simulate_maker_market_trade(self, estimate_fee_mock): """ Test that we can set up a liquidity mining strategy, and a trade """ estimate_fee_mock.return_value = TradeFee(percent=0, flat_fees=[('ETH', Decimal(0.00005))]) # initiate self.clock.add_iterator(self.default_strategy) self.clock.backtest_til(self.start_timestamp) # assert that there are no active trades on initialization and before clock has moved forward self.assertEqual(0, len(self.default_strategy.active_orders)) # advance by one tick, the strategy will initiate two orders per pair self.clock.backtest_til(self.start_timestamp + self.clock_tick_size) self.assertEqual(4, len(self.default_strategy.active_orders)) # assert that a buy and sell order is made for each pair self.assertTrue(self.has_limit_order(self.default_strategy.active_orders, 'ETH-USDT', True, Decimal(99.95), Decimal(2.0))) self.assertTrue(self.has_limit_order(self.default_strategy.active_orders, 'ETH-USDT', False, Decimal(100.05), Decimal(2.0))) self.assertTrue(self.has_limit_order(self.default_strategy.active_orders, 'ETH-BTC', True, Decimal(99.95), Decimal(1.0005))) self.assertTrue(self.has_limit_order(self.default_strategy.active_orders, 'ETH-BTC', False, Decimal(100.05), Decimal(2))) # Simulate buy order fill self.clock.backtest_til(self.start_timestamp + 8) self.simulate_maker_market_trade(False, Decimal("50"), Decimal("1"), "ETH-USDT") self.assertEqual(3, len(self.default_strategy.active_orders))
def setUpClass(cls): cls.ev_loop = asyncio.get_event_loop() cls.clock: Clock = Clock(ClockMode.REALTIME) cls.stack: contextlib.ExitStack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls._patcher = unittest.mock.patch("hummingbot.strategy.amm_arb.data_types.estimate_fee") cls._url_mock = cls._patcher.start() cls._url_mock.return_value = TradeFee(percent=0, flat_fees=[])
def test_inventory_skew(self, estimate_fee_mock): """ When inventory_skew_enabled is true, the strategy will try to balance the amounts of base to match it """ estimate_fee_mock.return_value = TradeFee(percent=0, flat_fees=[('ETH', Decimal(0.00005))]) # initiate with similar balances so the skew is obvious usdt_balance = 1000 busd_balance = 1000 eth_balance = 1000 btc_balance = 1000 trading_pairs = list(map(lambda quote_asset: "ETH-" + quote_asset, ["USDT", "BUSD", "BTC"])) market, market_infos = self.create_market(trading_pairs, 100, {"USDT": usdt_balance, "BUSD": busd_balance, "ETH": eth_balance, "BTC": btc_balance}) skewed_base_strategy = LiquidityMiningStrategy() skewed_base_strategy.init_params( exchange=market, market_infos=market_infos, token="ETH", order_amount=Decimal(2), spread=Decimal(0.0005), inventory_skew_enabled=True, target_base_pct=Decimal(0.1), # less base, more quote order_refresh_time=5, order_refresh_tolerance_pct=Decimal(0.1), # tolerance of 10 % change ) unskewed_strategy = LiquidityMiningStrategy() unskewed_strategy.init_params( exchange=market, market_infos=market_infos, token="ETH", order_amount=Decimal(2), spread=Decimal(0.0005), inventory_skew_enabled=False, order_refresh_time=5, target_base_pct=Decimal(0.1), # this does nothing when inventory_skew_enabled is False order_refresh_tolerance_pct=Decimal(0.1), # tolerance of 10 % change ) self.clock.add_iterator(skewed_base_strategy) self.clock.backtest_til(self.start_timestamp + 10) self.clock.add_iterator(unskewed_strategy) self.clock.backtest_til(self.start_timestamp + 20) # iterate through pairs in skewed and unskewed strategy for unskewed_order in unskewed_strategy.active_orders: for skewed_base_order in skewed_base_strategy.active_orders: # if the trading_pair and trade type are the same, compare them if skewed_base_order.trading_pair == unskewed_order.trading_pair and \ skewed_base_order.is_buy == unskewed_order.is_buy: if skewed_base_order.is_buy: # the skewed strategy tries to buy more quote thant the unskewed one self.assertGreater(skewed_base_order.price, unskewed_order.price) else: # trying to keep less base self.assertLessEqual(skewed_base_order.price, unskewed_order.price)
async def _process_trade_message(self, trade_msg: Dict[str, Any]): """ Updates in-flight order and trigger order filled event for trade message received. Triggers order completed event if the total executed amount equals to the specified order amount. """ # I'm not sure why it's needed. # for order in self._in_flight_orders.values(): # await order.get_exchange_order_id() track_order = [o for o in self._in_flight_orders.values() if trade_msg["order_id"] == o.exchange_order_id] if not track_order: return tracked_order = track_order[0] updated = tracked_order.update_with_trade_update(trade_msg) if not updated: return quantity = trade_msg["quantity"] if trade_msg.get("quantity") is not None else "0" price = trade_msg["price"] if trade_msg.get("price") is not None else "0" fee_amount = trade_msg["fee_amount"] if trade_msg.get("fee_amount") is not None else "0" fee_amount = "0" if not fee_amount else fee_amount self.trigger_event( MarketEvent.OrderFilled, OrderFilledEvent( self.current_timestamp, tracked_order.client_order_id, tracked_order.trading_pair, tracked_order.trade_type, tracked_order.order_type, Decimal(price), Decimal(quantity), TradeFee(0.0, [(trade_msg["fee_currency_id"], Decimal(fee_amount))]), exchange_trade_id=trade_msg["order_id"] ) ) if math.isclose(tracked_order.executed_amount_base, tracked_order.amount) or \ tracked_order.executed_amount_base >= tracked_order.amount: tracked_order.last_state = "FILLED" self.logger().info(f"The {tracked_order.trade_type.name} order " f"{tracked_order.client_order_id} has completed " f"according to order status API.") event_tag = MarketEvent.BuyOrderCompleted if tracked_order.trade_type is TradeType.BUY \ else MarketEvent.SellOrderCompleted event_class = BuyOrderCompletedEvent if tracked_order.trade_type is TradeType.BUY \ else SellOrderCompletedEvent self.trigger_event(event_tag, event_class(self.current_timestamp, tracked_order.client_order_id, tracked_order.base_asset, tracked_order.quote_asset, tracked_order.fee_asset, tracked_order.executed_amount_base, tracked_order.executed_amount_quote, tracked_order.fee_paid, tracked_order.order_type)) self.stop_tracking_order(tracked_order.client_order_id)
def _did_fill_order(self, event_tag: int, market: ConnectorBase, evt: OrderFilledEvent): if threading.current_thread() != threading.main_thread(): self._ev_loop.call_soon_threadsafe(self._did_fill_order, event_tag, market, evt) return session: Session = self.session base_asset, quote_asset = evt.trading_pair.split("-") timestamp: int = self.db_timestamp event_type: MarketEvent = self.market_event_tag_map[event_tag] order_id: str = evt.order_id # Try to find the order record, and update it if necessary. order_record: Optional[Order] = session.query(Order).filter( Order.id == order_id).one_or_none() if order_record is not None: order_record.last_status = event_type.name order_record.last_update_timestamp = timestamp # Order status and trade fill record should be added even if the order record is not found, because it's # possible for fill event to come in before the order created event for market orders. order_status: OrderStatus = OrderStatus(order_id=order_id, timestamp=timestamp, status=event_type.name) trade_fill_record: TradeFill = TradeFill( config_file_path=self.config_file_path, strategy=self.strategy_name, market=market.display_name, symbol=evt.trading_pair, base_asset=base_asset, quote_asset=quote_asset, timestamp=timestamp, order_id=order_id, trade_type=evt.trade_type.name, order_type=evt.order_type.name, price=float(evt.price) if evt.price == evt.price else 0, amount=float(evt.amount), leverage=evt.leverage if evt.leverage else 1, trade_fee=TradeFee.to_json(evt.trade_fee), exchange_trade_id=evt.exchange_trade_id, position=evt.position if evt.position else "NILL", ) session.add(order_status) session.add(trade_fill_record) self.save_market_states(self._config_file_path, market, no_commit=True) session.commit() market.add_trade_fills_from_market_recorder({ TradeFillOrderDetails(trade_fill_record.market, trade_fill_record.exchange_trade_id, trade_fill_record.symbol) }) self.append_to_csv(trade_fill_record)
async def update_swap_order(self, update_result: Dict[str, any], tracked_order: UniswapInFlightOrder): if update_result.get("confirmed", False): if update_result["receipt"].get("status", 0) == 1: order_id = await tracked_order.get_exchange_order_id() gas_used = update_result["receipt"]["gasUsed"] gas_price = tracked_order.gas_price fee = Decimal(str(gas_used)) * Decimal( str(gas_price)) / Decimal(str(1e9)) self.trigger_event( MarketEvent.OrderFilled, OrderFilledEvent(self.current_timestamp, tracked_order.client_order_id, tracked_order.trading_pair, tracked_order.trade_type, tracked_order.order_type, Decimal(str(tracked_order.price)), Decimal(str(tracked_order.amount)), TradeFee(0.0, [(tracked_order.fee_asset, Decimal(str(fee)))]), exchange_trade_id=order_id)) tracked_order.last_state = "FILLED" self.logger().info( f"The {tracked_order.trade_type.name} order " f"{tracked_order.client_order_id} has completed " f"according to order status API.") event_tag = MarketEvent.BuyOrderCompleted if tracked_order.trade_type is TradeType.BUY \ else MarketEvent.SellOrderCompleted event_class = BuyOrderCompletedEvent if tracked_order.trade_type is TradeType.BUY \ else SellOrderCompletedEvent self.trigger_event( event_tag, event_class(self.current_timestamp, tracked_order.client_order_id, tracked_order.base_asset, tracked_order.quote_asset, tracked_order.fee_asset, tracked_order.executed_amount_base, tracked_order.executed_amount_quote, float(fee), tracked_order.order_type)) self.stop_tracking_order(tracked_order.client_order_id) else: self.logger().info( f"The {tracked_order.type} order {tracked_order.client_order_id} has failed according to order status API. " ) self.trigger_event( MarketEvent.OrderFailure, MarketOrderFailureEvent(self.current_timestamp, tracked_order.client_order_id, tracked_order.order_type)) self.stop_tracking_order(tracked_order.client_order_id)
def get_fee(self, base_currency: str, quote_currency: str, order_type: OrderType, order_side: TradeType, amount: Decimal, price: Decimal = s_decimal_NaN) -> TradeFee: """ To get trading fee, this function is simplified by using fee override configuration. Most parameters to this function are ignore except order_type. Use OrderType.LIMIT_MAKER to specify you want trading fee for maker order. """ is_maker = order_type is OrderType.LIMIT_MAKER return TradeFee(percent=self.estimate_fee_pct(is_maker))
def get_trades() -> List[TradeFill]: trade_fee = TradeFee(percent=Decimal("5")) trades = [ TradeFill( id=1, config_file_path="some-strategy.yml", strategy="pure_market_making", market="binance", symbol="BTC-USDT", base_asset="BTC", quote_asset="USDT", timestamp=int(time.time()), order_id="someId", trade_type="BUY", order_type="LIMIT", price=1, amount=2, leverage=1, trade_fee=TradeFee.to_json(trade_fee), exchange_trade_id="someExchangeId", ) ] return trades
def test_process_order_fill_event_sell_no_initial_cost_set(self): amount = Decimal("1") price = Decimal("9000") event = OrderFilledEvent( timestamp=1, order_id="order1", trading_pair=self.trading_pair, trade_type=TradeType.SELL, order_type=OrderType.LIMIT, price=price, amount=amount, trade_fee=TradeFee(percent=Decimal("0"), flat_fees=[]), ) with self.assertRaises(RuntimeError): self.delegate.process_order_fill_event(event)
def get_fee(self, base_currency: str, quote_currency: str, order_type: OrderType, order_side: TradeType, amount: Decimal, price: Decimal = s_decimal_NaN) -> TradeFee: """For more information: https://ascendex.github.io/ascendex-pro-api/#place-order.""" trading_pair = f"{base_currency}-{quote_currency}" trading_rule = self._trading_rules[trading_pair] fee_percent = Decimal("0") if order_side == TradeType.BUY: if trading_rule.commission_type == AscendExCommissionType.QUOTE: fee_percent = trading_rule.commission_reserve_rate elif trading_rule.commission_type == AscendExCommissionType.BASE: fee_percent = trading_rule.commission_reserve_rate return TradeFee(percent=fee_percent)
def test_budget_allocation(self, estimate_fee_mock): """ Liquidity mining strategy budget allocation is different from pmm, it depends on the token base and it splits its budget between the quote tokens. """ estimate_fee_mock.return_value = TradeFee(percent=0, flat_fees=[('ETH', Decimal(0.00005))]) # initiate usdt_balance = 1000 busd_balance = 900 eth_balance = 100 btc_balance = 10 trading_pairs = list(map(lambda quote_asset: "ETH-" + quote_asset, ["USDT", "BUSD", "BTC"])) market, market_infos = self.create_market(trading_pairs, 100, {"USDT": usdt_balance, "BUSD": busd_balance, "ETH": eth_balance, "BTC": btc_balance}) strategy = LiquidityMiningStrategy() strategy.init_params( exchange=market, market_infos=market_infos, token="ETH", order_amount=Decimal(2), spread=Decimal(0.0005), inventory_skew_enabled=False, target_base_pct=Decimal(0.5), order_refresh_time=5, order_refresh_tolerance_pct=Decimal(0.1), # tolerance of 10 % change ) self.clock.add_iterator(strategy) self.clock.backtest_til(self.start_timestamp + 10) # there should be a buy and sell budget for each pair self.assertEqual(len(strategy.sell_budgets), 3) self.assertEqual(len(strategy.buy_budgets), 3) # the buy budgets use all of the available balance for the quote tokens self.assertEqual(strategy.buy_budgets["ETH-USDT"], usdt_balance) self.assertEqual(strategy.buy_budgets["ETH-BTC"], btc_balance) self.assertEqual(strategy.buy_budgets["ETH-BUSD"], busd_balance) # the sell budget tries to evenly split the base token between the quote tokens self.assertLess(strategy.sell_budgets["ETH-USDT"], eth_balance * 0.4) self.assertLess(strategy.sell_budgets["ETH-BTC"], eth_balance * 0.4) self.assertLess(strategy.sell_budgets["ETH-BUSD"], eth_balance * 0.4)
def test_multiple_markets(self, estimate_fee_mock): """ Liquidity Mining supports one base asset but multiple quote assets. This shows that the user can successfully provide liquidity for two different pairs and the market can execute the other side of them. """ estimate_fee_mock.return_value = TradeFee(percent=0, flat_fees=[('ETH', Decimal(0.00005))]) # initiate self.clock.add_iterator(self.default_strategy) self.clock.backtest_til(self.start_timestamp + self.clock_tick_size) # ETH-USDT self.simulate_maker_market_trade(False, 50, 1, "ETH-USDT") self.clock.backtest_til(self.start_timestamp + 8) # ETH-BTC self.simulate_maker_market_trade(False, 50, 1, "ETH-BTC") self.clock.backtest_til(self.start_timestamp + 16)
def test_volatility(self, estimate_fee_mock, get_mid_price_mock): """ Assert that volatility information is updated after the expected number of intervals """ estimate_fee_mock.return_value = TradeFee(percent=0, flat_fees=[('ETH', Decimal(0.00005))]) # initiate with similar balances so the skew is obvious usdt_balance = 1000 eth_balance = 1000 trading_pairs = list(map(lambda quote_asset: "ETH-" + quote_asset, ["USDT"])) market, market_infos = self.create_market(trading_pairs, 100, {"USDT": usdt_balance, "ETH": eth_balance}) strategy = LiquidityMiningStrategy() strategy.init_params( exchange=market, market_infos=market_infos, token="ETH", order_amount=Decimal(2), spread=Decimal(0.0005), inventory_skew_enabled=False, target_base_pct=Decimal(0.5), # less base, more quote order_refresh_time=1, order_refresh_tolerance_pct=Decimal(0.1), # tolerance of 10 % change # volatility_interval=2, # avg_volatility_period=2, # volatility_to_spread_multiplier=2, ) get_mid_price_mock.return_value = Decimal(100.0) self.clock.add_iterator(strategy) self.clock.backtest_til(self.start_timestamp + 1) # update prices to create volatility after 2 intervals get_mid_price_mock.return_value = Decimal(105.0) self.clock.backtest_til(self.start_timestamp + 2) get_mid_price_mock.return_value = Decimal(110) self.clock.backtest_til(self.start_timestamp + 3) # assert that volatility is none zero self.assertAlmostEqual(float(strategy.market_status_df().loc[0, 'Volatility'].strip('%')), 10.00, delta=0.1)
def estimate_fee(exchange: str, is_maker: bool) -> Decimal: override_config_name_suffix = "_maker_fee" if is_maker else "_taker_fee" override_config_name = exchange + override_config_name_suffix s_decimal_0 = Decimal("0") s_decimal_100 = Decimal("100") for connector_type, connectors in ALL_CONNECTORS.items(): if exchange in connectors: try: path = f"hummingbot.connector.{connector_type}.{exchange}.{exchange}_utils" is_cex = getattr(__import__(path, fromlist=["CENTRALIZED"]), "CENTRALIZED") fee = getattr(__import__(path, fromlist=["DEFAULT_FEES"]), "DEFAULT_FEES") except Exception: pass if is_maker: if is_cex: if fee_overrides_config_map[override_config_name].value is not None: return TradeFee(percent=fee_overrides_config_map[override_config_name].value / s_decimal_100) else: return TradeFee(percent=Decimal(fee[0]) / s_decimal_100) else: override_config_name += "_amount" if fee_overrides_config_map[override_config_name].value is not None: return TradeFee(percent=s_decimal_0, flat_fees=[("ETH", fee_overrides_config_map[override_config_name].value)]) else: return TradeFee(percent=s_decimal_0, flat_fees=[("ETH", Decimal(fee[0]))]) else: if is_cex: if fee_overrides_config_map[override_config_name].value is not None: return TradeFee(percent=fee_overrides_config_map[override_config_name].value / s_decimal_100) else: return TradeFee(percent=Decimal(fee[1]) / s_decimal_100) else: override_config_name += "_amount" if fee_overrides_config_map[override_config_name].value is not None: return TradeFee(percent=s_decimal_0, flat_fees=[("ETH", fee_overrides_config_map[override_config_name].value)]) else: return TradeFee(percent=s_decimal_0, flat_fees=[("ETH", Decimal(fee[1]))])
async def _trigger_order_fill(self, tracked_order: CoinzoomInFlightOrder, update_msg: Dict[str, Any]): self.trigger_event( MarketEvent.OrderFilled, OrderFilledEvent( self.current_timestamp, tracked_order.client_order_id, tracked_order.trading_pair, tracked_order.trade_type, tracked_order.order_type, Decimal(str(update_msg.get("averagePrice", update_msg.get("price", "0")))), tracked_order.executed_amount_base, TradeFee(percent=update_msg["trade_fee"]), update_msg.get("exchange_trade_id", update_msg.get("id", update_msg.get("orderId"))) ) ) if math.isclose(tracked_order.executed_amount_base, tracked_order.amount) or \ tracked_order.executed_amount_base >= tracked_order.amount or \ tracked_order.is_done: tracked_order.last_state = "FILLED" self.logger().info(f"The {tracked_order.trade_type.name} order " f"{tracked_order.client_order_id} has completed " f"according to order status API.") event_tag = MarketEvent.BuyOrderCompleted if tracked_order.trade_type is TradeType.BUY \ else MarketEvent.SellOrderCompleted event_class = BuyOrderCompletedEvent if tracked_order.trade_type is TradeType.BUY \ else SellOrderCompletedEvent await asyncio.sleep(0.1) self.trigger_event(event_tag, event_class(self.current_timestamp, tracked_order.client_order_id, tracked_order.base_asset, tracked_order.quote_asset, tracked_order.fee_asset, tracked_order.executed_amount_base, tracked_order.executed_amount_quote, tracked_order.fee_paid, tracked_order.order_type)) self.stop_tracking_order(tracked_order.client_order_id)
def test_tolerance_level(self, estimate_fee_mock): """ Test tolerance level """ estimate_fee_mock.return_value = TradeFee(percent=0, flat_fees=[('ETH', Decimal(0.00005))]) # initiate strategy and add active orders self.clock.add_iterator(self.default_strategy) self.clock.backtest_til(self.start_timestamp + 10) # the order tolerance is 1% # set the orders to the same values proposal = Proposal("ETH-USDT", PriceSize(100, 1), PriceSize(100, 1)) self.assertTrue(self.default_strategy.is_within_tolerance(self.default_strategy.active_orders, proposal)) # update orders to withint the tolerance proposal = Proposal("ETH-USDT", PriceSize(109, 1), PriceSize(91, 1)) self.assertTrue(self.default_strategy.is_within_tolerance(self.default_strategy.active_orders, proposal)) # push the orders beyond the tolerance, this proposal should return False proposal = Proposal("ETH-USDT", PriceSize(150, 1), PriceSize(50, 1)) self.assertFalse(self.default_strategy.is_within_tolerance(self.default_strategy.active_orders, proposal))
def _did_fill_order(self, event_tag: int, market: MarketBase, evt: OrderFilledEvent): session: Session = self.session base_asset, quote_asset = market.split_symbol(evt.symbol) timestamp: int = self.db_timestamp event_type: MarketEvent = self.market_event_tag_map[event_tag] order_id: str = evt.order_id # Try to find the order record, and update it if necessary. order_record: Optional[Order] = session.query(Order).filter( Order.id == order_id).one_or_none() if order_record is not None: order_record.last_status = event_type.name order_record.last_update_timestamp = timestamp # Order status and trade fill record should be added even if the order record is not found, because it's # possible for fill event to come in before the order created event for market orders. order_status: OrderStatus = OrderStatus(order_id=order_id, timestamp=timestamp, status=event_type.name) trade_fill_record: TradeFill = TradeFill( config_file_path=self.config_file_path, strategy=self.strategy_name, market=market.name, symbol=evt.symbol, base_asset=base_asset, quote_asset=quote_asset, timestamp=timestamp, order_id=order_id, trade_type=evt.trade_type.name, order_type=evt.order_type.name, price=evt.price, amount=evt.amount, trade_fee=TradeFee.to_json(evt.trade_fee), exchange_trade_id=evt.exchange_trade_id) session.add(order_status) session.add(trade_fill_record) self.save_market_states(self._config_file_path, market, no_commit=True) session.commit()
def test_process_order_fill_event_buy(self): amount = Decimal("1") price = Decimal("9000") event = OrderFilledEvent( timestamp=1, order_id="order1", trading_pair=self.trading_pair, trade_type=TradeType.BUY, order_type=OrderType.LIMIT, price=price, amount=amount, trade_fee=TradeFee(percent=Decimal("0"), flat_fees=[]), ) # first event creates DB record self.delegate.process_order_fill_event(event) count = self._session.query(InventoryCost).count() self.assertEqual(count, 1) # second event causes update to existing record self.delegate.process_order_fill_event(event) record = InventoryCost.get_record(self._session, self.base_asset, self.quote_asset) self.assertEqual(record.base_volume, amount * 2) self.assertEqual(record.quote_volume, price * 2)
def test_process_order_fill_event_sell(self): amount = Decimal("1") price = Decimal("9000") # Test when no records self.assertIsNone(self.delegate.get_price()) record = InventoryCost( base_asset=self.base_asset, quote_asset=self.quote_asset, base_volume=amount, quote_volume=amount * price, ) self._session.add(record) self._session.commit() amount_sell = Decimal("0.5") price_sell = Decimal("10000") event = OrderFilledEvent( timestamp=1, order_id="order1", trading_pair=self.trading_pair, trade_type=TradeType.SELL, order_type=OrderType.LIMIT, price=price_sell, amount=amount_sell, trade_fee=TradeFee(percent=Decimal("0"), flat_fees=[]), ) self.delegate.process_order_fill_event(event) record = InventoryCost.get_record(self._session, self.base_asset, self.quote_asset) # Remaining base volume reduced by sold amount self.assertEqual(record.base_volume, amount - amount_sell) # Remaining quote volume has been reduced using original price self.assertEqual(record.quote_volume, amount_sell * price)
def simulate_limit_order_fill(market: Market, limit_order: LimitOrder, timestamp: float = 0): quote_currency_traded: Decimal = limit_order.price * limit_order.quantity base_currency_traded: Decimal = limit_order.quantity quote_currency: str = limit_order.quote_currency base_currency: str = limit_order.base_currency config: MarketConfig = market.config trade_event: OrderBookTradeEvent = OrderBookTradeEvent( trading_pair=limit_order.trading_pair, timestamp=timestamp, type=TradeType.BUY if limit_order.is_buy else TradeType.SELL, price=limit_order.price, amount=limit_order.quantity ) market.get_order_book(limit_order.trading_pair).apply_trade(trade_event) if limit_order.is_buy: market.set_balance(quote_currency, market.get_balance(quote_currency) - quote_currency_traded) market.set_balance(base_currency, market.get_balance(base_currency) + base_currency_traded) market.trigger_event(MarketEvent.OrderFilled, OrderFilledEvent( market.current_timestamp, limit_order.client_order_id, limit_order.trading_pair, TradeType.BUY, OrderType.LIMIT, limit_order.price, limit_order.quantity, TradeFee(Decimal(0.0)) )) market.trigger_event(MarketEvent.BuyOrderCompleted, BuyOrderCompletedEvent( market.current_timestamp, limit_order.client_order_id, base_currency, quote_currency, base_currency if config.buy_fees_asset is AssetType.BASE_CURRENCY else quote_currency, base_currency_traded, quote_currency_traded, Decimal(0.0), OrderType.LIMIT )) else: market.set_balance(quote_currency, market.get_balance(quote_currency) + quote_currency_traded) market.set_balance(base_currency, market.get_balance(base_currency) - base_currency_traded) market.trigger_event(MarketEvent.OrderFilled, OrderFilledEvent( market.current_timestamp, limit_order.client_order_id, limit_order.trading_pair, TradeType.SELL, OrderType.LIMIT, limit_order.price, limit_order.quantity, TradeFee(Decimal(0.0)) )) market.trigger_event(MarketEvent.SellOrderCompleted, SellOrderCompletedEvent( market.current_timestamp, limit_order.client_order_id, base_currency, quote_currency, base_currency if config.sell_fees_asset is AssetType.BASE_CURRENCY else quote_currency, base_currency_traded, quote_currency_traded, Decimal(0.0), OrderType.LIMIT ))
def setUpClass(cls): cls._patcher = unittest.mock.patch( "hummingbot.connector.connector_base.estimate_fee") cls._url_mock = cls._patcher.start() cls._url_mock.return_value = TradeFee(percent=Decimal("0"), flat_fees=[])