1use 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#[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#[derive(Debug, Clone, Serialize)]
62pub struct CancelOrdersRequest {
63 pub order_ids: Vec<String>,
64}
65
66#[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#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct MarketIoc {
142 pub market_market_ioc: MarketParams,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct MarketFok {
148 pub market_market_fok: MarketParams,
149}
150
151#[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#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct LimitGtc {
175 pub limit_limit_gtc: LimitGtcParams,
176}
177
178#[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#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct LimitGtd {
198 pub limit_limit_gtd: LimitGtdParams,
199}
200
201#[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#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct LimitFok {
222 pub limit_limit_fok: LimitFokParams,
223}
224
225#[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#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct StopLimitGtc {
243 pub stop_limit_stop_limit_gtc: StopLimitGtcParams,
244}
245
246#[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#[derive(Debug, Clone, Serialize, Deserialize)]
269pub struct StopLimitGtd {
270 pub stop_limit_stop_limit_gtd: StopLimitGtdParams,
271}
272
273#[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}