1use 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#[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 #[serde(skip_serializing_if = "std::ops::Not::not")]
62 pub reduce_only: bool,
63}
64
65#[derive(Debug, Clone, Serialize)]
71pub struct CancelOrdersRequest {
72 pub order_ids: Vec<String>,
73}
74
75#[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#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct MarketIoc {
151 pub market_market_ioc: MarketParams,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct MarketFok {
157 pub market_market_fok: MarketParams,
158}
159
160#[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#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct LimitGtc {
184 pub limit_limit_gtc: LimitGtcParams,
185}
186
187#[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#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct LimitGtd {
207 pub limit_limit_gtd: LimitGtdParams,
208}
209
210#[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#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct LimitFok {
231 pub limit_limit_fok: LimitFokParams,
232}
233
234#[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#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct StopLimitGtc {
252 pub stop_limit_stop_limit_gtc: StopLimitGtcParams,
253}
254
255#[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#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct StopLimitGtd {
279 pub stop_limit_stop_limit_gtd: StopLimitGtdParams,
280}
281
282#[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}