Skip to content

Commit 5e3e4a2

Browse files
authored
Follow Up: Supporting Edit Order endpoint (#55)
* supporting edit order endpoint * supporting edit_order_preview endpoint
1 parent dbc3491 commit 5e3e4a2

7 files changed

Lines changed: 190 additions & 22 deletions

File tree

coinbaseadvanced/client.py

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from coinbaseadvanced.models.products import BidAsksPage, ProductBook, ProductsPage, Product, \
2222
CandlesPage, TradesPage, ProductType, Granularity, GRANULARITY_MAP_IN_MINUTES
2323
from coinbaseadvanced.models.accounts import AccountsPage, Account
24-
from coinbaseadvanced.models.orders import OrderPlacementSource, OrdersPage, Order, OrderEdit, \
24+
from coinbaseadvanced.models.orders import OrderEditPreview, OrderPlacementSource, OrdersPage, Order, OrderEdit, \
2525
OrderBatchCancellation, FillsPage, Side, StopDirection, OrderType
2626

2727

@@ -161,7 +161,8 @@ def get_account(self, account_id: str) -> Account:
161161
def create_buy_market_order(self,
162162
client_order_id: str,
163163
product_id: str,
164-
quote_size: float) -> Order:
164+
quote_size: float,
165+
retail_portfolio_id: Optional[str] = None) -> Order:
165166
"""
166167
https://docs.cloud.coinbase.com/advanced-trade-api/reference/retailbrokerageapi_postorder
167168
@@ -179,12 +180,13 @@ def create_buy_market_order(self,
179180
}
180181
}
181182

182-
return self.create_order(client_order_id, product_id, Side.BUY, order_configuration)
183+
return self.create_order(client_order_id, product_id, Side.BUY, order_configuration, retail_portfolio_id)
183184

184185
def create_sell_market_order(self,
185186
client_order_id: str,
186187
product_id: str,
187-
base_size: float) -> Order:
188+
base_size: float,
189+
retail_portfolio_id: Optional[str] = None) -> Order:
188190
"""
189191
https://docs.cloud.coinbase.com/advanced-trade-api/reference/retailbrokerageapi_postorder
190192
@@ -202,7 +204,7 @@ def create_sell_market_order(self,
202204
}
203205
}
204206

205-
return self.create_order(client_order_id, product_id, Side.SELL, order_configuration)
207+
return self.create_order(client_order_id, product_id, Side.SELL, order_configuration, retail_portfolio_id)
206208

207209
def create_limit_order(
208210
self,
@@ -212,7 +214,8 @@ def create_limit_order(
212214
limit_price: float,
213215
base_size: float,
214216
cancel_time: Optional[datetime] = None,
215-
post_only: Optional[bool] = None) -> Order:
217+
post_only: Optional[bool] = None,
218+
retail_portfolio_id: Optional[str] = None) -> Order:
216219
"""
217220
https://docs.cloud.coinbase.com/advanced-trade-api/reference/retailbrokerageapi_postorder
218221
@@ -245,7 +248,7 @@ def create_limit_order(
245248
else:
246249
order_configuration['limit_limit_gtc'] = limit_order_configuration
247250

248-
return self.create_order(client_order_id, product_id, side, order_configuration)
251+
return self.create_order(client_order_id, product_id, side, order_configuration, retail_portfolio_id)
249252

250253
def create_stop_limit_order(
251254
self,
@@ -256,7 +259,8 @@ def create_stop_limit_order(
256259
stop_direction: StopDirection,
257260
limit_price: float,
258261
base_size: float,
259-
cancel_time: Optional[datetime] = None) -> Order:
262+
cancel_time: Optional[datetime] = None,
263+
retail_portfolio_id: Optional[str] = None) -> Order:
260264
"""
261265
https://docs.cloud.coinbase.com/advanced-trade-api/reference/retailbrokerageapi_postorder
262266
@@ -294,12 +298,13 @@ def create_stop_limit_order(
294298
else:
295299
order_configuration['stop_limit_stop_limit_gtc'] = stop_limit_order_configuration
296300

297-
return self.create_order(client_order_id, product_id, side, order_configuration)
301+
return self.create_order(client_order_id, product_id, side, order_configuration, retail_portfolio_id)
298302

299303
def create_order(self, client_order_id: str,
300304
product_id: str,
301305
side: Side,
302-
order_configuration: dict) -> Order:
306+
order_configuration: dict,
307+
retail_portfolio_id: Optional[str] = None) -> Order:
303308
"""
304309
https://docs.cloud.coinbase.com/advanced-trade-api/reference/retailbrokerageapi_postorder
305310
@@ -318,8 +323,10 @@ def create_order(self, client_order_id: str,
318323
'client_order_id': client_order_id,
319324
'product_id': product_id,
320325
'side': side.value,
321-
'order_configuration': order_configuration
326+
'order_configuration': order_configuration,
322327
}
328+
if retail_portfolio_id is not None:
329+
payload['retail_portfolio_id'] = retail_portfolio_id
323330

324331
headers = self._build_request_headers(method, request_path, json.dumps(payload)) \
325332
if self._is_legacy_auth() \
@@ -343,7 +350,39 @@ def edit_order(self, order_id: str, limit_price: float, base_size: float) -> Ord
343350
- base_size: New size for order
344351
"""
345352

346-
request_path = f"/api/v3/brokerage/orders/edit"
353+
request_path = "/api/v3/brokerage/orders/edit"
354+
method = "POST"
355+
356+
payload = {
357+
'order_id': order_id,
358+
'price': str(limit_price),
359+
'size': str(base_size)
360+
}
361+
362+
headers = self._build_request_headers(method, request_path, json.dumps(payload)) \
363+
if self._is_legacy_auth() \
364+
else self._build_request_headers_for_cloud(method, self._host, request_path)
365+
response = requests.post(self._base_url+request_path,
366+
json=payload, headers=headers,
367+
timeout=self.timeout)
368+
369+
edit_result = OrderEdit.from_response(response)
370+
return edit_result
371+
372+
def edit_order_preview(self, order_id: str, limit_price: float, base_size: float) -> OrderEditPreview:
373+
"""
374+
https://docs.cloud.coinbase.com/advanced-trade-api/reference/retailbrokerageapi_previeweditorder
375+
376+
Simulate an edit order request with a specified new size, or new price, to preview the result of an edit.
377+
Only limit order types, with time in force type of good-till-cancelled can be edited.
378+
379+
Args:
380+
- order_id: ID of order to edit.
381+
- limit_price: New price for order.
382+
- base_size: New size for order
383+
"""
384+
385+
request_path = "/api/v3/brokerage/orders/edit_preview"
347386
method = "POST"
348387

349388
payload = {
@@ -359,7 +398,7 @@ def edit_order(self, order_id: str, limit_price: float, base_size: float) -> Ord
359398
json=payload, headers=headers,
360399
timeout=self.timeout)
361400

362-
edit_result = OrderEdit.from_get_edit_response(response)
401+
edit_result = OrderEditPreview.from_response(response)
363402
return edit_result
364403

365404
def cancel_orders(self, order_ids: list) -> OrderBatchCancellation:

coinbaseadvanced/models/orders.py

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -450,18 +450,61 @@ class OrderEdit(BaseModel):
450450
"""
451451

452452
success: bool
453-
errors: List[dict]
454-
edit_failure_reason: str
455-
preview_failure_reason: str
453+
errors: Optional[List[dict]]
454+
edit_failure_reason: Optional[str]
455+
preview_failure_reason: Optional[str]
456456

457-
def __init__(self, success: bool, errors: List[dict], **kwargs) -> None:
457+
def __init__(self, success: bool, errors: Optional[List[dict]] = None, edit_failure_reason: Optional[str] = None, preview_failure_reason: Optional[str] = None, **kwargs) -> None:
458458
self.success = success
459-
self.errors = errors if errors is not None else None
459+
self.errors = errors
460+
self.edit_failure_reason = edit_failure_reason
461+
self.preview_failure_reason = preview_failure_reason
462+
463+
self.kwargs = kwargs
464+
465+
@classmethod
466+
def from_response(cls, response: requests.Response) -> 'OrderEdit':
467+
"""
468+
Factory Method.
469+
"""
470+
471+
if not response.ok:
472+
raise CoinbaseAdvancedTradeAPIError.not_ok_response(response)
473+
474+
result = response.json()
475+
return cls(**result)
476+
477+
478+
class OrderEditPreview(BaseModel):
479+
"""
480+
Order edit.
481+
"""
482+
483+
errors: Optional[List[dict]]
484+
slippage: str
485+
order_total: str
486+
commission_total: str
487+
quote_size: str
488+
base_size: str
489+
best_bid: str
490+
best_ask: str
491+
average_filled_price: str
492+
493+
def __init__(self, errors: Optional[List[dict]] = None, slippage: str = "", order_total: str = "", commission_total: str = "", quote_size: str = "", base_size: str = "", best_bid: str = "", best_ask: str = "", average_filled_price: str = "", **kwargs) -> None:
494+
self.errors = errors
495+
self.slippage = slippage
496+
self.order_total = order_total
497+
self.commission_total = commission_total
498+
self.quote_size = quote_size
499+
self.base_size = base_size
500+
self.best_bid = best_bid
501+
self.best_ask = best_ask
502+
self.average_filled_price = average_filled_price
460503

461504
self.kwargs = kwargs
462-
505+
463506
@classmethod
464-
def from_get_edit_response(cls, response: requests.Response) -> 'OrderEdit':
507+
def from_response(cls, response: requests.Response) -> 'OrderEditPreview':
465508
"""
466509
Factory Method.
467510
"""
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"errors": [],
3+
"slippage": "string",
4+
"order_total": "string",
5+
"commission_total": "string",
6+
"quote_size": "string",
7+
"base_size": "string",
8+
"best_bid": "string",
9+
"best_ask": "string",
10+
"average_filled_price": "string"
11+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"success": true,
3+
"errors": {
4+
"edit_failure_reason": "UNKNOWN_EDIT_ORDER_FAILURE_REASON",
5+
"preview_failure_reason": "UNKNOWN_PREVIEW_FAILURE_REASON"
6+
}
7+
}

tests/fixtures/fixtures.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,22 @@ def fixture_create_sell_market_order_success_response() -> mock.Mock:
9393
text=content)
9494

9595

96+
def fixture_edit_order_success_response() -> mock.Mock:
97+
with open('tests/fixtures/edit_order_success_response.json', 'r', encoding="utf-8") as file:
98+
content = file.read()
99+
return _fixtured_mock_response(
100+
ok=True,
101+
text=content)
102+
103+
104+
def fixture_edit_order_preview_success_response() -> mock.Mock:
105+
with open('tests/fixtures/edit_order_preview_success_response.json', 'r', encoding="utf-8") as file:
106+
content = file.read()
107+
return _fixtured_mock_response(
108+
ok=True,
109+
text=content)
110+
111+
96112
def fixture_default_order_failure_response() -> mock.Mock:
97113
with open('tests/fixtures/default_order_failure_response.json', 'r', encoding="utf-8") as file:
98114
content = file.read()

tests/playground.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,27 @@ def audit(func, args, fixture_obj):
7777
# 'client_order_id': generate_random_id(),
7878
# 'product_id': "ALGO-USD",
7979
# 'side': Side.BUY,
80-
# 'limit_price': ".15",
81-
# 'base_size': 1000
80+
# 'limit_price': ".12",
81+
# 'base_size': 10,
82+
# 'retail_portfolio_id': "bba559eb-5b24-45f5-9898-472a81c46a56"
8283
# },
8384
# fixture_obj
8485
# )
8586

87+
# audit(client.edit_order, {
88+
# 'order_id': "754eb3d3-13ad-4f25-b239-c79257b49f10",
89+
# 'limit_price': 0.08,
90+
# 'base_size': 6
91+
# }, json.loads(
92+
# fixture_edit_order_success_response().text))
93+
94+
audit(client.edit_order_preview, {
95+
'order_id': "754eb3d3-13ad-4f25-b239-c79257b49f10",
96+
'limit_price': 0.08,
97+
'base_size': 6
98+
}, json.loads(
99+
fixture_edit_order_preview_success_response().text))
100+
86101
# audit(client.cancel_orders, {'order_ids': ["42a266d3-591b-43d4-a968-a9a126f7b1a5", "82c6919f-6884-4127-95af-11db89b21ed3",
87102
# "c1d5ab66-d99a-4329-9c1d-be6a9f32c686"]}, json.loads(fixture_cancel_orders_success_response().text))
88103

tests/test_client.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,43 @@ def test_create_order_failure_no_funds(self, mock_post):
470470

471471
self.assertIsNotNone(order.order_error)
472472

473+
@mock.patch("coinbaseadvanced.client.requests.post")
474+
def test_edit_order_success(self, mock_post):
475+
476+
mock_resp = fixture_edit_order_success_response()
477+
mock_post.return_value = mock_resp
478+
479+
client = CoinbaseAdvancedTradeAPIClient(
480+
api_key='kjsldfk32234', secret_key='jlsjljsfd89y98y98shdfjksfd')
481+
482+
# Check output
483+
order_edited = client.edit_order("nlksdbnfgjd8y9mn,m234", .19, 10000)
484+
485+
self.assertTrue(order_edited.success)
486+
487+
@mock.patch("coinbaseadvanced.client.requests.post")
488+
def test_edit_order_preview_success(self, mock_post):
489+
490+
mock_resp = fixture_edit_order_preview_success_response()
491+
mock_post.return_value = mock_resp
492+
493+
client = CoinbaseAdvancedTradeAPIClient(
494+
api_key='kjsldfk32234', secret_key='jlsjljsfd89y98y98shdfjksfd')
495+
496+
# Check output
497+
order_edit_preview = client.edit_order_preview(
498+
"nlksdbnfgjd8y9mn,m234", .19, 10000)
499+
500+
self.assertEqual(order_edit_preview.errors, [])
501+
self.assertIsNotNone(order_edit_preview.base_size)
502+
self.assertIsNotNone(order_edit_preview.average_filled_price)
503+
self.assertIsNotNone(order_edit_preview.best_ask)
504+
self.assertIsNotNone(order_edit_preview.best_bid)
505+
self.assertIsNotNone(order_edit_preview.commission_total)
506+
self.assertIsNotNone(order_edit_preview.order_total)
507+
self.assertIsNotNone(order_edit_preview.quote_size)
508+
self.assertIsNotNone(order_edit_preview.slippage)
509+
473510
@mock.patch("coinbaseadvanced.client.requests.post")
474511
def test_cancel_orders_success(self, mock_post):
475512

0 commit comments

Comments
 (0)