Skip to main content

nautilus_coinbase/http/
query.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Venue-shaped request bodies for the Coinbase Advanced Trade REST API.
17//!
18//! These types serialize to the exact JSON shape Coinbase expects on its
19//! POST endpoints. The raw HTTP client takes one of these types per endpoint;
20//! the domain HTTP client builds them from Nautilus types.
21
22use jiff::Timestamp;
23use rust_decimal::Decimal;
24use serde::{Deserialize, Serialize};
25use ustr::Ustr;
26
27use crate::common::{
28    enums::{CoinbaseMarginType, CoinbaseOrderSide, CoinbaseStopDirection},
29    parse::{
30        deserialize_decimal_from_str, deserialize_optional_decimal_from_str,
31        serialize_decimal_as_str, serialize_optional_decimal_as_str,
32    },
33};
34
35/// Request body for `POST /api/v3/brokerage/orders` (Create Order).
36///
37/// # References
38///
39/// - <https://docs.cdp.coinbase.com/api-reference/advanced-trade-api/rest-api/orders/create-order>
40#[derive(Debug, Clone, Serialize)]
41pub struct CreateOrderRequest {
42    pub client_order_id: String,
43    pub product_id: Ustr,
44    pub side: CoinbaseOrderSide,
45    pub order_configuration: OrderConfiguration,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub self_trade_prevention_id: Option<String>,
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub leverage: Option<String>,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub margin_type: Option<CoinbaseMarginType>,
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub retail_portfolio_id: Option<String>,
54}
55
56/// Request body for `POST /api/v3/brokerage/orders/batch_cancel` (Cancel Orders).
57///
58/// # References
59///
60/// - <https://docs.cdp.coinbase.com/api-reference/advanced-trade-api/rest-api/orders/cancel-order>
61#[derive(Debug, Clone, Serialize)]
62pub struct CancelOrdersRequest {
63    pub order_ids: Vec<String>,
64}
65
66/// Filter parameters for `GET /api/v3/brokerage/orders/historical/batch`
67/// (List Orders).
68///
69/// `client_order_id_filter` is a client-side filter applied during pagination
70/// because Coinbase's batch endpoint does not accept a `client_order_id`
71/// query parameter.
72///
73/// # References
74///
75/// - <https://docs.cdp.coinbase.com/api-reference/advanced-trade-api/rest-api/orders/list-orders>
76#[derive(Debug, Clone, Default)]
77pub struct OrderListQuery {
78    pub product_id: Option<String>,
79    pub open_only: bool,
80    pub start: Option<Timestamp>,
81    pub end: Option<Timestamp>,
82    pub limit: Option<u32>,
83    pub client_order_id_filter: Option<String>,
84}
85
86/// Filter parameters for `GET /api/v3/brokerage/orders/historical/fills`
87/// (List Fills).
88///
89/// # References
90///
91/// - <https://docs.cdp.coinbase.com/api-reference/advanced-trade-api/rest-api/orders/list-fills>
92#[derive(Debug, Clone, Default)]
93pub struct FillListQuery {
94    pub product_id: Option<String>,
95    pub venue_order_id: Option<String>,
96    pub start: Option<Timestamp>,
97    pub end: Option<Timestamp>,
98    pub limit: Option<u32>,
99}
100
101/// Request body for `POST /api/v3/brokerage/orders/edit` (Edit Order).
102///
103/// Coinbase restricts edits to GTC variants of LIMIT (and limited STOP_LIMIT
104/// configurations). Each field is optional so callers can edit a subset.
105///
106/// # References
107///
108/// - <https://docs.cdp.coinbase.com/api-reference/advanced-trade-api/rest-api/orders/edit-order>
109#[derive(Debug, Clone, Serialize)]
110pub struct EditOrderRequest {
111    pub order_id: String,
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub price: Option<String>,
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub size: Option<String>,
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub stop_price: Option<String>,
118}
119
120/// Order configuration for different order types.
121///
122/// Uses `#[serde(untagged)]` because Coinbase wraps each order type in a
123/// uniquely-named key (e.g. `market_market_ioc`, `limit_limit_gtc`), which
124/// serde matches by attempting each variant in declaration order. Error
125/// messages on deserialization failure are opaque; prefer constructing
126/// variants directly rather than deserializing from untrusted JSON.
127#[derive(Debug, Clone, Serialize, Deserialize)]
128#[serde(untagged)]
129pub enum OrderConfiguration {
130    MarketIoc(MarketIoc),
131    MarketFok(MarketFok),
132    LimitGtc(LimitGtc),
133    LimitGtd(LimitGtd),
134    LimitFok(LimitFok),
135    StopLimitGtc(StopLimitGtc),
136    StopLimitGtd(StopLimitGtd),
137}
138
139/// Market order with IOC fill.
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct MarketIoc {
142    pub market_market_ioc: MarketParams,
143}
144
145/// Market order with FOK fill.
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct MarketFok {
148    pub market_market_fok: MarketParams,
149}
150
151/// Market order parameters (shared by `market_market_ioc` and
152/// `market_market_fok`; both wire shapes accept the same `base_size` /
153/// `quote_size` body).
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct MarketParams {
156    #[serde(
157        default,
158        skip_serializing_if = "Option::is_none",
159        deserialize_with = "deserialize_optional_decimal_from_str",
160        serialize_with = "serialize_optional_decimal_as_str"
161    )]
162    pub quote_size: Option<Decimal>,
163    #[serde(
164        default,
165        skip_serializing_if = "Option::is_none",
166        deserialize_with = "deserialize_optional_decimal_from_str",
167        serialize_with = "serialize_optional_decimal_as_str"
168    )]
169    pub base_size: Option<Decimal>,
170}
171
172/// Limit GTC order.
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct LimitGtc {
175    pub limit_limit_gtc: LimitGtcParams,
176}
177
178/// Limit GTC parameters.
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct LimitGtcParams {
181    #[serde(
182        serialize_with = "serialize_decimal_as_str",
183        deserialize_with = "deserialize_decimal_from_str"
184    )]
185    pub base_size: Decimal,
186    #[serde(
187        serialize_with = "serialize_decimal_as_str",
188        deserialize_with = "deserialize_decimal_from_str"
189    )]
190    pub limit_price: Decimal,
191    #[serde(default)]
192    pub post_only: bool,
193}
194
195/// Limit GTD order.
196#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct LimitGtd {
198    pub limit_limit_gtd: LimitGtdParams,
199}
200
201/// Limit GTD parameters.
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct LimitGtdParams {
204    #[serde(
205        serialize_with = "serialize_decimal_as_str",
206        deserialize_with = "deserialize_decimal_from_str"
207    )]
208    pub base_size: Decimal,
209    #[serde(
210        serialize_with = "serialize_decimal_as_str",
211        deserialize_with = "deserialize_decimal_from_str"
212    )]
213    pub limit_price: Decimal,
214    pub end_time: String,
215    #[serde(default)]
216    pub post_only: bool,
217}
218
219/// Limit FOK order.
220#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct LimitFok {
222    pub limit_limit_fok: LimitFokParams,
223}
224
225/// Limit FOK parameters.
226#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct LimitFokParams {
228    #[serde(
229        serialize_with = "serialize_decimal_as_str",
230        deserialize_with = "deserialize_decimal_from_str"
231    )]
232    pub base_size: Decimal,
233    #[serde(
234        serialize_with = "serialize_decimal_as_str",
235        deserialize_with = "deserialize_decimal_from_str"
236    )]
237    pub limit_price: Decimal,
238}
239
240/// Stop-limit GTC order.
241#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct StopLimitGtc {
243    pub stop_limit_stop_limit_gtc: StopLimitGtcParams,
244}
245
246/// Stop-limit GTC parameters.
247#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct StopLimitGtcParams {
249    #[serde(
250        serialize_with = "serialize_decimal_as_str",
251        deserialize_with = "deserialize_decimal_from_str"
252    )]
253    pub base_size: Decimal,
254    #[serde(
255        serialize_with = "serialize_decimal_as_str",
256        deserialize_with = "deserialize_decimal_from_str"
257    )]
258    pub limit_price: Decimal,
259    #[serde(
260        serialize_with = "serialize_decimal_as_str",
261        deserialize_with = "deserialize_decimal_from_str"
262    )]
263    pub stop_price: Decimal,
264    pub stop_direction: CoinbaseStopDirection,
265}
266
267/// Stop-limit GTD order.
268#[derive(Debug, Clone, Serialize, Deserialize)]
269pub struct StopLimitGtd {
270    pub stop_limit_stop_limit_gtd: StopLimitGtdParams,
271}
272
273/// Stop-limit GTD parameters.
274#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct StopLimitGtdParams {
276    #[serde(
277        serialize_with = "serialize_decimal_as_str",
278        deserialize_with = "deserialize_decimal_from_str"
279    )]
280    pub base_size: Decimal,
281    #[serde(
282        serialize_with = "serialize_decimal_as_str",
283        deserialize_with = "deserialize_decimal_from_str"
284    )]
285    pub limit_price: Decimal,
286    #[serde(
287        serialize_with = "serialize_decimal_as_str",
288        deserialize_with = "deserialize_decimal_from_str"
289    )]
290    pub stop_price: Decimal,
291    pub stop_direction: CoinbaseStopDirection,
292    pub end_time: String,
293}
294
295#[cfg(test)]
296mod tests {
297    use std::str::FromStr;
298
299    use rstest::rstest;
300    use rust_decimal::Decimal;
301    use serde_json::json;
302
303    use super::*;
304    use crate::common::consts::{
305        ORDER_CONFIG_BASE_SIZE, ORDER_CONFIG_LIMIT_GTC, ORDER_CONFIG_LIMIT_PRICE,
306        ORDER_CONFIG_MARKET_IOC, ORDER_CONFIG_QUOTE_SIZE,
307    };
308
309    #[rstest]
310    fn test_serialize_market_order() {
311        let order = CreateOrderRequest {
312            client_order_id: "test-123".to_string(),
313            product_id: Ustr::from("BTC-USD"),
314            side: CoinbaseOrderSide::Buy,
315            order_configuration: OrderConfiguration::MarketIoc(MarketIoc {
316                market_market_ioc: MarketParams {
317                    quote_size: Some(Decimal::from_str("100").unwrap()),
318                    base_size: None,
319                },
320            }),
321            self_trade_prevention_id: None,
322            leverage: None,
323            margin_type: None,
324            retail_portfolio_id: None,
325        };
326
327        let value = serde_json::to_value(&order).unwrap();
328        assert_eq!(value["client_order_id"], "test-123");
329        assert_eq!(value["product_id"], "BTC-USD");
330        assert_eq!(value["side"], "BUY");
331        assert_eq!(
332            value["order_configuration"][ORDER_CONFIG_MARKET_IOC][ORDER_CONFIG_QUOTE_SIZE],
333            "100"
334        );
335    }
336
337    #[rstest]
338    fn test_serialize_limit_gtc_order() {
339        let order = CreateOrderRequest {
340            client_order_id: "test-456".to_string(),
341            product_id: Ustr::from("ETH-USD"),
342            side: CoinbaseOrderSide::Sell,
343            order_configuration: OrderConfiguration::LimitGtc(LimitGtc {
344                limit_limit_gtc: LimitGtcParams {
345                    base_size: Decimal::from_str("1.5").unwrap(),
346                    limit_price: Decimal::from_str("3500.00").unwrap(),
347                    post_only: true,
348                },
349            }),
350            self_trade_prevention_id: None,
351            leverage: None,
352            margin_type: None,
353            retail_portfolio_id: None,
354        };
355
356        let value = serde_json::to_value(&order).unwrap();
357        assert_eq!(value["side"], "SELL");
358        assert_eq!(
359            value["order_configuration"][ORDER_CONFIG_LIMIT_GTC][ORDER_CONFIG_BASE_SIZE],
360            "1.5"
361        );
362        assert_eq!(
363            value["order_configuration"][ORDER_CONFIG_LIMIT_GTC][ORDER_CONFIG_LIMIT_PRICE],
364            "3500.00"
365        );
366    }
367
368    #[rstest]
369    fn test_serialize_cancel_orders_request() {
370        let request = CancelOrdersRequest {
371            order_ids: vec!["abc".to_string(), "def".to_string()],
372        };
373        assert_eq!(
374            serde_json::to_value(&request).unwrap(),
375            json!({"order_ids": ["abc", "def"]})
376        );
377    }
378
379    #[rstest]
380    fn test_serialize_edit_order_request_omits_none_fields() {
381        let request = EditOrderRequest {
382            order_id: "venue-1".to_string(),
383            price: Some("100.00".to_string()),
384            size: None,
385            stop_price: None,
386        };
387        let value = serde_json::to_value(&request).unwrap();
388        assert_eq!(value["order_id"], "venue-1");
389        assert_eq!(value["price"], "100.00");
390        assert!(value.get("size").is_none());
391        assert!(value.get("stop_price").is_none());
392    }
393}