def setUp(self) -> None: super().setUp() self.exchange_task = None self.return_values_queue = asyncio.Queue() self.resume_test_event = asyncio.Event() self.log_records = [] self.exchange = DydxPerpetualDerivative( dydx_perpetual_api_key="someAPIKey", dydx_perpetual_api_secret="someAPISecret", dydx_perpetual_passphrase="somePassPhrase", dydx_perpetual_account_number=1234, dydx_perpetual_ethereum_address="someETHAddress", dydx_perpetual_stark_private_key="1234", trading_pairs=[self.trading_pair], ) self.exchange.logger().setLevel(1) self.exchange.logger().addHandler(self) self.ev_loop = asyncio.get_event_loop()
def test_tick_manual_poll_interval(self): poll_interval = 10 exchange = DydxPerpetualDerivative( dydx_perpetual_api_key="someAPIKey", dydx_perpetual_api_secret="someAPISecret", dydx_perpetual_passphrase="somePassPhrase", dydx_perpetual_account_number=1234, dydx_perpetual_ethereum_address="someETHAddress", dydx_perpetual_stark_private_key="1234", trading_pairs=[self.trading_pair], poll_interval=poll_interval, ) initial_timestamp = 0 exchange._last_timestamp = initial_timestamp exchange.tick(timestamp=initial_timestamp + poll_interval - 1) self.assertFalse(exchange._poll_notifier.is_set()) exchange.tick(timestamp=initial_timestamp + poll_interval) self.assertTrue(exchange._poll_notifier.is_set())
class DydxPerpetualDerivativeTest(unittest.TestCase): level = 0 @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.base_asset = "COINALPHA" cls.quote_asset = "USD" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" def setUp(self) -> None: super().setUp() self.exchange_task = None self.return_values_queue = asyncio.Queue() self.resume_test_event = asyncio.Event() self.log_records = [] self.exchange = DydxPerpetualDerivative( dydx_perpetual_api_key="someAPIKey", dydx_perpetual_api_secret="someAPISecret", dydx_perpetual_passphrase="somePassPhrase", dydx_perpetual_account_number=1234, dydx_perpetual_ethereum_address="someETHAddress", dydx_perpetual_stark_private_key="1234", trading_pairs=[self.trading_pair], ) self.exchange.logger().setLevel(1) self.exchange.logger().addHandler(self) self.ev_loop = asyncio.get_event_loop() def tearDown(self) -> None: self.exchange_task and self.exchange_task.cancel() super().tearDown() def handle(self, record): self.log_records.append(record) def check_is_logged(self, log_level: str, message: str) -> bool: is_logged = any( record.levelname == log_level and record.getMessage() == message for record in self.log_records) return is_logged def simulate_balances_initialized(self, account_balances: Optional[Dict] = None): if account_balances is None: account_balances = { self.quote_asset: Decimal("10"), self.base_asset: Decimal("20"), } self.exchange._account_balances = account_balances async def return_queued_values_and_unlock_with_event(self): val = await self.return_values_queue.get() self.resume_test_event.set() return val def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret def get_user_stream_account_ws_message_mock(self, size: float, status: str = "OPEN") -> Dict: account_message_mock = { "contents": self.get_account_rest_message_mock(size, status) } return account_message_mock def get_account_rest_message_mock(self, size: float, status: str = "OPEN") -> Dict: account_message_mock = { "account": { "equity": "1000", "freeCollateral": "10", "openPositions": { self.trading_pair: { "market": self.trading_pair, "entryPrice": "10", "size": str(size), "side": "LONG", "unrealizedPnl": "2", "status": status, } } } } return account_message_mock def get_markets_message_mock(self, index_price: float) -> Dict: markets_message_mock = { "markets": { self.trading_pair: { "indexPrice": str(index_price), "oraclePrice": "10.1", "nextFundingAt": str(datetime.now()), "nextFundingRate": "0.1", } } } return markets_message_mock def get_user_stream_positions_ws_message_mock(self, size: float, status: str = "OPEN" ) -> Dict: positions_message_mock = { "contents": self.get_positions_rest_message_mock(size, status) } return positions_message_mock def get_positions_rest_message_mock(self, size: float, status: str = "OPEN") -> Dict: positions_message_mock = { "positions": [{ "market": self.trading_pair, "side": "LONG", "unrealizedPnl": "2", "size": str(size), "status": status, }] } return positions_message_mock def test_user_stream_event_listener_creates_position_from_account_update( self): self.exchange_task = self.ev_loop.create_task( self.exchange._user_stream_event_listener()) dummy_user_stream = AsyncMock() dummy_user_stream.get.side_effect = self.return_queued_values_and_unlock_with_event position_size = 1 account_message_mock = self.get_user_stream_account_ws_message_mock( position_size) self.return_values_queue.put_nowait(account_message_mock) self.exchange._user_stream_tracker._user_stream = dummy_user_stream self.async_run_with_timeout(self.resume_test_event.wait()) self.resume_test_event.clear() self.assertEqual(1, len(self.exchange.account_positions)) position = self.exchange.get_position(self.trading_pair) self.assertEqual(position_size, position.amount) def test_user_stream_event_listener_updates_position_from_positions_update( self): self.exchange_task = self.ev_loop.create_task( self.exchange._user_stream_event_listener()) dummy_user_stream = AsyncMock() dummy_user_stream.get.side_effect = self.return_queued_values_and_unlock_with_event position_size = 1 account_message_mock = self.get_user_stream_positions_ws_message_mock( position_size, status="CLOSED") self.return_values_queue.put_nowait(account_message_mock) self.exchange._user_stream_tracker._user_stream = dummy_user_stream position = DydxPerpetualPosition( self.trading_pair, PositionSide.LONG, unrealized_pnl=Decimal("2"), entry_price=Decimal("1"), amount=Decimal(position_size) / 2, leverage=Decimal("10"), ) self.exchange._account_positions[self.trading_pair] = position self.async_run_with_timeout(self.resume_test_event.wait()) self.resume_test_event.clear() self.assertEqual(position_size, position.amount) # position was updated with message self.assertEqual(0, len( self.exchange.account_positions)) # closed position removed @patch( "hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_client_wrapper" ".DydxPerpetualClientWrapper.get_account") def test_update_account_positions_creates_position_from_account_update( self, get_account_mock: AsyncMock): self.simulate_balances_initialized() position_size = 1 account_message_mock = self.get_account_rest_message_mock( position_size) get_account_mock.return_value = account_message_mock self.async_run_with_timeout(self.exchange._update_account_positions()) self.assertEqual(1, len(self.exchange.account_positions)) position = self.exchange.get_position(self.trading_pair) self.assertEqual(position_size, position.amount) @patch( "hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_client_wrapper" ".DydxPerpetualClientWrapper.get_account") def test_update_account_positions_updates_position_from_account_update( self, get_account_mock: AsyncMock): self.simulate_balances_initialized() position_size = 1 account_message_mock = self.get_account_rest_message_mock( position_size, status="CLOSED") get_account_mock.return_value = account_message_mock position = DydxPerpetualPosition( self.trading_pair, PositionSide.LONG, unrealized_pnl=Decimal("2"), entry_price=Decimal("1"), amount=Decimal(position_size) / 2, leverage=Decimal("10"), ) self.exchange._account_positions[self.trading_pair] = position self.async_run_with_timeout(self.exchange._update_account_positions()) self.assertEqual(position_size, position.amount) # position was updated with message self.assertEqual(0, len( self.exchange.account_positions)) # closed position removed @patch( "hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_client_wrapper" ".DydxPerpetualClientWrapper.get_markets") def test_update_funding_rates_succeeds(self, get_markets_mock: AsyncMock): index_price = 10.0 markets_message_mock = self.get_markets_message_mock(index_price) get_markets_mock.return_value = markets_message_mock self.async_run_with_timeout(self.exchange._update_funding_rates()) funding_info = self.exchange.get_funding_info(self.trading_pair) self.assertIsInstance(funding_info, FundingInfo) self.assertEqual(Decimal(index_price), funding_info.index_price) @patch( "hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_client_wrapper" ".DydxPerpetualClientWrapper.get_markets") def test_update_funding_fails_on_rate_limit(self, get_markets_mock: AsyncMock): resp = Response() resp.status_code = 429 resp._content = b'{"errors": [{"msg": "Too many requests"}]}' get_markets_mock.return_value = DydxApiError(resp) self.async_run_with_timeout(self.exchange._update_funding_rates()) self.check_is_logged(log_level="NETWORK", message="Rate-limit error.") @patch( "hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_client_wrapper" ".DydxPerpetualClientWrapper.get_markets") def test_update_funding_fails_on_other_dydx_api_error( self, get_markets_mock: AsyncMock): resp = Response() resp.status_code = 430 resp._content = b'{"errors": [{"msg": "Some other dydx API error."}]}' get_markets_mock.return_value = DydxApiError(resp) self.async_run_with_timeout(self.exchange._update_funding_rates()) self.check_is_logged(log_level="NETWORK", message="dYdX API error.") @patch( "hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_client_wrapper" ".DydxPerpetualClientWrapper.get_markets") def test_update_funding_fails_on_general_exception( self, get_markets_mock: AsyncMock): get_markets_mock.return_value = Exception("Dummy exception") self.async_run_with_timeout(self.exchange._update_funding_rates()) self.check_is_logged(log_level="NETWORK", message="Unknown error.") def test_tick_manual_poll_interval(self): poll_interval = 10 exchange = DydxPerpetualDerivative( dydx_perpetual_api_key="someAPIKey", dydx_perpetual_api_secret="someAPISecret", dydx_perpetual_passphrase="somePassPhrase", dydx_perpetual_account_number=1234, dydx_perpetual_ethereum_address="someETHAddress", dydx_perpetual_stark_private_key="1234", trading_pairs=[self.trading_pair], poll_interval=poll_interval, ) initial_timestamp = 0 exchange._last_timestamp = initial_timestamp exchange.tick(timestamp=initial_timestamp + poll_interval - 1) self.assertFalse(exchange._poll_notifier.is_set()) exchange.tick(timestamp=initial_timestamp + poll_interval) self.assertTrue(exchange._poll_notifier.is_set()) @patch( "hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_derivative.time.time" ) def test_tick_short_interval(self, time_patch: MagicMock): initial_timestamp = 0 self.exchange._last_timestamp = initial_timestamp self.exchange._user_stream_tracker._last_recv_time = 0 time_patch.return_value = 61 self.exchange.tick(timestamp=initial_timestamp + self.exchange.SHORT_POLL_INTERVAL - 1) self.assertFalse(self.exchange._poll_notifier.is_set()) self.exchange.tick(timestamp=initial_timestamp + self.exchange.SHORT_POLL_INTERVAL) self.assertTrue(self.exchange._poll_notifier.is_set()) @patch( "hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_derivative.time.time" ) def test_tick_long_interval(self, time_patch: MagicMock): initial_time_stamp = 0 self.exchange._last_timestamp = initial_time_stamp self.exchange._user_stream_tracker._last_recv_time = 0 time_patch.return_value = 5 self.exchange.tick(timestamp=initial_time_stamp + self.exchange.LONG_POLL_INTERVAL - 1) self.assertFalse(self.exchange._poll_notifier.is_set()) self.exchange.tick(timestamp=initial_time_stamp + self.exchange.LONG_POLL_INTERVAL) self.assertTrue(self.exchange._poll_notifier.is_set())
class DydxPerpetualDerivativeTest(unittest.TestCase): # the level is required to receive logs from the data source logger level = 0 start_timestamp: float = pd.Timestamp("2021-01-01", tz="UTC").timestamp() @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.base_asset = "COINALPHA" cls.quote_asset = "USD" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" def setUp(self) -> None: super().setUp() self.exchange_task = None self.return_values_queue = asyncio.Queue() self.resume_test_event = asyncio.Event() self.log_records = [] self.exchange = DydxPerpetualDerivative( dydx_perpetual_api_key="someAPIKey", dydx_perpetual_api_secret="someAPISecret", dydx_perpetual_passphrase="somePassPhrase", dydx_perpetual_account_number=1234, dydx_perpetual_ethereum_address="someETHAddress", dydx_perpetual_stark_private_key="1234", trading_pairs=[self.trading_pair], ) self.exchange.logger().setLevel(1) self.exchange.logger().addHandler(self) self._initialize_event_loggers() self.ev_loop = asyncio.get_event_loop() def tearDown(self) -> None: self.exchange_task and self.exchange_task.cancel() super().tearDown() def _initialize_event_loggers(self): self.buy_order_completed_logger = EventLogger() self.buy_order_created_logger = EventLogger() self.order_cancelled_logger = EventLogger() self.order_failure_logger = EventLogger() self.order_filled_logger = EventLogger() self.sell_order_completed_logger = EventLogger() self.sell_order_created_logger = EventLogger() events_and_loggers = [ (MarketEvent.BuyOrderCompleted, self.buy_order_completed_logger), (MarketEvent.BuyOrderCreated, self.buy_order_created_logger), (MarketEvent.OrderCancelled, self.order_cancelled_logger), (MarketEvent.OrderFailure, self.order_failure_logger), (MarketEvent.OrderFilled, self.order_filled_logger), (MarketEvent.SellOrderCompleted, self.sell_order_completed_logger), (MarketEvent.SellOrderCreated, self.sell_order_created_logger)] for event, logger in events_and_loggers: self.exchange.add_listener(event, logger) def handle(self, record): self.log_records.append(record) def check_is_logged(self, log_level: str, message: str) -> bool: is_logged = any( record.levelname == log_level and record.getMessage() == message for record in self.log_records ) return is_logged def simulate_balances_initialized(self, account_balances: Optional[Dict] = None): if account_balances is None: account_balances = { self.quote_asset: Decimal("10"), self.base_asset: Decimal("20"), } self.exchange._account_balances = account_balances def _simulate_trading_rules_initialized(self): self.exchange._trading_rules = { self.trading_pair: TradingRule( trading_pair=self.trading_pair, min_order_size=Decimal("0.01"), min_price_increment=Decimal("0.0001"), min_base_amount_increment=Decimal("0.000001"), ) } def _simulate_reset_poll_notifier(self): self.exchange._poll_notifier.clear() def _simulate_ws_message_received(self, timestamp: float): self.exchange._user_stream_tracker._data_source._ws_assistant._connection._last_recv_time = timestamp async def return_queued_values_and_unlock_with_event(self): val = await self.return_values_queue.get() self.resume_test_event.set() return val def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete(asyncio.wait_for(coroutine, timeout)) return ret def get_user_stream_account_ws_message_mock(self, size: float, status: str = "OPEN") -> Dict: account_message_mock = { "contents": self.get_account_rest_message_mock(size, status) } return account_message_mock def get_account_rest_message_mock(self, size: float, status: str = "OPEN") -> Dict: account_message_mock = { "account": { "equity": "1000", "freeCollateral": "10", "openPositions": { self.trading_pair: { "market": self.trading_pair, "entryPrice": "10", "size": str(size), "side": "LONG", "unrealizedPnl": "2", "status": status, } } } } return account_message_mock def get_markets_message_mock( self, index_price: float = 1, min_order_size: float = 2, min_price_increment: float = 3, min_base_amount_increment: float = 4, ) -> Dict: markets_message_mock = { # irrelevant fields removed "markets": { self.trading_pair: { "quoteAsset": self.quote_asset, "minOrderSize": str(min_order_size), "tickSize": str(min_price_increment), "stepSize": str(min_base_amount_increment), "indexPrice": str(index_price), "oraclePrice": "10.1", "nextFundingAt": str(datetime.now()), "nextFundingRate": "0.1", "initialMarginFraction": "0.1", "maintenanceMarginFraction": "0.2", } } } return markets_message_mock def get_user_stream_positions_ws_message_mock(self, size: float, status: str = "OPEN") -> Dict: positions_message_mock = { "contents": self.get_positions_rest_message_mock(size, status) } return positions_message_mock def get_positions_rest_message_mock(self, size: float, status: str = "OPEN") -> Dict: positions_message_mock = { "positions": [ { "market": self.trading_pair, "side": "LONG", "unrealizedPnl": "2", "size": str(size), "status": status, } ] } return positions_message_mock def test_user_stream_event_listener_creates_position_from_account_update(self): self.exchange_task = self.ev_loop.create_task(self.exchange._user_stream_event_listener()) dummy_user_stream = AsyncMock() dummy_user_stream.get.side_effect = self.return_queued_values_and_unlock_with_event position_size = 1 account_message_mock = self.get_user_stream_account_ws_message_mock(position_size) self.return_values_queue.put_nowait(account_message_mock) self.exchange._user_stream_tracker._user_stream = dummy_user_stream self.async_run_with_timeout(self.resume_test_event.wait()) self.resume_test_event.clear() self.assertEqual(1, len(self.exchange.account_positions)) position = self.exchange.get_position(self.trading_pair) self.assertEqual(position_size, position.amount) def test_user_stream_event_listener_updates_position_from_positions_update(self): self.exchange_task = self.ev_loop.create_task(self.exchange._user_stream_event_listener()) dummy_user_stream = AsyncMock() dummy_user_stream.get.side_effect = self.return_queued_values_and_unlock_with_event position_size = 1 account_message_mock = self.get_user_stream_positions_ws_message_mock(position_size, status="CLOSED") self.return_values_queue.put_nowait(account_message_mock) self.exchange._user_stream_tracker._user_stream = dummy_user_stream position = DydxPerpetualPosition( self.trading_pair, PositionSide.LONG, unrealized_pnl=Decimal("2"), entry_price=Decimal("1"), amount=Decimal(position_size) / 2, leverage=Decimal("10"), ) self.exchange._account_positions[self.trading_pair] = position self.async_run_with_timeout(self.resume_test_event.wait()) self.resume_test_event.clear() self.assertEqual(position_size, position.amount) # position was updated with message self.assertEqual(0, len(self.exchange.account_positions)) # closed position removed @patch("hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_client_wrapper" ".DydxPerpetualClientWrapper.get_account") def test_update_account_positions_creates_position_from_account_update(self, get_account_mock: AsyncMock): self.simulate_balances_initialized() position_size = 1 account_message_mock = self.get_account_rest_message_mock(position_size) get_account_mock.return_value = account_message_mock self.async_run_with_timeout(self.exchange._update_account_positions()) self.assertEqual(1, len(self.exchange.account_positions)) position = self.exchange.get_position(self.trading_pair) self.assertEqual(position_size, position.amount) @patch("hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_client_wrapper" ".DydxPerpetualClientWrapper.get_account") def test_update_account_positions_updates_position_from_account_update(self, get_account_mock: AsyncMock): self.simulate_balances_initialized() position_size = 1 account_message_mock = self.get_account_rest_message_mock(position_size, status="CLOSED") get_account_mock.return_value = account_message_mock position = DydxPerpetualPosition( self.trading_pair, PositionSide.LONG, unrealized_pnl=Decimal("2"), entry_price=Decimal("1"), amount=Decimal(position_size) / 2, leverage=Decimal("10"), ) self.exchange._account_positions[self.trading_pair] = position self.async_run_with_timeout(self.exchange._update_account_positions()) self.assertEqual(position_size, position.amount) # position was updated with message self.assertEqual(0, len(self.exchange.account_positions)) # closed position removed @patch("hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_client_wrapper" ".DydxPerpetualClientWrapper.get_markets") def test_update_funding_rates_succeeds(self, get_markets_mock: AsyncMock): index_price = 10.0 markets_message_mock = self.get_markets_message_mock(index_price) get_markets_mock.return_value = markets_message_mock self.async_run_with_timeout(self.exchange._update_funding_rates()) funding_info = self.exchange.get_funding_info(self.trading_pair) self.assertIsInstance(funding_info, FundingInfo) self.assertEqual(Decimal(index_price), funding_info.index_price) @patch("hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_client_wrapper" ".DydxPerpetualClientWrapper.get_markets") def test_update_funding_fails_on_rate_limit(self, get_markets_mock: AsyncMock): resp = Response() resp.status_code = 429 resp._content = b'{"errors": [{"msg": "Too many requests"}]}' get_markets_mock.return_value = DydxApiError(resp) self.async_run_with_timeout(self.exchange._update_funding_rates()) self.check_is_logged(log_level="NETWORK", message="Rate-limit error.") @patch("hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_client_wrapper" ".DydxPerpetualClientWrapper.get_markets") def test_update_funding_fails_on_other_dydx_api_error(self, get_markets_mock: AsyncMock): resp = Response() resp.status_code = 430 resp._content = b'{"errors": [{"msg": "Some other dydx API error."}]}' get_markets_mock.return_value = DydxApiError(resp) self.async_run_with_timeout(self.exchange._update_funding_rates()) self.check_is_logged(log_level="NETWORK", message="dYdX API error.") @patch("hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_client_wrapper" ".DydxPerpetualClientWrapper.get_markets") def test_update_funding_fails_on_general_exception(self, get_markets_mock: AsyncMock): get_markets_mock.return_value = Exception("Dummy exception") self.async_run_with_timeout(self.exchange._update_funding_rates()) self.check_is_logged(log_level="NETWORK", message="Unknown error.") def test_tick_initial_tick_successful(self): start_ts: float = time.time() * 1e3 self.exchange.tick(start_ts) self.assertEqual(start_ts, self.exchange._last_poll_timestamp) self.assertTrue(self.exchange._poll_notifier.is_set()) @patch("hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_derivative.DydxPerpetualDerivative.time_now_s") @patch("hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_user_stream_data_source.DydxPerpetualUserStreamDataSource.last_recv_time", new_callable=PropertyMock) def test_tick_subsequent_tick_within_short_poll_interval(self, mock_last_recv_time, mock_ts): # Assumes user stream tracker has NOT been receiving messages, Hence SHORT_POLL_INTERVAL in use start_ts: float = self.start_timestamp next_tick: float = start_ts + (self.exchange.SHORT_POLL_INTERVAL - 1) mock_ts.return_value = start_ts mock_last_recv_time.return_value = -1 self.exchange.tick(start_ts) self.assertEqual(start_ts, self.exchange._last_poll_timestamp) self.assertTrue(self.exchange._poll_notifier.is_set()) self._simulate_reset_poll_notifier() # Simulate last message received 1 sec ago mock_last_recv_time.return_value = next_tick - 1 mock_ts.return_value = next_tick self.exchange.tick(next_tick) self.assertEqual(next_tick, self.exchange._last_poll_timestamp) self.assertFalse(self.exchange._poll_notifier.is_set()) @patch("hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_derivative.DydxPerpetualDerivative.time_now_s") @patch("hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_user_stream_data_source.DydxPerpetualUserStreamDataSource.last_recv_time", new_callable=PropertyMock) def test_tick_subsequent_tick_exceed_short_poll_interval(self, mock_last_recv_time, mock_ts): # Assumes user stream tracker has NOT been receiving messages, Hence SHORT_POLL_INTERVAL in use start_ts: float = self.start_timestamp next_tick: float = start_ts + (self.exchange.SHORT_POLL_INTERVAL + 1) mock_ts.return_value = start_ts mock_last_recv_time.return_value = -1 self.exchange.tick(start_ts) self.assertEqual(start_ts, self.exchange._last_poll_timestamp) self.assertTrue(self.exchange._poll_notifier.is_set()) self._simulate_reset_poll_notifier() mock_ts.return_value = next_tick self.exchange.tick(next_tick) self.assertEqual(next_tick, self.exchange._last_poll_timestamp) self.assertTrue(self.exchange._poll_notifier.is_set()) @patch("hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_derivative.DydxPerpetualDerivative.time_now_s") @patch("hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_user_stream_data_source.DydxPerpetualUserStreamDataSource.last_recv_time", new_callable=PropertyMock) def test_tick_subsequent_tick_within_long_poll_interval(self, mock_last_recv_time, mock_time): start_ts: float = self.start_timestamp next_tick: float = start_ts + (self.exchange.LONG_POLL_INTERVAL - 1) mock_time.return_value = start_ts mock_last_recv_time.return_value = -1 self.exchange.tick(start_ts) self.assertEqual(start_ts, self.exchange._last_poll_timestamp) self.assertTrue(self.exchange._poll_notifier.is_set()) # Simulate last message received 1 sec ago mock_last_recv_time.return_value = next_tick - 1 self._simulate_reset_poll_notifier() mock_time.return_value = next_tick self.exchange.tick(next_tick) self.assertEqual(next_tick, self.exchange._last_poll_timestamp) self.assertFalse(self.exchange._poll_notifier.is_set()) @patch("hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_derivative.DydxPerpetualDerivative.time_now_s") @patch("hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_user_stream_data_source.DydxPerpetualUserStreamDataSource.last_recv_time", new_callable=PropertyMock) def test_tick_subsequent_tick_exceed_long_poll_interval(self, mock_last_recv_time, mock_time): # Assumes user stream tracker has been receiving messages, Hence LONG_POLL_INTERVAL in use start_ts: float = self.start_timestamp next_tick: float = start_ts + (self.exchange.LONG_POLL_INTERVAL - 1) mock_last_recv_time.return_value = -1 mock_time.return_value = start_ts self.exchange.tick(start_ts) self.assertEqual(start_ts, self.exchange._last_poll_timestamp) self.assertTrue(self.exchange._poll_notifier.is_set()) mock_last_recv_time.return_value = start_ts self._simulate_reset_poll_notifier() mock_time.return_value = next_tick self.exchange.tick(next_tick) self.assertEqual(next_tick, self.exchange._last_poll_timestamp) self.assertTrue(self.exchange._poll_notifier.is_set()) @patch("hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_client_wrapper" ".DydxPerpetualClientWrapper.get_markets") def test_update_trading_rules(self, get_markets_mock: AsyncMock): min_order_size = 1 min_price_increment = 2 min_base_amount_increment = 3 min_notional_size = min_order_size * min_price_increment markets_message_mock = self.get_markets_message_mock( min_order_size=min_order_size, min_price_increment=min_price_increment, min_base_amount_increment=min_base_amount_increment, ) get_markets_mock.return_value = markets_message_mock self.async_run_with_timeout(self.exchange._update_trading_rules()) trading_rule: TradingRule = self.exchange._trading_rules[self.trading_pair] self.assertEqual(min_order_size, trading_rule.min_order_size) self.assertEqual(min_price_increment, trading_rule.min_price_increment) self.assertEqual(min_base_amount_increment, trading_rule.min_base_amount_increment) self.assertEqual(min_notional_size, trading_rule.min_notional_size) self.assertEqual(self.quote_asset, trading_rule.buy_order_collateral_token) self.assertEqual(self.quote_asset, trading_rule.sell_order_collateral_token) @patch("hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_client_wrapper" ".DydxPerpetualClientWrapper.get_markets") def test_get_buy_and_sell_collateral_token(self, get_markets_mock: AsyncMock): markets_message_mock = self.get_markets_message_mock() get_markets_mock.return_value = markets_message_mock self.async_run_with_timeout(self.exchange._update_trading_rules()) buy_collateral_token = self.exchange.get_buy_collateral_token(self.trading_pair) sell_collateral_token = self.exchange.get_sell_collateral_token(self.trading_pair) self.assertEqual(self.quote_asset, buy_collateral_token) self.assertEqual(self.quote_asset, sell_collateral_token) @patch("hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_derivative.DydxPerpetualDerivative" ".time_now_s") @patch("hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_client_wrapper" ".DydxPerpetualClientWrapper.place_order") def test_create_buy_order(self, place_order_mock, time_mock): self._simulate_trading_rules_initialized() time_mock.return_value = 1640001112.223 place_order_mock.return_value = { # irrelevant fields removed "order": { "id": "EOID1", "status": "OPEN", } } self.async_run_with_timeout(self.exchange.execute_buy( order_id="OID1", trading_pair=self.trading_pair, amount=Decimal(1), order_type=OrderType.LIMIT, position_action=PositionAction.OPEN, price=Decimal(1000) )) self.assertIn("OID1", self.exchange.in_flight_orders) self.assertTrue(self.check_is_logged( "INFO", f"Created LIMIT BUY order OID1 for 1.000000 {self.trading_pair}." )) order = self.exchange.in_flight_orders["OID1"] self.assertEqual(time_mock.return_value, order.creation_timestamp) self.assertTrue(1, len(self.buy_order_created_logger.event_log)) buy_event: BuyOrderCreatedEvent = self.buy_order_created_logger.event_log[0] self.assertEqual("OID1", buy_event.order_id) self.assertEqual(1640001112.223, buy_event.creation_timestamp) @patch("hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_derivative.DydxPerpetualDerivative" ".time_now_s") @patch("hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_client_wrapper" ".DydxPerpetualClientWrapper.place_order") def test_create_sell_order(self, place_order_mock, time_mock): self._simulate_trading_rules_initialized() time_mock.return_value = 1640001112.223 place_order_mock.return_value = { # irrelevant fields removed "order": { "id": "EOID1", "status": "OPEN", } } self.async_run_with_timeout(self.exchange.execute_sell( order_id="OID1", trading_pair=self.trading_pair, amount=Decimal(1), order_type=OrderType.LIMIT, position_action=PositionAction.OPEN, price=Decimal(1000) )) self.assertIn("OID1", self.exchange.in_flight_orders) self.assertTrue(self.check_is_logged( "INFO", f"Created LIMIT SELL order OID1 for 1.000000 {self.trading_pair}." )) order = self.exchange.in_flight_orders["OID1"] self.assertEqual(time_mock.return_value, order.creation_timestamp) self.assertTrue(1, len(self.sell_order_created_logger.event_log)) sell_event: SellOrderCreatedEvent = self.sell_order_created_logger.event_log[0] self.assertEqual("OID1", sell_event.order_id) self.assertEqual(1640001112.223, sell_event.creation_timestamp) def test_cancel_order_no_in_flight_order(self): self._simulate_trading_rules_initialized() self.async_run_with_timeout(self.exchange.cancel_order( client_order_id="OID1" )) self.check_is_logged(log_level="WARNING", message="Canceled an untracked order OID1") @patch("hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_derivative.DydxPerpetualDerivative" ".time_now_s") def test_cancel_order_no_exchange_order_id(self, time_mock): self._simulate_trading_rules_initialized() time_mock.return_value = 1640001112.223 self.exchange.start_tracking_order( order_side=TradeType.BUY, client_order_id="OID1", order_type=OrderType.LIMIT, created_at=1630001112.223, hash="hashcode", trading_pair=self.trading_pair, price=Decimal(1000), amount=Decimal(1), leverage=Decimal(1), position="position", ) self.assertTrue("OID1" in self.exchange.in_flight_orders) self.async_run_with_timeout(self.exchange.cancel_order( client_order_id="OID1" )) self.assertFalse("OID1" in self.exchange.in_flight_orders) @patch("hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_derivative.DydxPerpetualDerivative" ".time_now_s") def test_cancel_order_already_canceled(self, time_mock): self._simulate_trading_rules_initialized() resp = Response() resp.status_code = 429 resp._content = b'{"errors": [{"msg": "Order with specified id: EOID1 is already canceled"}]}' time_mock.return_value = 1640001112.223 time_mock.side_effect = DydxApiError(resp) self.exchange.start_tracking_order( order_side=TradeType.BUY, client_order_id="OID1", order_type=OrderType.LIMIT, created_at=1640001112.223, hash="hashcode", trading_pair=self.trading_pair, price=Decimal(1000), amount=Decimal(1), leverage=Decimal(1), position="position", ) self.assertTrue("OID1" in self.exchange.in_flight_orders) self.async_run_with_timeout(self.exchange.cancel_order( client_order_id="OID1" )) self.assertFalse("OID1" in self.exchange.in_flight_orders) def test_cancel_all(self): self._simulate_trading_rules_initialized() self.exchange.start_tracking_order( order_side=TradeType.BUY, client_order_id="OID1", order_type=OrderType.LIMIT, created_at=1640001112.223, hash="hashcode", trading_pair=self.trading_pair, price=Decimal(1000), amount=Decimal(1), leverage=Decimal(1), position="position", ) result = self.async_run_with_timeout(self.exchange.cancel_all( timeout_seconds=1 )) self.assertEqual(result, [CancellationResult(order_id='OID1', success=True)])