From 2cf0e5d49d73ddfe8c3606cc87fabc5f2f6b2972 Mon Sep 17 00:00:00 2001 From: Andy Rechenberg Date: Wed, 1 Jan 2025 19:37:57 -0500 Subject: [PATCH 1/6] Try testing for order errors. --- .../services/order_service.py | 77 +++++++++++++++++-- 1 file changed, 69 insertions(+), 8 deletions(-) diff --git a/coinbase_advanced_trader/services/order_service.py b/coinbase_advanced_trader/services/order_service.py index 7a6f5ed..f79f148 100644 --- a/coinbase_advanced_trader/services/order_service.py +++ b/coinbase_advanced_trader/services/order_service.py @@ -207,17 +207,78 @@ def _place_limit_order(self, product_id: str, fiat_amount: str, limit_price: Opt str(base_size), str(adjusted_price) ) + + if order_response["success"]: + """ + The order was successful. Process, log, and return. + """ + order = self._build_order( + order_response['success_response']['order_id'], + product_id, + side, + OrderType.LIMIT, + base_size, + adjusted_price + ) + self._log_order_result(order_response, product_id, base_size, adjusted_price, side) + return order + else: + """ + For some reason, the order placement failed. Log and return "something(?)" + """ + order_error = order_response['error_response']["error"] + logger.error(f"Order placed resulted in {order_error}") + + """ + Build an Order object with the error_response.error as the the + order_id. This convention allows to return an order and be able + to handle issues with order placement. + """ + + error_order_id = 'ORDER_ERROR_'+order_error + + error_order = self._build_order( + error_order_id, + product_id, + side, + OrderType.LIMIT, + base_size, + adjusted_price, + 'error' + ) + + match order_error: + case 'INSUFFICIENT_FUND': + logger.debug("Do NSF stuff here.") + return error_order + + case 'INVALID_LIMIT_PRICE_POST_ONLY': + logger.debug("Do INVALID_LIMIT_PRICE_POST_ONLY stuff here") + return error_order + + case 'INVALID_PRICE_PRECISION': + logger.debug("Do INVALID_PRICE_PRECISION stuff here.") + return error_order + + case _: + logger.error("An unprocessed order error occurred.") + return error_order + + def _build_order(id, product_id, side, type, size, price, status = 'pending') -> Order: + """ + Helper function to build an Order object. Include 'status' parameter + to allow for error orders. + """ order = Order( - id=order_response['success_response']['order_id'], - product_id=product_id, - side=side, - type=OrderType.LIMIT, - size=base_size, - price=adjusted_price + id=id, + product_id=product_id, + side=side, + type=type, + size=size, + price=price, + status=status ) - - self._log_order_result(order_response, product_id, base_size, adjusted_price, side) return order def _log_order_result(self, order: Dict[str, Any], product_id: str, amount: Any, price: Any = None, side: OrderSide = None) -> None: From d7aeb8ed6fd01cb78c4cce540f2c955595f5e36e Mon Sep 17 00:00:00 2001 From: Andy Rechenberg Date: Wed, 1 Jan 2025 20:30:40 -0500 Subject: [PATCH 2/6] Change the logging to info. --- coinbase_advanced_trader/services/order_service.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/coinbase_advanced_trader/services/order_service.py b/coinbase_advanced_trader/services/order_service.py index f79f148..741e511 100644 --- a/coinbase_advanced_trader/services/order_service.py +++ b/coinbase_advanced_trader/services/order_service.py @@ -221,6 +221,7 @@ def _place_limit_order(self, product_id: str, fiat_amount: str, limit_price: Opt adjusted_price ) self._log_order_result(order_response, product_id, base_size, adjusted_price, side) + logger.info(f"Order: {order}") return order else: @@ -228,7 +229,7 @@ def _place_limit_order(self, product_id: str, fiat_amount: str, limit_price: Opt For some reason, the order placement failed. Log and return "something(?)" """ order_error = order_response['error_response']["error"] - logger.error(f"Order placed resulted in {order_error}") + logger.info(f"Order placed resulted in {order_error}") """ Build an Order object with the error_response.error as the the @@ -250,15 +251,15 @@ def _place_limit_order(self, product_id: str, fiat_amount: str, limit_price: Opt match order_error: case 'INSUFFICIENT_FUND': - logger.debug("Do NSF stuff here.") + logger.info("Do NSF stuff here.") return error_order case 'INVALID_LIMIT_PRICE_POST_ONLY': - logger.debug("Do INVALID_LIMIT_PRICE_POST_ONLY stuff here") + logger.info("Do INVALID_LIMIT_PRICE_POST_ONLY stuff here") return error_order case 'INVALID_PRICE_PRECISION': - logger.debug("Do INVALID_PRICE_PRECISION stuff here.") + logger.info("Do INVALID_PRICE_PRECISION stuff here.") return error_order case _: @@ -313,4 +314,4 @@ def _log_order_result(self, order: Dict[str, Any], product_id: str, amount: Any, f"Reason: {failure_reason}. " f"Preview failure reason: {preview_failure_reason}") - logger.debug(f"Coinbase response: {order}") \ No newline at end of file + logger.info(f"Coinbase response: {order}") \ No newline at end of file From 8d9add359d768699e6f918db49e843dc13b9cac0 Mon Sep 17 00:00:00 2001 From: Andy Rechenberg Date: Wed, 1 Jan 2025 21:08:25 -0500 Subject: [PATCH 3/6] Make check look like the check in fiat_market_sell. --- .../services/order_service.py | 105 ++++++------------ 1 file changed, 31 insertions(+), 74 deletions(-) diff --git a/coinbase_advanced_trader/services/order_service.py b/coinbase_advanced_trader/services/order_service.py index 741e511..e9d0112 100644 --- a/coinbase_advanced_trader/services/order_service.py +++ b/coinbase_advanced_trader/services/order_service.py @@ -201,86 +201,43 @@ def _place_limit_order(self, product_id: str, fiat_amount: str, limit_price: Opt if side == OrderSide.BUY else self.rest_client.limit_order_gtc_sell) - order_response = order_func( - self._generate_client_order_id(), - product_id, - str(base_size), - str(adjusted_price) - ) - - if order_response["success"]: - """ - The order was successful. Process, log, and return. - """ - order = self._build_order( - order_response['success_response']['order_id'], + try: + order_response = order_func( + self._generate_client_order_id(), product_id, - side, - OrderType.LIMIT, - base_size, - adjusted_price - ) - self._log_order_result(order_response, product_id, base_size, adjusted_price, side) - logger.info(f"Order: {order}") - return order - - else: - """ - For some reason, the order placement failed. Log and return "something(?)" - """ - order_error = order_response['error_response']["error"] - logger.info(f"Order placed resulted in {order_error}") - - """ - Build an Order object with the error_response.error as the the - order_id. This convention allows to return an order and be able - to handle issues with order placement. - """ - - error_order_id = 'ORDER_ERROR_'+order_error - - error_order = self._build_order( - error_order_id, - product_id, - side, - OrderType.LIMIT, - base_size, - adjusted_price, - 'error' + str(base_size), + str(adjusted_price) ) - match order_error: - case 'INSUFFICIENT_FUND': - logger.info("Do NSF stuff here.") - return error_order - - case 'INVALID_LIMIT_PRICE_POST_ONLY': - logger.info("Do INVALID_LIMIT_PRICE_POST_ONLY stuff here") - return error_order - - case 'INVALID_PRICE_PRECISION': - logger.info("Do INVALID_PRICE_PRECISION stuff here.") - return error_order + if not order_response['success']: + error_response = order_response.get('error_response', {}) + error_message = error_response.get('message', 'Unknown error') + preview_failure_reason = error_response.get('preview_failure_reason', 'Unknown') + error_log = (f"Failed to place a limit order. " + f"Reason: {error_message}. " + f"Preview failure reason: {preview_failure_reason}") + logger.error(error_log) + raise Exception(error_log) - case _: - logger.error("An unprocessed order error occurred.") - return error_order - - def _build_order(id, product_id, side, type, size, price, status = 'pending') -> Order: - """ - Helper function to build an Order object. Include 'status' parameter - to allow for error orders. - """ - order = Order( - id=id, + order = Order( + id=order_response['success_response']['order_id'], product_id=product_id, side=side, - type=type, - size=size, - price=price, - status=status - ) - return order + type=OrderType.LIMIT, + size=base_size, + price=adjusted_price + ) + self._log_order_result(order_response, product_id, base_size, adjusted_price, side) + return order + + except Exception as e: + error_message = str(e) + if "Invalid product_id" in error_message: + error_log = (f"Failed to place a limit order. " + f"Reason: {error_message}. " + f"Preview failure reason: Unknown") + logger.error(error_log) + raise def _log_order_result(self, order: Dict[str, Any], product_id: str, amount: Any, price: Any = None, side: OrderSide = None) -> None: """ From e89273049b24aee4e866aa9b0d62c23920ea9c1f Mon Sep 17 00:00:00 2001 From: Andy Rechenberg Date: Wed, 1 Jan 2025 21:11:16 -0500 Subject: [PATCH 4/6] Revert to debug. --- coinbase_advanced_trader/services/order_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coinbase_advanced_trader/services/order_service.py b/coinbase_advanced_trader/services/order_service.py index e9d0112..cd3fb1e 100644 --- a/coinbase_advanced_trader/services/order_service.py +++ b/coinbase_advanced_trader/services/order_service.py @@ -271,4 +271,4 @@ def _log_order_result(self, order: Dict[str, Any], product_id: str, amount: Any, f"Reason: {failure_reason}. " f"Preview failure reason: {preview_failure_reason}") - logger.info(f"Coinbase response: {order}") \ No newline at end of file + logger.debug(f"Coinbase response: {order}") \ No newline at end of file From cc7a4135a617d76930c4a8b77d10836df573ec41 Mon Sep 17 00:00:00 2001 From: Andy Rechenberg Date: Wed, 1 Jan 2025 21:35:30 -0500 Subject: [PATCH 5/6] Add a test for order_service fiat_limit_buy error. --- .../tests/test_order_service.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/coinbase_advanced_trader/tests/test_order_service.py b/coinbase_advanced_trader/tests/test_order_service.py index dd37f56..59b5d68 100644 --- a/coinbase_advanced_trader/tests/test_order_service.py +++ b/coinbase_advanced_trader/tests/test_order_service.py @@ -110,6 +110,40 @@ def test_fiat_limit_buy(self): self.assertEqual(order.size, expected_size) self.assertEqual(order.price, adjusted_price) + def test_fiat_limit_buy_error(self): + """Test the fiat_limit_buy with a response that raises an exception.""" + product_id = "BTC-USDC" + fiat_amount = "10" + + # Mock responses with correct format + mock_order_response = { + 'success': False, + 'error_response': { + 'error': 'INSUFFICIENT_FUND', + 'message': 'Insufficient balance in source account', + 'error_details': '', + 'preview_failure_reason': 'PREVIEW_INSUFFICIENT_FUND' + } + } + self.rest_client_mock.limit_order_gtc_buy.return_value = mock_order_response + + # Mock price service to return specific values + spot_price = Decimal('50000') + base_increment = Decimal('0.00000001') + quote_increment = Decimal('0.01') + price_multiplier = Decimal('0.9995') + + self.order_service.price_service.get_spot_price.return_value = spot_price + self.order_service.price_service.get_product_details.return_value = { + 'base_increment': str(base_increment), + 'quote_increment': str(quote_increment) + } + + with self.assertRaises(Exception) as context: + order = self.order_service.fiat_limit_buy(product_id, fiat_amount) + + self.assertEqual(str(context.exception), "Failed to place a limit order. Reason: Insufficient balance in source account. Preview failure reason: PREVIEW_INSUFFICIENT_FUND") + def test_fiat_limit_sell(self): """Test the fiat_limit_sell method.""" product_id = "BTC-USDC" From 838a51f4a716a49d291ae040ea87e6f1fc52d47b Mon Sep 17 00:00:00 2001 From: Andy Rechenberg Date: Thu, 2 Jan 2025 08:42:57 -0500 Subject: [PATCH 6/6] 'CreateOrderResponse' has no attribute 'get' - convert to dict. --- coinbase_advanced_trader/services/order_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coinbase_advanced_trader/services/order_service.py b/coinbase_advanced_trader/services/order_service.py index cd3fb1e..e3f5b5e 100644 --- a/coinbase_advanced_trader/services/order_service.py +++ b/coinbase_advanced_trader/services/order_service.py @@ -210,7 +210,7 @@ def _place_limit_order(self, product_id: str, fiat_amount: str, limit_price: Opt ) if not order_response['success']: - error_response = order_response.get('error_response', {}) + error_response = order_response.to_dict().get('error_response', {}) error_message = error_response.get('message', 'Unknown error') preview_failure_reason = error_response.get('preview_failure_reason', 'Unknown') error_log = (f"Failed to place a limit order. "