Skip to main content

nautilus_architect_ax/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//! Request parameter structures for the Ax REST API.
17//!
18//! Each struct corresponds to an Ax REST endpoint and is annotated
19//! using `serde` so that it can be serialized directly into the query string
20//! or request body expected by the exchange.
21//!
22//! Parameter structs are built using the builder pattern and then passed to
23//! `AxRawHttpClient` methods where they are automatically serialized.
24
25use serde::{Deserialize, Serialize, Serializer};
26use ustr::Ustr;
27
28use crate::common::enums::{AxCandleWidth, AxOrderStatus};
29
30/// Parameters for the GET /tickers endpoint.
31///
32/// # References
33/// - <https://docs.architect.exchange/api-reference/marketdata/get-tickers>
34#[derive(Clone, Debug, Default, Deserialize, Serialize)]
35pub struct GetTickersParams {
36    /// Maximum number of tickers to return.
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub limit: Option<i32>,
39    /// Number of sorted tickers to skip.
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub offset: Option<i32>,
42    /// Sort order. Currently AX supports `symbol`.
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub sort: Option<String>,
45}
46
47impl GetTickersParams {
48    /// Creates a new empty [`GetTickersParams`].
49    #[must_use]
50    pub fn new() -> Self {
51        Self::default()
52    }
53}
54
55/// Parameters for the GET /ticker endpoint.
56///
57/// # References
58/// - <https://docs.architect.exchange/api-reference/marketdata/get-ticker>
59#[derive(Clone, Debug, Deserialize, Serialize)]
60pub struct GetTickerParams {
61    /// Instrument symbol, e.g. "GBPUSD-PERP", "EURUSD-PERP".
62    pub symbol: Ustr,
63}
64
65impl GetTickerParams {
66    /// Creates a new [`GetTickerParams`] with the given symbol.
67    #[must_use]
68    pub fn new(symbol: Ustr) -> Self {
69        Self { symbol }
70    }
71}
72
73/// Parameters for the GET /instrument endpoint.
74///
75/// # References
76/// - <https://docs.architect.exchange/api-reference/symbols-instruments/get-instrument>
77#[derive(Clone, Debug, Deserialize, Serialize)]
78pub struct GetInstrumentParams {
79    /// Instrument symbol, e.g. "GBPUSD-PERP", "EURUSD-PERP".
80    pub symbol: Ustr,
81}
82
83impl GetInstrumentParams {
84    /// Creates a new [`GetInstrumentParams`] with the given symbol.
85    #[must_use]
86    pub fn new(symbol: Ustr) -> Self {
87        Self { symbol }
88    }
89}
90
91/// Parameters for the GET /candles endpoint.
92///
93/// # References
94/// - <https://docs.architect.exchange/api-reference/marketdata/get-candles>
95#[derive(Clone, Debug, Deserialize, Serialize)]
96pub struct GetCandlesParams {
97    /// Instrument symbol.
98    pub symbol: Ustr,
99    /// Start timestamp in nanoseconds.
100    pub start_timestamp_ns: i64,
101    /// End timestamp in nanoseconds.
102    pub end_timestamp_ns: i64,
103    /// Candle width/interval.
104    pub candle_width: AxCandleWidth,
105}
106
107impl GetCandlesParams {
108    /// Creates a new [`GetCandlesParams`].
109    #[must_use]
110    pub fn new(
111        symbol: Ustr,
112        start_timestamp_ns: i64,
113        end_timestamp_ns: i64,
114        candle_width: AxCandleWidth,
115    ) -> Self {
116        Self {
117            symbol,
118            start_timestamp_ns,
119            end_timestamp_ns,
120            candle_width,
121        }
122    }
123}
124
125/// Parameters for the GET /candles/current and GET /candles/last endpoints.
126///
127/// # References
128/// - <https://docs.architect.exchange/api-reference/marketdata/get-current-candle>
129/// - <https://docs.architect.exchange/api-reference/marketdata/get-last-candle>
130#[derive(Clone, Debug, Deserialize, Serialize)]
131pub struct GetCandleParams {
132    /// Instrument symbol.
133    pub symbol: Ustr,
134    /// Candle width/interval.
135    pub candle_width: AxCandleWidth,
136}
137
138impl GetCandleParams {
139    /// Creates a new [`GetCandleParams`].
140    #[must_use]
141    pub fn new(symbol: Ustr, candle_width: AxCandleWidth) -> Self {
142        Self {
143            symbol,
144            candle_width,
145        }
146    }
147}
148
149/// Parameters for the GET /funding-rates endpoint.
150///
151/// # References
152/// - <https://docs.architect.exchange/api-reference/marketdata/get-funding-rates>
153#[derive(Clone, Debug, Deserialize, Serialize)]
154pub struct GetFundingRatesParams {
155    /// Instrument symbol.
156    pub symbol: Ustr,
157    /// Start timestamp in nanoseconds.
158    pub start_timestamp_ns: i64,
159    /// End timestamp in nanoseconds.
160    pub end_timestamp_ns: i64,
161    /// Cursor for the next page.
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub cursor: Option<String>,
164    /// Maximum number of records to return.
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub limit: Option<i32>,
167    /// Timestamp sort direction (`asc` or `desc`).
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub sort_ts: Option<String>,
170}
171
172impl GetFundingRatesParams {
173    /// Creates a new [`GetFundingRatesParams`].
174    #[must_use]
175    pub fn new(symbol: Ustr, start_timestamp_ns: i64, end_timestamp_ns: i64) -> Self {
176        Self {
177            symbol,
178            start_timestamp_ns,
179            end_timestamp_ns,
180            cursor: None,
181            limit: None,
182            sort_ts: None,
183        }
184    }
185}
186
187/// Parameters for the GET /funding-slots endpoint.
188///
189/// # References
190/// - <https://docs.architect.exchange/api-reference>
191#[derive(Clone, Debug, Deserialize, Serialize)]
192pub struct GetFundingSlotsParams {
193    /// Instrument symbol.
194    pub symbol: Ustr,
195    /// Trading day (`YYYY-MM-DD`) in the symbol's funding-schedule timezone. AX defaults to the
196    /// current date when omitted.
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub date: Option<String>,
199}
200
201impl GetFundingSlotsParams {
202    /// Creates a new [`GetFundingSlotsParams`] with the given symbol and no date filter.
203    #[must_use]
204    pub fn new(symbol: Ustr) -> Self {
205        Self { symbol, date: None }
206    }
207}
208
209/// Parameters for the GET /fills endpoint.
210///
211/// # References
212/// - <https://docs.architect.exchange/api-reference/order-management/get-order-fills>
213#[derive(Clone, Debug, Deserialize, Serialize)]
214pub struct GetFillsParams {
215    /// Start timestamp in nanoseconds.
216    pub start_timestamp_ns: i64,
217    /// End timestamp in nanoseconds.
218    pub end_timestamp_ns: i64,
219    /// Optional account ID. AX uses the primary account when omitted.
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub account_id: Option<Ustr>,
222    /// Optional symbol filter.
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub symbol: Option<Ustr>,
225    /// Cursor for the next page.
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub cursor: Option<String>,
228    /// Maximum number of records to return.
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub limit: Option<i32>,
231    /// Timestamp sort direction (`asc` or `desc`).
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub sort_ts: Option<String>,
234}
235
236impl GetFillsParams {
237    /// Creates a new [`GetFillsParams`].
238    #[must_use]
239    pub fn new(start_timestamp_ns: i64, end_timestamp_ns: i64) -> Self {
240        Self {
241            start_timestamp_ns,
242            end_timestamp_ns,
243            account_id: None,
244            symbol: None,
245            cursor: None,
246            limit: None,
247            sort_ts: None,
248        }
249    }
250}
251
252/// Parameters for the GET /transactions endpoint.
253///
254/// # References
255/// - <https://docs.architect.exchange/api-reference/portfolio-management/get-transactions>
256#[derive(Clone, Debug, Deserialize)]
257pub struct GetTransactionsParams {
258    /// Transaction types to filter by.
259    pub transaction_types: Vec<Ustr>,
260    /// Start timestamp in nanoseconds.
261    pub start_timestamp_ns: i64,
262    /// End timestamp in nanoseconds.
263    pub end_timestamp_ns: i64,
264    /// Optional account ID. AX uses the primary account when omitted.
265    pub account_id: Option<Ustr>,
266    /// Cursor for the next page.
267    pub cursor: Option<String>,
268    /// Maximum number of records to return.
269    pub limit: Option<i32>,
270    /// Timestamp sort direction (`asc` or `desc`).
271    pub sort_ts: Option<String>,
272}
273
274impl Serialize for GetTransactionsParams {
275    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
276    where
277        S: Serializer,
278    {
279        let mut query = Vec::with_capacity(7);
280
281        if !self.transaction_types.is_empty() {
282            query.push((
283                "transaction_types",
284                self.transaction_types
285                    .iter()
286                    .map(Ustr::as_str)
287                    .collect::<Vec<_>>()
288                    .join(","),
289            ));
290        }
291
292        query.push(("start_timestamp_ns", self.start_timestamp_ns.to_string()));
293        query.push(("end_timestamp_ns", self.end_timestamp_ns.to_string()));
294
295        if let Some(account_id) = &self.account_id {
296            query.push(("account_id", account_id.to_string()));
297        }
298
299        if let Some(cursor) = &self.cursor {
300            query.push(("cursor", cursor.clone()));
301        }
302
303        if let Some(limit) = self.limit {
304            query.push(("limit", limit.to_string()));
305        }
306
307        if let Some(sort_ts) = &self.sort_ts {
308            query.push(("sort_ts", sort_ts.clone()));
309        }
310
311        query.serialize(serializer)
312    }
313}
314
315impl GetTransactionsParams {
316    /// Creates a new [`GetTransactionsParams`].
317    #[must_use]
318    pub fn new(
319        transaction_types: Vec<Ustr>,
320        start_timestamp_ns: i64,
321        end_timestamp_ns: i64,
322    ) -> Self {
323        Self {
324            transaction_types,
325            start_timestamp_ns,
326            end_timestamp_ns,
327            account_id: None,
328            cursor: None,
329            limit: None,
330            sort_ts: None,
331        }
332    }
333}
334
335/// Parameters for the GET /trades endpoint.
336///
337/// # References
338/// - <https://docs.architect.exchange/api-reference/marketdata/get-trades>
339#[derive(Clone, Debug, Deserialize, Serialize)]
340pub struct GetTradesParams {
341    /// Instrument symbol, e.g. "BTC-PERP".
342    pub symbol: Ustr,
343    /// Maximum number of trades to return (max 100, default 10).
344    #[serde(skip_serializing_if = "Option::is_none")]
345    pub limit: Option<i32>,
346}
347
348impl GetTradesParams {
349    /// Creates a new [`GetTradesParams`].
350    #[must_use]
351    pub fn new(symbol: Ustr, limit: Option<i32>) -> Self {
352        Self { symbol, limit }
353    }
354}
355
356/// Parameters for the GET /book endpoint.
357///
358/// # References
359/// - <https://docs.architect.exchange/api-reference/marketdata/get-book>
360#[derive(Clone, Debug, Deserialize, Serialize)]
361pub struct GetBookParams {
362    /// Instrument symbol, e.g. "BTC-PERP".
363    pub symbol: Ustr,
364    /// Book depth level: 2 (aggregated) or 3 (individual orders). Defaults to 2.
365    #[serde(skip_serializing_if = "Option::is_none")]
366    pub level: Option<i32>,
367}
368
369impl GetBookParams {
370    /// Creates a new [`GetBookParams`].
371    #[must_use]
372    pub fn new(symbol: Ustr, level: Option<i32>) -> Self {
373        Self { symbol, level }
374    }
375}
376
377/// Parameters for the GET /order-status endpoint.
378///
379/// Exactly one of `oid` or `cid` must be provided.
380///
381/// # References
382/// - <https://docs.architect.exchange/api-reference/order-management/get-order-status>
383#[derive(Clone, Debug, Deserialize, Serialize)]
384pub struct GetOrderStatusParams {
385    /// Order ID (e.g. "O-01ARZ3NDEKTSV4RRFFQ69G5FAV").
386    #[serde(rename = "oid", skip_serializing_if = "Option::is_none")]
387    pub order_id: Option<String>,
388    /// Client order ID (64-bit integer).
389    #[serde(rename = "cid", skip_serializing_if = "Option::is_none")]
390    pub client_order_id: Option<u64>,
391}
392
393impl GetOrderStatusParams {
394    /// Creates params to look up by venue order ID.
395    #[must_use]
396    pub fn by_order_id(order_id: impl Into<String>) -> Self {
397        Self {
398            order_id: Some(order_id.into()),
399            client_order_id: None,
400        }
401    }
402
403    /// Creates params to look up by client order ID.
404    #[must_use]
405    pub fn by_client_order_id(cid: u64) -> Self {
406        Self {
407            order_id: None,
408            client_order_id: Some(cid),
409        }
410    }
411}
412
413/// Parameters for the GET /open-orders endpoint.
414///
415/// # References
416/// - <https://docs.architect.exchange/api-reference/order-management/get-open-orders>
417#[derive(Clone, Debug, Default, Deserialize, Serialize)]
418pub struct GetOpenOrdersParams {
419    /// Optional account ID. AX uses the primary account when omitted.
420    #[serde(skip_serializing_if = "Option::is_none")]
421    pub account_id: Option<Ustr>,
422    /// Maximum number of open orders to return.
423    #[serde(skip_serializing_if = "Option::is_none")]
424    pub limit: Option<i32>,
425    /// Number of sorted open orders to skip.
426    #[serde(skip_serializing_if = "Option::is_none")]
427    pub offset: Option<i32>,
428    /// Timestamp sort direction (`asc` or `desc`).
429    #[serde(skip_serializing_if = "Option::is_none")]
430    pub sort_ts: Option<String>,
431}
432
433impl GetOpenOrdersParams {
434    /// Creates a new empty [`GetOpenOrdersParams`].
435    #[must_use]
436    pub fn new() -> Self {
437        Self::default()
438    }
439}
440
441/// Parameters for the GET /orders endpoint.
442///
443/// # References
444/// - <https://docs.architect.exchange/api-reference/order-management/get-orders>
445#[derive(Clone, Debug, Default, Deserialize)]
446pub struct GetOrdersParams {
447    /// Filter by trading symbol.
448    pub symbol: Option<Ustr>,
449    /// Beginning of time range (ISO 8601).
450    pub start_time: Option<String>,
451    /// End of time range (ISO 8601).
452    pub end_time: Option<String>,
453    /// Start timestamp in nanoseconds.
454    pub start_timestamp_ns: Option<i64>,
455    /// End timestamp in nanoseconds.
456    pub end_timestamp_ns: Option<i64>,
457    /// Maximum results returned.
458    pub limit: Option<i32>,
459    /// Pagination offset.
460    pub offset: Option<i32>,
461    /// Filter by order state.
462    pub order_state: Option<AxOrderStatus>,
463    /// Filter by a single order ID.
464    pub order_id: Option<String>,
465    /// Filter by multiple order IDs.
466    pub order_ids: Vec<String>,
467    /// Filter by account ID.
468    pub account_id: Option<Ustr>,
469    /// Cursor for the next page.
470    pub cursor: Option<String>,
471}
472
473impl Serialize for GetOrdersParams {
474    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
475    where
476        S: Serializer,
477    {
478        let mut query = Vec::new();
479
480        if let Some(symbol) = self.symbol {
481            query.push(("symbol", symbol.to_string()));
482        }
483
484        if let Some(start_time) = &self.start_time {
485            query.push(("start_time", start_time.clone()));
486        }
487
488        if let Some(end_time) = &self.end_time {
489            query.push(("end_time", end_time.clone()));
490        }
491
492        if let Some(start_timestamp_ns) = self.start_timestamp_ns {
493            query.push(("start_timestamp_ns", start_timestamp_ns.to_string()));
494        }
495
496        if let Some(end_timestamp_ns) = self.end_timestamp_ns {
497            query.push(("end_timestamp_ns", end_timestamp_ns.to_string()));
498        }
499
500        if let Some(limit) = self.limit {
501            query.push(("limit", limit.to_string()));
502        }
503
504        if let Some(offset) = self.offset {
505            query.push(("offset", offset.to_string()));
506        }
507
508        if let Some(order_state) = self.order_state {
509            query.push(("order_states", order_state.to_string()));
510        }
511
512        if let Some(order_id) = &self.order_id {
513            query.push(("order_id", order_id.clone()));
514        }
515
516        if !self.order_ids.is_empty() {
517            query.push(("order_ids", self.order_ids.join(",")));
518        }
519
520        if let Some(account_id) = &self.account_id {
521            query.push(("account_id", account_id.to_string()));
522        }
523
524        if let Some(cursor) = &self.cursor {
525            query.push(("cursor", cursor.clone()));
526        }
527
528        query.serialize(serializer)
529    }
530}
531
532impl GetOrdersParams {
533    /// Creates a new empty [`GetOrdersParams`].
534    #[must_use]
535    pub fn new() -> Self {
536        Self::default()
537    }
538}
539
540#[cfg(test)]
541mod tests {
542    use rstest::rstest;
543    use ustr::Ustr;
544
545    use super::*;
546
547    #[rstest]
548    fn test_get_ticker_params_serialization() {
549        let params = GetTickerParams::new(Ustr::from("GBPUSD-PERP"));
550        let qs = serde_urlencoded::to_string(&params).unwrap();
551        assert_eq!(qs, "symbol=GBPUSD-PERP");
552    }
553
554    #[rstest]
555    fn test_get_tickers_params_serialization() {
556        let params = GetTickersParams {
557            limit: Some(50),
558            offset: Some(10),
559            sort: Some("symbol".to_string()),
560        };
561        let qs = serde_urlencoded::to_string(&params).unwrap();
562
563        assert!(qs.contains("limit=50"));
564        assert!(qs.contains("offset=10"));
565        assert!(qs.contains("sort=symbol"));
566    }
567
568    #[rstest]
569    fn test_get_instrument_params_serialization() {
570        let params = GetInstrumentParams::new(Ustr::from("EURUSD-PERP"));
571        let qs = serde_urlencoded::to_string(&params).unwrap();
572        assert_eq!(qs, "symbol=EURUSD-PERP");
573    }
574
575    #[rstest]
576    fn test_get_candles_params_serialization() {
577        let params = GetCandlesParams::new(
578            Ustr::from("GBPUSD-PERP"),
579            1000000000,
580            2000000000,
581            AxCandleWidth::Minutes1,
582        );
583        let qs = serde_urlencoded::to_string(&params).unwrap();
584        assert!(qs.contains("symbol=GBPUSD-PERP"));
585        assert!(qs.contains("start_timestamp_ns=1000000000"));
586        assert!(qs.contains("end_timestamp_ns=2000000000"));
587        assert!(qs.contains("candle_width=1m"));
588    }
589
590    #[rstest]
591    fn test_get_candle_params_serialization() {
592        let params = GetCandleParams::new(Ustr::from("GBPUSD-PERP"), AxCandleWidth::Hours1);
593        let qs = serde_urlencoded::to_string(&params).unwrap();
594        assert!(qs.contains("symbol=GBPUSD-PERP"));
595        assert!(qs.contains("candle_width=1h"));
596    }
597
598    #[rstest]
599    fn test_get_funding_rates_params_serialization() {
600        let mut params =
601            GetFundingRatesParams::new(Ustr::from("GBPUSD-PERP"), 1000000000, 2000000000);
602        params.cursor = Some("opaque+/=".to_string());
603        params.limit = Some(100);
604        params.sort_ts = Some("desc".to_string());
605        let qs = serde_urlencoded::to_string(&params).unwrap();
606        assert!(qs.contains("symbol=GBPUSD-PERP"));
607        assert!(qs.contains("start_timestamp_ns=1000000000"));
608        assert!(qs.contains("end_timestamp_ns=2000000000"));
609        assert!(qs.contains("cursor=opaque%2B%2F%3D"));
610        assert!(qs.contains("limit=100"));
611        assert!(qs.contains("sort_ts=desc"));
612    }
613
614    #[rstest]
615    fn test_get_funding_slots_params_serialization() {
616        let params = GetFundingSlotsParams::new(Ustr::from("GBPUSD-PERP"));
617        let qs = serde_urlencoded::to_string(&params).unwrap();
618        assert_eq!(qs, "symbol=GBPUSD-PERP");
619    }
620
621    #[rstest]
622    fn test_get_funding_slots_params_serialization_with_date() {
623        let mut params = GetFundingSlotsParams::new(Ustr::from("GBPUSD-PERP"));
624        params.date = Some("2026-07-06".to_string());
625        let qs = serde_urlencoded::to_string(&params).unwrap();
626        assert!(qs.contains("symbol=GBPUSD-PERP"));
627        assert!(qs.contains("date=2026-07-06"));
628    }
629
630    #[rstest]
631    fn test_get_fills_params_serialization() {
632        let mut params = GetFillsParams::new(1000000000, 2000000000);
633        params.account_id = Some(Ustr::from("account-1"));
634        params.symbol = Some(Ustr::from("GBPUSD-PERP"));
635        params.cursor = Some("opaque+/=".to_string());
636        params.limit = Some(100);
637        params.sort_ts = Some("desc".to_string());
638        let qs = serde_urlencoded::to_string(&params).unwrap();
639        assert!(qs.contains("start_timestamp_ns=1000000000"));
640        assert!(qs.contains("end_timestamp_ns=2000000000"));
641        assert!(qs.contains("account_id=account-1"));
642        assert!(qs.contains("symbol=GBPUSD-PERP"));
643        assert!(qs.contains("cursor=opaque%2B%2F%3D"));
644        assert!(qs.contains("limit=100"));
645        assert!(qs.contains("sort_ts=desc"));
646    }
647
648    #[rstest]
649    fn test_get_transactions_params_serialization() {
650        let mut params = GetTransactionsParams::new(
651            vec![Ustr::from("FUNDING"), Ustr::from("TRADE")],
652            1000000000,
653            2000000000,
654        );
655        params.account_id = Some(Ustr::from("account-1"));
656        params.cursor = Some("opaque+/=".to_string());
657        params.limit = Some(100);
658        params.sort_ts = Some("desc".to_string());
659        let qs = serde_urlencoded::to_string(&params).unwrap();
660
661        assert!(qs.contains("transaction_types=FUNDING%2CTRADE"));
662        assert!(qs.contains("start_timestamp_ns=1000000000"));
663        assert!(qs.contains("end_timestamp_ns=2000000000"));
664        assert!(qs.contains("account_id=account-1"));
665        assert!(qs.contains("cursor=opaque%2B%2F%3D"));
666        assert!(qs.contains("limit=100"));
667        assert!(qs.contains("sort_ts=desc"));
668    }
669
670    #[rstest]
671    fn test_get_trades_params_serialization() {
672        let params = GetTradesParams::new(Ustr::from("BTC-PERP"), Some(50));
673        let qs = serde_urlencoded::to_string(&params).unwrap();
674        assert!(qs.contains("symbol=BTC-PERP"));
675        assert!(qs.contains("limit=50"));
676    }
677
678    #[rstest]
679    fn test_get_trades_params_serialization_no_limit() {
680        let params = GetTradesParams::new(Ustr::from("BTC-PERP"), None);
681        let qs = serde_urlencoded::to_string(&params).unwrap();
682        assert_eq!(qs, "symbol=BTC-PERP");
683    }
684
685    #[rstest]
686    fn test_get_open_orders_params_serialization() {
687        let params = GetOpenOrdersParams {
688            account_id: Some(Ustr::from("account-1")),
689            limit: Some(100),
690            offset: Some(200),
691            sort_ts: Some("desc".to_string()),
692        };
693        let qs = serde_urlencoded::to_string(&params).unwrap();
694
695        assert!(qs.contains("account_id=account-1"));
696        assert!(qs.contains("limit=100"));
697        assert!(qs.contains("offset=200"));
698        assert!(qs.contains("sort_ts=desc"));
699    }
700
701    #[rstest]
702    fn test_get_book_params_serialization() {
703        let params = GetBookParams::new(Ustr::from("EURUSD-PERP"), Some(3));
704        let qs = serde_urlencoded::to_string(&params).unwrap();
705        assert!(qs.contains("symbol=EURUSD-PERP"));
706        assert!(qs.contains("level=3"));
707    }
708
709    #[rstest]
710    fn test_get_order_status_by_order_id_serialization() {
711        let params = GetOrderStatusParams::by_order_id("O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
712        let qs = serde_urlencoded::to_string(&params).unwrap();
713        assert_eq!(qs, "oid=O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
714    }
715
716    #[rstest]
717    fn test_get_order_status_by_client_order_id_serialization() {
718        let params = GetOrderStatusParams::by_client_order_id(12345);
719        let qs = serde_urlencoded::to_string(&params).unwrap();
720        assert_eq!(qs, "cid=12345");
721    }
722
723    #[rstest]
724    fn test_get_orders_params_serialization() {
725        let params = GetOrdersParams {
726            symbol: Some(Ustr::from("EURUSD-PERP")),
727            order_state: Some(AxOrderStatus::Filled),
728            order_id: Some("ORD-1".to_string()),
729            order_ids: vec!["ORD-2".to_string(), "ORD-3".to_string()],
730            account_id: Some(Ustr::from("account-1")),
731            cursor: Some("next".to_string()),
732            start_timestamp_ns: Some(1000000000),
733            end_timestamp_ns: Some(2000000000),
734            limit: Some(100),
735            ..Default::default()
736        };
737        let qs = serde_urlencoded::to_string(&params).unwrap();
738
739        assert!(qs.contains("symbol=EURUSD-PERP"));
740        assert!(qs.contains("order_states=FILLED"));
741        assert!(qs.contains("order_id=ORD-1"));
742        assert!(qs.contains("order_ids=ORD-2%2CORD-3"));
743        assert!(qs.contains("account_id=account-1"));
744        assert!(qs.contains("cursor=next"));
745        assert!(qs.contains("start_timestamp_ns=1000000000"));
746        assert!(qs.contains("end_timestamp_ns=2000000000"));
747        assert!(qs.contains("limit=100"));
748    }
749}