def test_performance_metrics(self): trade_fee = AddedToCostTradeFee( flat_fees=[TokenAmount(quote, Decimal("0"))]) trades = [ TradeFill( config_file_path="some-strategy.yml", strategy="pure_market_making", market="binance", symbol=trading_pair, base_asset=base, quote_asset=quote, timestamp=int(time.time()), order_id="someId0", trade_type="BUY", order_type="LIMIT", price=100, amount=10, trade_fee=trade_fee.to_json(), exchange_trade_id="someExchangeId0", position=PositionAction.NIL.value, ), TradeFill( config_file_path="some-strategy.yml", strategy="pure_market_making", market="binance", symbol=trading_pair, base_asset=base, quote_asset=quote, timestamp=int(time.time()), order_id="someId1", trade_type="SELL", order_type="LIMIT", price=120, amount=15, trade_fee=trade_fee.to_json(), exchange_trade_id="someExchangeId1", position=PositionAction.NIL.value, ) ] cur_bals = {base: 100, quote: 10000} metrics = asyncio.get_event_loop().run_until_complete( PerformanceMetrics.create("hbot_exchange", trading_pair, trades, cur_bals)) self.assertEqual(Decimal("200"), metrics.trade_pnl) print(metrics)
def test_attribute_names_for_file_export_are_valid(self): trade_fill = TradeFill(config_file_path=self.config_file_path, strategy=self.strategy_name, market=self.display_name, symbol=self.symbol, base_asset=self.base, quote_asset=self.quote, timestamp=int(time.time()), order_id="OID1", trade_type=TradeType.BUY.name, order_type=OrderType.LIMIT.name, price=Decimal(1000), amount=Decimal(1), leverage=1, trade_fee=AddedToCostTradeFee().to_json(), exchange_trade_id="EOID1", position="NILL") values = [ getattr(trade_fill, attribute) for attribute in TradeFill.attribute_names_for_file_export() ] expected_values = [ trade_fill.exchange_trade_id, trade_fill.config_file_path, trade_fill.strategy, trade_fill.market, trade_fill.symbol, trade_fill.base_asset, trade_fill.quote_asset, trade_fill.timestamp, trade_fill.order_id, trade_fill.trade_type, trade_fill.order_type, trade_fill.price, trade_fill.amount, trade_fill.leverage, trade_fill.trade_fee, trade_fill.position, ] self.assertEqual(expected_values, values)
def save_trade_fill_records( self, trade_price_amount_list, market_trading_pair_tuple: MarketTradingPairTuple, order_type, start_time, strategy): trade_records: List[TradeFill] = [] for trade in self.create_trade_fill_records(trade_price_amount_list, market_trading_pair_tuple, order_type, start_time, strategy): trade_records.append(TradeFill(**trade)) self.trade_fill_sql.get_shared_session().add_all(trade_records)
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 base_asset, quote_asset = evt.trading_pair.split("-") timestamp: int = int(evt.timestamp * 1e3) if evt.timestamp is not None else self.db_timestamp event_type: MarketEvent = self.market_event_tag_map[event_tag] order_id: str = evt.order_id with self._sql_manager.get_new_session() as session: with session.begin(): # 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=Decimal( evt.price) if evt.price == evt.price else Decimal(0), amount=Decimal(evt.amount), leverage=evt.leverage if evt.leverage else 1, trade_fee=evt.trade_fee.to_json(), exchange_trade_id=evt.exchange_trade_id, position=evt.position if evt.position else PositionAction.NIL.value, ) session.add(order_status) session.add(trade_fill_record) self.save_market_states(self._config_file_path, market, session=session) 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)
def calculate_asset_delta_from_trades(self, analysis_start_time: int, current_startegy_name: str, market_trading_pair_tuples: List[MarketTradingPairTuple] ) -> Dict[MarketTradingPairTuple, Dict[str, Decimal]]: """ Calculate spent and acquired amount for each asset from trades. Example: A buy trade of ETH_USD for price 100 and amount 1, will have 1 ETH as acquired and 100 USD as spent amount. :param analysis_start_time: Start timestamp for the trades to be quired :param current_startegy_name: Name of the currently configured strategy :param market_trading_pair_tuples: Current MarketTradingPairTuple :return: Dictionary consisting of spent and acquired amount for each assets """ market_trading_pair_stats: Dict[MarketTradingPairTuple, Dict[str, Decimal]] = {} for market_trading_pair_tuple in market_trading_pair_tuples: asset_stats: Dict[str, Decimal] = { market_trading_pair_tuple.base_asset.upper(): {"spent": s_decimal_0, "acquired": s_decimal_0}, market_trading_pair_tuple.quote_asset.upper(): {"spent": s_decimal_0, "acquired": s_decimal_0} } queried_trades: List[TradeFill] = TradeFill.get_trades(self.sql_manager.get_shared_session(), start_time=analysis_start_time, market=market_trading_pair_tuple.market.display_name, strategy=current_startegy_name) if not queried_trades: market_trading_pair_stats[market_trading_pair_tuple] = { "starting_quote_rate": market_trading_pair_tuple.get_mid_price(), "asset": asset_stats } continue for trade in queried_trades: # For each trade, calculate the spent and acquired amount of the corresponding base and quote asset trade_side: str = trade.trade_type base_asset: str = trade.base_asset.upper() quote_asset: str = trade.quote_asset.upper() base_delta, quote_delta = PerformanceAnalysis.calculate_trade_asset_delta_with_fees(trade) if trade_side == TradeType.SELL.name: asset_stats[base_asset]["spent"] += base_delta asset_stats[quote_asset]["acquired"] += quote_delta elif trade_side == TradeType.BUY.name: asset_stats[base_asset]["acquired"] += base_delta asset_stats[quote_asset]["spent"] += quote_delta market_trading_pair_stats[market_trading_pair_tuple] = { "starting_quote_rate": Decimal(repr(queried_trades[0].price)), "asset": asset_stats } return market_trading_pair_stats
async def submit_trades(self): try: trades: List[TradeFill] = await self.get_unsubmitted_trades() # only submit 5000 at a time formatted_trades: List[Dict[str, Any]] = [TradeFill.to_bounty_api_json(trade) for trade in trades[:5000]] if self._last_submitted_trade_timestamp >= 0 and len(formatted_trades) > 0: url = f"{self.LIQUIDITY_BOUNTY_REST_API}/trade" results = await self.authenticated_request("POST", url, json={"trades": formatted_trades}) num_submitted = results.get("trades_submitted", 0) num_recorded = results.get("trades_recorded", 0) if num_submitted != num_recorded: self.logger().warning(f"Failed to submit {num_submitted - num_recorded} trade(s)") if num_recorded > 0: self.logger().info(f"Successfully sent {num_recorded} trade(s) to claim bounty") except Exception: raise
def _did_fill_order(self, event_tag: int, market: MarketBase, 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 = market.split_trading_pair(evt.trading_pair) 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), 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() self.append_to_csv(trade_fill_record)
def test_calculate_fees_in_quote_for_one_trade_fill_with_fees_different_tokens( self): rate_oracle = RateOracle() rate_oracle._prices["DAI-COINALPHA"] = Decimal("2") rate_oracle._prices["USDT-DAI"] = Decimal("0.9") RateOracle._shared_instance = rate_oracle performance_metric = PerformanceMetrics() flat_fees = [ TokenAmount(token="USDT", amount=Decimal("10")), TokenAmount(token="DAI", amount=Decimal("5")), ] trade = TradeFill( config_file_path="some-strategy.yml", strategy="pure_market_making", market="binance", symbol="HBOT-COINALPHA", base_asset="HBOT", quote_asset="COINALPHA", timestamp=int(time.time()), order_id="someId0", trade_type="BUY", order_type="LIMIT", price=1000, amount=1, trade_fee=AddedToCostTradeFee(percent=Decimal("0.1"), percent_token="COINALPHA", flat_fees=flat_fees).to_json(), exchange_trade_id="someExchangeId0", position=PositionAction.NIL.value, ) self.async_run_with_timeout( performance_metric._calculate_fees(quote="COINALPHA", trades=[trade])) expected_fee_amount = Decimal(str(trade.amount)) * Decimal( str(trade.price)) * Decimal("0.1") expected_fee_amount += flat_fees[0].amount * Decimal("0.9") * Decimal( "2") expected_fee_amount += flat_fees[1].amount * Decimal("2") self.assertEqual(expected_fee_amount, performance_metric.fee_in_quote)
def get_trades(self) -> List[TradeFill]: trade_fee = AddedToCostTradeFee(percent=Decimal("5")) trades = [ TradeFill( config_file_path=f"{self.mock_strategy_name}.yml", strategy=self.mock_strategy_name, 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=trade_fee.to_json(), exchange_trade_id="someExchangeId", ) ] return trades
def test_attribute_names_for_file_export(self): expected_attributes = [ "exchange_trade_id", "config_file_path", "strategy", "market", "symbol", "base_asset", "quote_asset", "timestamp", "order_id", "trade_type", "order_type", "price", "amount", "leverage", "trade_fee", "position", ] self.assertEqual(expected_attributes, TradeFill.attribute_names_for_file_export())
def append_to_csv(self, trade: TradeFill): csv_filename = "trades_" + trade.config_file_path[:-4] + ".csv" csv_path = os.path.join(data_path(), csv_filename) field_names = tuple(trade.attribute_names_for_file_export()) field_data = tuple(getattr(trade, attr) for attr in field_names) # adding extra field "age" # // indicates order is a paper order so 'n/a'. For real orders, calculate age. age = pd.Timestamp(int((trade.timestamp * 1e-3) - (trade.order.creation_timestamp * 1e-3)), unit='s').strftime( '%H:%M:%S') if (trade.order is not None and "//" not in trade.order_id) else "n/a" field_names += ("age",) field_data += (age,) if (os.path.exists(csv_path) and (not self._csv_matches_header(csv_path, field_names))): move(csv_path, csv_path[:-4] + '_old_' + pd.Timestamp.utcnow().strftime("%Y%m%d-%H%M%S") + ".csv") if not os.path.exists(csv_path): df_header = pd.DataFrame([field_names]) df_header.to_csv(csv_path, mode='a', header=False, index=False) df = pd.DataFrame([field_data]) df.to_csv(csv_path, mode='a', header=False, index=False)
def export_trades( self, # type: HummingbotApplication path: str = ""): if threading.current_thread() != threading.main_thread(): self.ev_loop.call_soon_threadsafe(self.export_trades, path) return if not path: fname = f"trades_{pd.Timestamp.now().strftime('%Y-%m-%d-%H-%M-%S')}.csv" path = join(dirname(__file__), f"../../../logs/{fname}") trades: List[TradeFill] = self._get_trades_from_session(self.init_time) if len(trades) > 0: try: df: pd.DataFrame = TradeFill.to_pandas(trades) df.to_csv(path, header=True) self._notify(f"Successfully saved trades to {path}") except Exception as e: self._notify(f"Error saving trades to {path}: {e}") else: self._notify("No past trades to export.")
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
async def export_trades( self, # type: HummingbotApplication ): trades: List[TradeFill] = self._get_trades_from_session(self.init_time) if len(trades) == 0: self._notify("No past trades to export.") return self.placeholder_mode = True self.app.hide_input = True path = global_config_map["log_file_path"].value if path is None: path = DEFAULT_LOG_FILE_PATH file_name = await self.prompt_new_export_file_name(path) file_path = os.path.join(path, file_name) try: df: pd.DataFrame = TradeFill.to_pandas(trades) df.to_csv(file_path, header=True) self._notify(f"Successfully exported trades to {file_path}") except Exception as e: self._notify(f"Error exporting trades to {path}: {e}") self.app.change_prompt(prompt=">>> ") self.placeholder_mode = False self.app.hide_input = False
def test_list_trades(self, notify_mock): global_config_map["tables_format"].value = "psql" captures = [] notify_mock.side_effect = lambda s: captures.append(s) self.app.strategy_file_name = f"{self.mock_strategy_name}.yml" trade_fee = AddedToCostTradeFee(percent=Decimal("5")) order_id = PaperTradeExchange.random_order_id(order_side="BUY", trading_pair="BTC-USDT") with self.app.trade_fill_db.get_new_session() as session: o = Order( id=order_id, config_file_path=f"{self.mock_strategy_name}.yml", strategy=self.mock_strategy_name, market="binance", symbol="BTC-USDT", base_asset="BTC", quote_asset="USDT", creation_timestamp=0, order_type="LMT", amount=4, leverage=0, price=3, last_status="PENDING", last_update_timestamp=0, ) session.add(o) for i in [1, 2]: t = TradeFill( config_file_path=f"{self.mock_strategy_name}.yml", strategy=self.mock_strategy_name, market="binance", symbol="BTC-USDT", base_asset="BTC", quote_asset="USDT", timestamp=i, order_id=order_id, trade_type="BUY", order_type="LIMIT", price=i, amount=2, leverage=1, trade_fee=trade_fee.to_json(), exchange_trade_id=f"someExchangeId{i}", ) session.add(t) session.commit() self.app.list_trades(start_time=0) self.assertEqual(1, len(captures)) creation_time_str = str(datetime.datetime.fromtimestamp(0)) df_str_expected = ( f"\n Recent trades:" f"\n +---------------------+------------+----------+--------------+--------+---------+----------+------------+------------+-------+" # noqa: E501 f"\n | Timestamp | Exchange | Market | Order_type | Side | Price | Amount | Leverage | Position | Age |" # noqa: E501 f"\n |---------------------+------------+----------+--------------+--------+---------+----------+------------+------------+-------|" # noqa: E501 f"\n | {creation_time_str} | binance | BTC-USDT | limit | buy | 1 | 2 | 1 | NIL | n/a |" # noqa: E501 f"\n | {creation_time_str} | binance | BTC-USDT | limit | buy | 2 | 2 | 1 | NIL | n/a |" # noqa: E501 f"\n +---------------------+------------+----------+--------------+--------+---------+----------+------------+------------+-------+" # noqa: E501 ) self.assertEqual(df_str_expected, captures[0])