Skip to main content

coinbase_advanced/rest/
order_builder.rs

1//! Ergonomic order builder APIs.
2//!
3//! These builders provide a more convenient way to construct orders
4//! compared to manually creating `CreateOrderRequest` objects.
5
6use crate::client::RestClient;
7use crate::error::{Error, Result};
8use crate::models::{CreateOrderRequest, CreateOrderResponse, OrderConfiguration, OrderSide, StopDirection};
9
10/// Builder for market orders.
11pub struct MarketOrderBuilder<'a> {
12    client: &'a RestClient,
13    product_id: Option<String>,
14    side: Option<OrderSide>,
15    quote_size: Option<String>,
16    base_size: Option<String>,
17    client_order_id: Option<String>,
18}
19
20impl<'a> MarketOrderBuilder<'a> {
21    /// Create a new market order builder.
22    pub(crate) fn new(client: &'a RestClient) -> Self {
23        Self {
24            client,
25            product_id: None,
26            side: None,
27            quote_size: None,
28            base_size: None,
29            client_order_id: None,
30        }
31    }
32
33    /// Set the product ID.
34    pub fn product_id(mut self, product_id: impl Into<String>) -> Self {
35        self.product_id = Some(product_id.into());
36        self
37    }
38
39    /// Set as a buy order.
40    pub fn buy(mut self, product_id: impl Into<String>) -> Self {
41        self.product_id = Some(product_id.into());
42        self.side = Some(OrderSide::Buy);
43        self
44    }
45
46    /// Set as a sell order.
47    pub fn sell(mut self, product_id: impl Into<String>) -> Self {
48        self.product_id = Some(product_id.into());
49        self.side = Some(OrderSide::Sell);
50        self
51    }
52
53    /// Set the quote size (amount in quote currency, e.g., USD).
54    pub fn quote_size(mut self, quote_size: impl Into<String>) -> Self {
55        self.quote_size = Some(quote_size.into());
56        self
57    }
58
59    /// Set the base size (amount in base currency, e.g., BTC).
60    pub fn base_size(mut self, base_size: impl Into<String>) -> Self {
61        self.base_size = Some(base_size.into());
62        self
63    }
64
65    /// Set a custom client order ID.
66    pub fn client_order_id(mut self, client_order_id: impl Into<String>) -> Self {
67        self.client_order_id = Some(client_order_id.into());
68        self
69    }
70
71    /// Build and send the order.
72    pub async fn send(self) -> Result<CreateOrderResponse> {
73        let product_id = self.product_id
74            .ok_or_else(|| Error::request("product_id is required"))?;
75        let side = self.side
76            .ok_or_else(|| Error::request("side is required (use .buy() or .sell())"))?;
77
78        let config = if let Some(quote_size) = self.quote_size {
79            OrderConfiguration::market_buy_quote(quote_size)
80        } else if let Some(base_size) = self.base_size {
81            if side == OrderSide::Buy {
82                OrderConfiguration::market_buy_base(base_size)
83            } else {
84                OrderConfiguration::market_sell(base_size)
85            }
86        } else {
87            return Err(Error::request("either quote_size or base_size is required"));
88        };
89
90        let client_order_id = self.client_order_id
91            .unwrap_or_else(uuid_v4);
92
93        let request = CreateOrderRequest::new(client_order_id, product_id, side, config);
94        self.client.orders().create(request).await
95    }
96}
97
98/// Builder for limit GTC (good-til-cancelled) orders.
99pub struct LimitOrderGtcBuilder<'a> {
100    client: &'a RestClient,
101    product_id: Option<String>,
102    side: Option<OrderSide>,
103    base_size: Option<String>,
104    limit_price: Option<String>,
105    post_only: bool,
106    client_order_id: Option<String>,
107}
108
109impl<'a> LimitOrderGtcBuilder<'a> {
110    /// Create a new limit order GTC builder.
111    pub(crate) fn new(client: &'a RestClient) -> Self {
112        Self {
113            client,
114            product_id: None,
115            side: None,
116            base_size: None,
117            limit_price: None,
118            post_only: false,
119            client_order_id: None,
120        }
121    }
122
123    /// Set as a buy order.
124    pub fn buy(mut self, product_id: impl Into<String>) -> Self {
125        self.product_id = Some(product_id.into());
126        self.side = Some(OrderSide::Buy);
127        self
128    }
129
130    /// Set as a sell order.
131    pub fn sell(mut self, product_id: impl Into<String>) -> Self {
132        self.product_id = Some(product_id.into());
133        self.side = Some(OrderSide::Sell);
134        self
135    }
136
137    /// Set the base size.
138    pub fn base_size(mut self, base_size: impl Into<String>) -> Self {
139        self.base_size = Some(base_size.into());
140        self
141    }
142
143    /// Set the limit price.
144    pub fn limit_price(mut self, limit_price: impl Into<String>) -> Self {
145        self.limit_price = Some(limit_price.into());
146        self
147    }
148
149    /// Set post-only mode (only add liquidity).
150    pub fn post_only(mut self, post_only: bool) -> Self {
151        self.post_only = post_only;
152        self
153    }
154
155    /// Set a custom client order ID.
156    pub fn client_order_id(mut self, client_order_id: impl Into<String>) -> Self {
157        self.client_order_id = Some(client_order_id.into());
158        self
159    }
160
161    /// Build and send the order.
162    pub async fn send(self) -> Result<CreateOrderResponse> {
163        let product_id = self.product_id
164            .ok_or_else(|| Error::request("product_id is required"))?;
165        let side = self.side
166            .ok_or_else(|| Error::request("side is required (use .buy() or .sell())"))?;
167        let base_size = self.base_size
168            .ok_or_else(|| Error::request("base_size is required"))?;
169        let limit_price = self.limit_price
170            .ok_or_else(|| Error::request("limit_price is required"))?;
171
172        let config = OrderConfiguration::limit_gtc(base_size, limit_price, self.post_only);
173        let client_order_id = self.client_order_id.unwrap_or_else(uuid_v4);
174
175        let request = CreateOrderRequest::new(client_order_id, product_id, side, config);
176        self.client.orders().create(request).await
177    }
178}
179
180/// Builder for limit GTD (good-til-date) orders.
181pub struct LimitOrderGtdBuilder<'a> {
182    client: &'a RestClient,
183    product_id: Option<String>,
184    side: Option<OrderSide>,
185    base_size: Option<String>,
186    limit_price: Option<String>,
187    end_time: Option<String>,
188    post_only: bool,
189    client_order_id: Option<String>,
190}
191
192impl<'a> LimitOrderGtdBuilder<'a> {
193    /// Create a new limit order GTD builder.
194    pub(crate) fn new(client: &'a RestClient) -> Self {
195        Self {
196            client,
197            product_id: None,
198            side: None,
199            base_size: None,
200            limit_price: None,
201            end_time: None,
202            post_only: false,
203            client_order_id: None,
204        }
205    }
206
207    /// Set as a buy order.
208    pub fn buy(mut self, product_id: impl Into<String>) -> Self {
209        self.product_id = Some(product_id.into());
210        self.side = Some(OrderSide::Buy);
211        self
212    }
213
214    /// Set as a sell order.
215    pub fn sell(mut self, product_id: impl Into<String>) -> Self {
216        self.product_id = Some(product_id.into());
217        self.side = Some(OrderSide::Sell);
218        self
219    }
220
221    /// Set the base size.
222    pub fn base_size(mut self, base_size: impl Into<String>) -> Self {
223        self.base_size = Some(base_size.into());
224        self
225    }
226
227    /// Set the limit price.
228    pub fn limit_price(mut self, limit_price: impl Into<String>) -> Self {
229        self.limit_price = Some(limit_price.into());
230        self
231    }
232
233    /// Set the end time (ISO 8601 format).
234    pub fn end_time(mut self, end_time: impl Into<String>) -> Self {
235        self.end_time = Some(end_time.into());
236        self
237    }
238
239    /// Set post-only mode (only add liquidity).
240    pub fn post_only(mut self, post_only: bool) -> Self {
241        self.post_only = post_only;
242        self
243    }
244
245    /// Set a custom client order ID.
246    pub fn client_order_id(mut self, client_order_id: impl Into<String>) -> Self {
247        self.client_order_id = Some(client_order_id.into());
248        self
249    }
250
251    /// Build and send the order.
252    pub async fn send(self) -> Result<CreateOrderResponse> {
253        let product_id = self.product_id
254            .ok_or_else(|| Error::request("product_id is required"))?;
255        let side = self.side
256            .ok_or_else(|| Error::request("side is required (use .buy() or .sell())"))?;
257        let base_size = self.base_size
258            .ok_or_else(|| Error::request("base_size is required"))?;
259        let limit_price = self.limit_price
260            .ok_or_else(|| Error::request("limit_price is required"))?;
261        let end_time = self.end_time
262            .ok_or_else(|| Error::request("end_time is required"))?;
263
264        let config = OrderConfiguration::limit_gtd(base_size, limit_price, end_time, self.post_only);
265        let client_order_id = self.client_order_id.unwrap_or_else(uuid_v4);
266
267        let request = CreateOrderRequest::new(client_order_id, product_id, side, config);
268        self.client.orders().create(request).await
269    }
270}
271
272/// Builder for stop-limit GTC orders.
273pub struct StopLimitOrderGtcBuilder<'a> {
274    client: &'a RestClient,
275    product_id: Option<String>,
276    side: Option<OrderSide>,
277    base_size: Option<String>,
278    limit_price: Option<String>,
279    stop_price: Option<String>,
280    stop_direction: Option<StopDirection>,
281    client_order_id: Option<String>,
282}
283
284impl<'a> StopLimitOrderGtcBuilder<'a> {
285    /// Create a new stop-limit order GTC builder.
286    pub(crate) fn new(client: &'a RestClient) -> Self {
287        Self {
288            client,
289            product_id: None,
290            side: None,
291            base_size: None,
292            limit_price: None,
293            stop_price: None,
294            stop_direction: None,
295            client_order_id: None,
296        }
297    }
298
299    /// Set as a buy order.
300    pub fn buy(mut self, product_id: impl Into<String>) -> Self {
301        self.product_id = Some(product_id.into());
302        self.side = Some(OrderSide::Buy);
303        self
304    }
305
306    /// Set as a sell order.
307    pub fn sell(mut self, product_id: impl Into<String>) -> Self {
308        self.product_id = Some(product_id.into());
309        self.side = Some(OrderSide::Sell);
310        self
311    }
312
313    /// Set the base size.
314    pub fn base_size(mut self, base_size: impl Into<String>) -> Self {
315        self.base_size = Some(base_size.into());
316        self
317    }
318
319    /// Set the limit price.
320    pub fn limit_price(mut self, limit_price: impl Into<String>) -> Self {
321        self.limit_price = Some(limit_price.into());
322        self
323    }
324
325    /// Set the stop price.
326    pub fn stop_price(mut self, stop_price: impl Into<String>) -> Self {
327        self.stop_price = Some(stop_price.into());
328        self
329    }
330
331    /// Set the stop direction.
332    pub fn stop_direction(mut self, stop_direction: StopDirection) -> Self {
333        self.stop_direction = Some(stop_direction);
334        self
335    }
336
337    /// Set a custom client order ID.
338    pub fn client_order_id(mut self, client_order_id: impl Into<String>) -> Self {
339        self.client_order_id = Some(client_order_id.into());
340        self
341    }
342
343    /// Build and send the order.
344    pub async fn send(self) -> Result<CreateOrderResponse> {
345        let product_id = self.product_id
346            .ok_or_else(|| Error::request("product_id is required"))?;
347        let side = self.side
348            .ok_or_else(|| Error::request("side is required (use .buy() or .sell())"))?;
349        let base_size = self.base_size
350            .ok_or_else(|| Error::request("base_size is required"))?;
351        let limit_price = self.limit_price
352            .ok_or_else(|| Error::request("limit_price is required"))?;
353        let stop_price = self.stop_price
354            .ok_or_else(|| Error::request("stop_price is required"))?;
355        let stop_direction = self.stop_direction
356            .ok_or_else(|| Error::request("stop_direction is required"))?;
357
358        let config = OrderConfiguration::stop_limit_gtc(base_size, limit_price, stop_price, stop_direction);
359        let client_order_id = self.client_order_id.unwrap_or_else(uuid_v4);
360
361        let request = CreateOrderRequest::new(client_order_id, product_id, side, config);
362        self.client.orders().create(request).await
363    }
364}
365
366/// Generate a simple UUID v4 string.
367fn uuid_v4() -> String {
368    use std::time::{SystemTime, UNIX_EPOCH};
369
370    let now = SystemTime::now()
371        .duration_since(UNIX_EPOCH)
372        .unwrap_or_default()
373        .as_nanos();
374
375    // Simple UUID-like format using timestamp and random bits.
376    format!(
377        "{:08x}-{:04x}-4{:03x}-{:04x}-{:012x}",
378        (now >> 96) as u32,
379        (now >> 80) as u16,
380        (now >> 68) as u16 & 0x0fff,
381        ((now >> 52) as u16 & 0x3fff) | 0x8000,
382        now as u64 & 0xffffffffffff
383    )
384}
385
386// Add builder methods to RestClient.
387impl RestClient {
388    /// Create a market order builder.
389    ///
390    /// # Example
391    ///
392    /// ```no_run
393    /// # use coinbase_advanced::{RestClient, Credentials};
394    /// # async fn example() -> coinbase_advanced::Result<()> {
395    /// let client = RestClient::builder()
396    ///     .credentials(Credentials::from_env()?)
397    ///     .build()?;
398    ///
399    /// // Buy $100 of BTC
400    /// let response = client.market_order()
401    ///     .buy("BTC-USD")
402    ///     .quote_size("100.00")
403    ///     .send()
404    ///     .await?;
405    ///
406    /// // Sell 0.001 BTC
407    /// let response = client.market_order()
408    ///     .sell("BTC-USD")
409    ///     .base_size("0.001")
410    ///     .send()
411    ///     .await?;
412    /// # Ok(())
413    /// # }
414    /// ```
415    pub fn market_order(&self) -> MarketOrderBuilder<'_> {
416        MarketOrderBuilder::new(self)
417    }
418
419    /// Create a limit order (GTC) builder.
420    ///
421    /// # Example
422    ///
423    /// ```no_run
424    /// # use coinbase_advanced::{RestClient, Credentials};
425    /// # async fn example() -> coinbase_advanced::Result<()> {
426    /// let client = RestClient::builder()
427    ///     .credentials(Credentials::from_env()?)
428    ///     .build()?;
429    ///
430    /// let response = client.limit_order_gtc()
431    ///     .buy("BTC-USD")
432    ///     .base_size("0.001")
433    ///     .limit_price("50000.00")
434    ///     .post_only(true)
435    ///     .send()
436    ///     .await?;
437    /// # Ok(())
438    /// # }
439    /// ```
440    pub fn limit_order_gtc(&self) -> LimitOrderGtcBuilder<'_> {
441        LimitOrderGtcBuilder::new(self)
442    }
443
444    /// Create a limit order (GTD) builder.
445    ///
446    /// # Example
447    ///
448    /// ```no_run
449    /// # use coinbase_advanced::{RestClient, Credentials};
450    /// # async fn example() -> coinbase_advanced::Result<()> {
451    /// let client = RestClient::builder()
452    ///     .credentials(Credentials::from_env()?)
453    ///     .build()?;
454    ///
455    /// let response = client.limit_order_gtd()
456    ///     .buy("BTC-USD")
457    ///     .base_size("0.001")
458    ///     .limit_price("50000.00")
459    ///     .end_time("2024-12-31T23:59:59Z")
460    ///     .send()
461    ///     .await?;
462    /// # Ok(())
463    /// # }
464    /// ```
465    pub fn limit_order_gtd(&self) -> LimitOrderGtdBuilder<'_> {
466        LimitOrderGtdBuilder::new(self)
467    }
468
469    /// Create a stop-limit order (GTC) builder.
470    ///
471    /// # Example
472    ///
473    /// ```no_run
474    /// # use coinbase_advanced::{RestClient, Credentials, models::StopDirection};
475    /// # async fn example() -> coinbase_advanced::Result<()> {
476    /// let client = RestClient::builder()
477    ///     .credentials(Credentials::from_env()?)
478    ///     .build()?;
479    ///
480    /// let response = client.stop_limit_order_gtc()
481    ///     .sell("BTC-USD")
482    ///     .base_size("0.001")
483    ///     .limit_price("49000.00")
484    ///     .stop_price("50000.00")
485    ///     .stop_direction(StopDirection::StopDirectionStopDown)
486    ///     .send()
487    ///     .await?;
488    /// # Ok(())
489    /// # }
490    /// ```
491    pub fn stop_limit_order_gtc(&self) -> StopLimitOrderGtcBuilder<'_> {
492        StopLimitOrderGtcBuilder::new(self)
493    }
494}