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 chrono::{DateTime, Utc};
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    /// Derivatives-only flag that marks the order as position-reducing only.
55    ///
56    /// Coinbase does not document `reduce_only` as an accepted create-order
57    /// field; the venue's failure-reason enum acknowledges the concept but the
58    /// order schema has no slot for it. The field is threaded through the
59    /// request for API parity with other adapters and is omitted from the wire
60    /// payload when `false`.
61    #[serde(skip_serializing_if = "std::ops::Not::not")]
62    pub reduce_only: bool,
63}
64
65/// Request body for `POST /api/v3/brokerage/orders/batch_cancel` (Cancel Orders).
66///
67/// # References
68///
69/// - <https://docs.cdp.coinbase.com/api-reference/advanced-trade-api/rest-api/orders/cancel-order>
70#[derive(Debug, Clone, Serialize)]
71pub struct CancelOrdersRequest {
72    pub order_ids: Vec<String>,
73}
74
75/// Filter parameters for `GET /api/v3/brokerage/orders/historical/batch`
76/// (List Orders).
77///
78/// `client_order_id_filter` is a client-side filter applied during pagination
79/// because Coinbase's batch endpoint does not accept a `client_order_id`
80/// query parameter.
81///
82/// # References
83///
84/// - <https://docs.cdp.coinbase.com/api-reference/advanced-trade-api/rest-api/orders/list-orders>
85#[derive(Debug, Clone, Default)]
86pub struct OrderListQuery {
87    pub product_id: Option<String>,
88    pub open_only: bool,
89    pub start: Option<DateTime<Utc>>,
90    pub end: Option<DateTime<Utc>>,
91    pub limit: Option<u32>,
92    pub client_order_id_filter: Option<String>,
93}
94
95/// Filter parameters for `GET /api/v3/brokerage/orders/historical/fills`
96/// (List Fills).
97///
98/// # References
99///
100/// - <https://docs.cdp.coinbase.com/api-reference/advanced-trade-api/rest-api/orders/list-fills>
101#[derive(Debug, Clone, Default)]
102pub struct FillListQuery {
103    pub product_id: Option<String>,
104    pub venue_order_id: Option<String>,
105    pub start: Option<DateTime<Utc>>,
106    pub end: Option<DateTime<Utc>>,
107    pub limit: Option<u32>,
108}
109
110/// Request body for `POST /api/v3/brokerage/orders/edit` (Edit Order).
111///
112/// Coinbase restricts edits to GTC variants of LIMIT (and limited STOP_LIMIT
113/// configurations). Each field is optional so callers can edit a subset.
114///
115/// # References
116///
117/// - <https://docs.cdp.coinbase.com/api-reference/advanced-trade-api/rest-api/orders/edit-order>
118#[derive(Debug, Clone, Serialize)]
119pub struct EditOrderRequest {
120    pub order_id: String,
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub price: Option<String>,
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub size: Option<String>,
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub stop_price: Option<String>,
127}
128
129/// Order configuration for different order types.
130///
131/// Uses `#[serde(untagged)]` because Coinbase wraps each order type in a
132/// uniquely-named key (e.g. `market_market_ioc`, `limit_limit_gtc`), which
133/// serde matches by attempting each variant in declaration order. Error
134/// messages on deserialization failure are opaque; prefer constructing
135/// variants directly rather than deserializing from untrusted JSON.
136#[derive(Debug, Clone, Serialize, Deserialize)]
137#[serde(untagged)]
138pub enum OrderConfiguration {
139    MarketIoc(MarketIoc),
140    MarketFok(MarketFok),
141    LimitGtc(LimitGtc),
142    LimitGtd(LimitGtd),
143    LimitFok(LimitFok),
144    StopLimitGtc(StopLimitGtc),
145    StopLimitGtd(StopLimitGtd),
146}
147
148/// Market order with IOC fill.
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct MarketIoc {
151    pub market_market_ioc: MarketParams,
152}
153
154/// Market order with FOK fill.
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct MarketFok {
157    pub market_market_fok: MarketParams,
158}
159
160/// Market order parameters (shared by `market_market_ioc` and
161/// `market_market_fok`; both wire shapes accept the same `base_size` /
162/// `quote_size` body).
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct MarketParams {
165    #[serde(
166        default,
167        skip_serializing_if = "Option::is_none",
168        deserialize_with = "deserialize_optional_decimal_from_str",
169        serialize_with = "serialize_optional_decimal_as_str"
170    )]
171    pub quote_size: Option<Decimal>,
172    #[serde(
173        default,
174        skip_serializing_if = "Option::is_none",
175        deserialize_with = "deserialize_optional_decimal_from_str",
176        serialize_with = "serialize_optional_decimal_as_str"
177    )]
178    pub base_size: Option<Decimal>,
179}
180
181/// Limit GTC order.
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct LimitGtc {
184    pub limit_limit_gtc: LimitGtcParams,
185}
186
187/// Limit GTC parameters.
188#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct LimitGtcParams {
190    #[serde(
191        serialize_with = "serialize_decimal_as_str",
192        deserialize_with = "deserialize_decimal_from_str"
193    )]
194    pub base_size: Decimal,
195    #[serde(
196        serialize_with = "serialize_decimal_as_str",
197        deserialize_with = "deserialize_decimal_from_str"
198    )]
199    pub limit_price: Decimal,
200    #[serde(default)]
201    pub post_only: bool,
202}
203
204/// Limit GTD order.
205#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct LimitGtd {
207    pub limit_limit_gtd: LimitGtdParams,
208}
209
210/// Limit GTD parameters.
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct LimitGtdParams {
213    #[serde(
214        serialize_with = "serialize_decimal_as_str",
215        deserialize_with = "deserialize_decimal_from_str"
216    )]
217    pub base_size: Decimal,
218    #[serde(
219        serialize_with = "serialize_decimal_as_str",
220        deserialize_with = "deserialize_decimal_from_str"
221    )]
222    pub limit_price: Decimal,
223    pub end_time: String,
224    #[serde(default)]
225    pub post_only: bool,
226}
227
228/// Limit FOK order.
229#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct LimitFok {
231    pub limit_limit_fok: LimitFokParams,
232}
233
234/// Limit FOK parameters.
235#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct LimitFokParams {
237    #[serde(
238        serialize_with = "serialize_decimal_as_str",
239        deserialize_with = "deserialize_decimal_from_str"
240    )]
241    pub base_size: Decimal,
242    #[serde(
243        serialize_with = "serialize_decimal_as_str",
244        deserialize_with = "deserialize_decimal_from_str"
245    )]
246    pub limit_price: Decimal,
247}
248
249/// Stop-limit GTC order.
250#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct StopLimitGtc {
252    pub stop_limit_stop_limit_gtc: StopLimitGtcParams,
253}
254
255/// Stop-limit GTC parameters.
256#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct StopLimitGtcParams {
258    #[serde(
259        serialize_with = "serialize_decimal_as_str",
260        deserialize_with = "deserialize_decimal_from_str"
261    )]
262    pub base_size: Decimal,
263    #[serde(
264        serialize_with = "serialize_decimal_as_str",
265        deserialize_with = "deserialize_decimal_from_str"
266    )]
267    pub limit_price: Decimal,
268    #[serde(
269        serialize_with = "serialize_decimal_as_str",
270        deserialize_with = "deserialize_decimal_from_str"
271    )]
272    pub stop_price: Decimal,
273    pub stop_direction: CoinbaseStopDirection,
274}
275
276/// Stop-limit GTD order.
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct StopLimitGtd {
279    pub stop_limit_stop_limit_gtd: StopLimitGtdParams,
280}
281
282/// Stop-limit GTD parameters.
283#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct StopLimitGtdParams {
285    #[serde(
286        serialize_with = "serialize_decimal_as_str",
287        deserialize_with = "deserialize_decimal_from_str"
288    )]
289    pub base_size: Decimal,
290    #[serde(
291        serialize_with = "serialize_decimal_as_str",
292        deserialize_with = "deserialize_decimal_from_str"
293    )]
294    pub limit_price: Decimal,
295    #[serde(
296        serialize_with = "serialize_decimal_as_str",
297        deserialize_with = "deserialize_decimal_from_str"
298    )]
299    pub stop_price: Decimal,
300    pub stop_direction: CoinbaseStopDirection,
301    pub end_time: String,
302}
303
304#[cfg(test)]
305mod tests {
306    use std::str::FromStr;
307
308    use rstest::rstest;
309    use rust_decimal::Decimal;
310    use serde_json::json;
311
312    use super::*;
313    use crate::common::consts::{
314        ORDER_CONFIG_BASE_SIZE, ORDER_CONFIG_LIMIT_GTC, ORDER_CONFIG_LIMIT_PRICE,
315        ORDER_CONFIG_MARKET_IOC, ORDER_CONFIG_QUOTE_SIZE,
316    };
317
318    #[rstest]
319    fn test_serialize_market_order() {
320        let order = CreateOrderRequest {
321            client_order_id: "test-123".to_string(),
322            product_id: Ustr::from("BTC-USD"),
323            side: CoinbaseOrderSide::Buy,
324            order_configuration: OrderConfiguration::MarketIoc(MarketIoc {
325                market_market_ioc: MarketParams {
326                    quote_size: Some(Decimal::from_str("100").unwrap()),
327                    base_size: None,
328                },
329            }),
330            self_trade_prevention_id: None,
331            leverage: None,
332            margin_type: None,
333            retail_portfolio_id: None,
334            reduce_only: false,
335        };
336
337        let value = serde_json::to_value(&order).unwrap();
338        assert_eq!(value["client_order_id"], "test-123");
339        assert_eq!(value["product_id"], "BTC-USD");
340        assert_eq!(value["side"], "BUY");
341        assert_eq!(
342            value["order_configuration"][ORDER_CONFIG_MARKET_IOC][ORDER_CONFIG_QUOTE_SIZE],
343            "100"
344        );
345    }
346
347    #[rstest]
348    fn test_serialize_limit_gtc_order() {
349        let order = CreateOrderRequest {
350            client_order_id: "test-456".to_string(),
351            product_id: Ustr::from("ETH-USD"),
352            side: CoinbaseOrderSide::Sell,
353            order_configuration: OrderConfiguration::LimitGtc(LimitGtc {
354                limit_limit_gtc: LimitGtcParams {
355                    base_size: Decimal::from_str("1.5").unwrap(),
356                    limit_price: Decimal::from_str("3500.00").unwrap(),
357                    post_only: true,
358                },
359            }),
360            self_trade_prevention_id: None,
361            leverage: None,
362            margin_type: None,
363            retail_portfolio_id: None,
364            reduce_only: false,
365        };
366
367        let value = serde_json::to_value(&order).unwrap();
368        assert_eq!(value["side"], "SELL");
369        assert_eq!(
370            value["order_configuration"][ORDER_CONFIG_LIMIT_GTC][ORDER_CONFIG_BASE_SIZE],
371            "1.5"
372        );
373        assert_eq!(
374            value["order_configuration"][ORDER_CONFIG_LIMIT_GTC][ORDER_CONFIG_LIMIT_PRICE],
375            "3500.00"
376        );
377    }
378
379    #[rstest]
380    fn test_serialize_cancel_orders_request() {
381        let request = CancelOrdersRequest {
382            order_ids: vec!["abc".to_string(), "def".to_string()],
383        };
384        assert_eq!(
385            serde_json::to_value(&request).unwrap(),
386            json!({"order_ids": ["abc", "def"]})
387        );
388    }
389
390    #[rstest]
391    fn test_serialize_edit_order_request_omits_none_fields() {
392        let request = EditOrderRequest {
393            order_id: "venue-1".to_string(),
394            price: Some("100.00".to_string()),
395            size: None,
396            stop_price: None,
397        };
398        let value = serde_json::to_value(&request).unwrap();
399        assert_eq!(value["order_id"], "venue-1");
400        assert_eq!(value["price"], "100.00");
401        assert!(value.get("size").is_none());
402        assert!(value.get("stop_price").is_none());
403    }
404}