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/marketdata/get-funding-slots>
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<String>,
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<String>,
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<String>,
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(("transaction_types", self.transaction_types.join(",")));
283        }
284
285        query.push(("start_timestamp_ns", self.start_timestamp_ns.to_string()));
286        query.push(("end_timestamp_ns", self.end_timestamp_ns.to_string()));
287
288        if let Some(account_id) = &self.account_id {
289            query.push(("account_id", account_id.clone()));
290        }
291
292        if let Some(cursor) = &self.cursor {
293            query.push(("cursor", cursor.clone()));
294        }
295
296        if let Some(limit) = self.limit {
297            query.push(("limit", limit.to_string()));
298        }
299
300        if let Some(sort_ts) = &self.sort_ts {
301            query.push(("sort_ts", sort_ts.clone()));
302        }
303
304        query.serialize(serializer)
305    }
306}
307
308impl GetTransactionsParams {
309    /// Creates a new [`GetTransactionsParams`].
310    #[must_use]
311    pub fn new(
312        transaction_types: Vec<String>,
313        start_timestamp_ns: i64,
314        end_timestamp_ns: i64,
315    ) -> Self {
316        Self {
317            transaction_types,
318            start_timestamp_ns,
319            end_timestamp_ns,
320            account_id: None,
321            cursor: None,
322            limit: None,
323            sort_ts: None,
324        }
325    }
326}
327
328/// Parameters for the GET /trades endpoint.
329///
330/// # References
331/// - <https://docs.architect.exchange/api-reference/market-data/get-trades>
332#[derive(Clone, Debug, Deserialize, Serialize)]
333pub struct GetTradesParams {
334    /// Instrument symbol, e.g. "BTC-PERP".
335    pub symbol: Ustr,
336    /// Maximum number of trades to return (max 100, default 10).
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub limit: Option<i32>,
339}
340
341impl GetTradesParams {
342    /// Creates a new [`GetTradesParams`].
343    #[must_use]
344    pub fn new(symbol: Ustr, limit: Option<i32>) -> Self {
345        Self { symbol, limit }
346    }
347}
348
349/// Parameters for the GET /book endpoint.
350///
351/// # References
352/// - <https://docs.architect.exchange/api-reference/market-data/get-book>
353#[derive(Clone, Debug, Deserialize, Serialize)]
354pub struct GetBookParams {
355    /// Instrument symbol, e.g. "BTC-PERP".
356    pub symbol: Ustr,
357    /// Book depth level: 2 (aggregated) or 3 (individual orders). Defaults to 2.
358    #[serde(skip_serializing_if = "Option::is_none")]
359    pub level: Option<i32>,
360}
361
362impl GetBookParams {
363    /// Creates a new [`GetBookParams`].
364    #[must_use]
365    pub fn new(symbol: Ustr, level: Option<i32>) -> Self {
366        Self { symbol, level }
367    }
368}
369
370/// Parameters for the GET /order-status endpoint.
371///
372/// Exactly one of `oid` or `cid` must be provided.
373///
374/// # References
375/// - <https://docs.architect.exchange/api-reference/order-management/get-order-status>
376#[derive(Clone, Debug, Deserialize, Serialize)]
377pub struct GetOrderStatusParams {
378    /// Order ID (e.g. "O-01ARZ3NDEKTSV4RRFFQ69G5FAV").
379    #[serde(rename = "oid", skip_serializing_if = "Option::is_none")]
380    pub order_id: Option<String>,
381    /// Client order ID (64-bit integer).
382    #[serde(rename = "cid", skip_serializing_if = "Option::is_none")]
383    pub client_order_id: Option<u64>,
384}
385
386impl GetOrderStatusParams {
387    /// Creates params to look up by venue order ID.
388    #[must_use]
389    pub fn by_order_id(order_id: impl Into<String>) -> Self {
390        Self {
391            order_id: Some(order_id.into()),
392            client_order_id: None,
393        }
394    }
395
396    /// Creates params to look up by client order ID.
397    #[must_use]
398    pub fn by_client_order_id(cid: u64) -> Self {
399        Self {
400            order_id: None,
401            client_order_id: Some(cid),
402        }
403    }
404}
405
406/// Parameters for the GET /open-orders endpoint.
407///
408/// # References
409/// - <https://docs.architect.exchange/api-reference/order-management/get-open-orders>
410#[derive(Clone, Debug, Default, Deserialize, Serialize)]
411pub struct GetOpenOrdersParams {
412    /// Optional account ID. AX uses the primary account when omitted.
413    #[serde(skip_serializing_if = "Option::is_none")]
414    pub account_id: Option<String>,
415    /// Maximum number of open orders to return.
416    #[serde(skip_serializing_if = "Option::is_none")]
417    pub limit: Option<i32>,
418    /// Number of sorted open orders to skip.
419    #[serde(skip_serializing_if = "Option::is_none")]
420    pub offset: Option<i32>,
421    /// Timestamp sort direction (`asc` or `desc`).
422    #[serde(skip_serializing_if = "Option::is_none")]
423    pub sort_ts: Option<String>,
424}
425
426impl GetOpenOrdersParams {
427    /// Creates a new empty [`GetOpenOrdersParams`].
428    #[must_use]
429    pub fn new() -> Self {
430        Self::default()
431    }
432}
433
434/// Parameters for the GET /orders endpoint.
435///
436/// # References
437/// - <https://docs.architect.exchange/api-reference/order-management/get-orders>
438#[derive(Clone, Debug, Default, Deserialize)]
439pub struct GetOrdersParams {
440    /// Filter by trading symbol.
441    pub symbol: Option<Ustr>,
442    /// Beginning of time range (ISO 8601).
443    pub start_time: Option<String>,
444    /// End of time range (ISO 8601).
445    pub end_time: Option<String>,
446    /// Start timestamp in nanoseconds.
447    pub start_timestamp_ns: Option<i64>,
448    /// End timestamp in nanoseconds.
449    pub end_timestamp_ns: Option<i64>,
450    /// Maximum results returned.
451    pub limit: Option<i32>,
452    /// Pagination offset.
453    pub offset: Option<i32>,
454    /// Filter by order state.
455    pub order_state: Option<AxOrderStatus>,
456    /// Filter by a single order ID.
457    pub order_id: Option<String>,
458    /// Filter by multiple order IDs.
459    pub order_ids: Vec<String>,
460    /// Filter by account ID.
461    pub account_id: Option<String>,
462    /// Cursor for the next page.
463    pub cursor: Option<String>,
464}
465
466impl Serialize for GetOrdersParams {
467    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
468    where
469        S: Serializer,
470    {
471        let mut query = Vec::new();
472
473        if let Some(symbol) = self.symbol {
474            query.push(("symbol", symbol.to_string()));
475        }
476
477        if let Some(start_time) = &self.start_time {
478            query.push(("start_time", start_time.clone()));
479        }
480
481        if let Some(end_time) = &self.end_time {
482            query.push(("end_time", end_time.clone()));
483        }
484
485        if let Some(start_timestamp_ns) = self.start_timestamp_ns {
486            query.push(("start_timestamp_ns", start_timestamp_ns.to_string()));
487        }
488
489        if let Some(end_timestamp_ns) = self.end_timestamp_ns {
490            query.push(("end_timestamp_ns", end_timestamp_ns.to_string()));
491        }
492
493        if let Some(limit) = self.limit {
494            query.push(("limit", limit.to_string()));
495        }
496
497        if let Some(offset) = self.offset {
498            query.push(("offset", offset.to_string()));
499        }
500
501        if let Some(order_state) = self.order_state {
502            query.push(("order_states", order_state.to_string()));
503        }
504
505        if let Some(order_id) = &self.order_id {
506            query.push(("order_id", order_id.clone()));
507        }
508
509        if !self.order_ids.is_empty() {
510            query.push(("order_ids", self.order_ids.join(",")));
511        }
512
513        if let Some(account_id) = &self.account_id {
514            query.push(("account_id", account_id.clone()));
515        }
516
517        if let Some(cursor) = &self.cursor {
518            query.push(("cursor", cursor.clone()));
519        }
520
521        query.serialize(serializer)
522    }
523}
524
525impl GetOrdersParams {
526    /// Creates a new empty [`GetOrdersParams`].
527    #[must_use]
528    pub fn new() -> Self {
529        Self::default()
530    }
531}
532
533#[cfg(test)]
534mod tests {
535    use rstest::rstest;
536    use ustr::Ustr;
537
538    use super::*;
539
540    #[rstest]
541    fn test_get_ticker_params_serialization() {
542        let params = GetTickerParams::new(Ustr::from("GBPUSD-PERP"));
543        let qs = serde_urlencoded::to_string(&params).unwrap();
544        assert_eq!(qs, "symbol=GBPUSD-PERP");
545    }
546
547    #[rstest]
548    fn test_get_tickers_params_serialization() {
549        let params = GetTickersParams {
550            limit: Some(50),
551            offset: Some(10),
552            sort: Some("symbol".to_string()),
553        };
554        let qs = serde_urlencoded::to_string(&params).unwrap();
555
556        assert!(qs.contains("limit=50"));
557        assert!(qs.contains("offset=10"));
558        assert!(qs.contains("sort=symbol"));
559    }
560
561    #[rstest]
562    fn test_get_instrument_params_serialization() {
563        let params = GetInstrumentParams::new(Ustr::from("EURUSD-PERP"));
564        let qs = serde_urlencoded::to_string(&params).unwrap();
565        assert_eq!(qs, "symbol=EURUSD-PERP");
566    }
567
568    #[rstest]
569    fn test_get_candles_params_serialization() {
570        let params = GetCandlesParams::new(
571            Ustr::from("GBPUSD-PERP"),
572            1000000000,
573            2000000000,
574            AxCandleWidth::Minutes1,
575        );
576        let qs = serde_urlencoded::to_string(&params).unwrap();
577        assert!(qs.contains("symbol=GBPUSD-PERP"));
578        assert!(qs.contains("start_timestamp_ns=1000000000"));
579        assert!(qs.contains("end_timestamp_ns=2000000000"));
580        assert!(qs.contains("candle_width=1m"));
581    }
582
583    #[rstest]
584    fn test_get_candle_params_serialization() {
585        let params = GetCandleParams::new(Ustr::from("GBPUSD-PERP"), AxCandleWidth::Hours1);
586        let qs = serde_urlencoded::to_string(&params).unwrap();
587        assert!(qs.contains("symbol=GBPUSD-PERP"));
588        assert!(qs.contains("candle_width=1h"));
589    }
590
591    #[rstest]
592    fn test_get_funding_rates_params_serialization() {
593        let mut params =
594            GetFundingRatesParams::new(Ustr::from("GBPUSD-PERP"), 1000000000, 2000000000);
595        params.cursor = Some("opaque+/=".to_string());
596        params.limit = Some(100);
597        params.sort_ts = Some("desc".to_string());
598        let qs = serde_urlencoded::to_string(&params).unwrap();
599        assert!(qs.contains("symbol=GBPUSD-PERP"));
600        assert!(qs.contains("start_timestamp_ns=1000000000"));
601        assert!(qs.contains("end_timestamp_ns=2000000000"));
602        assert!(qs.contains("cursor=opaque%2B%2F%3D"));
603        assert!(qs.contains("limit=100"));
604        assert!(qs.contains("sort_ts=desc"));
605    }
606
607    #[rstest]
608    fn test_get_funding_slots_params_serialization() {
609        let params = GetFundingSlotsParams::new(Ustr::from("GBPUSD-PERP"));
610        let qs = serde_urlencoded::to_string(&params).unwrap();
611        assert_eq!(qs, "symbol=GBPUSD-PERP");
612    }
613
614    #[rstest]
615    fn test_get_funding_slots_params_serialization_with_date() {
616        let mut params = GetFundingSlotsParams::new(Ustr::from("GBPUSD-PERP"));
617        params.date = Some("2026-07-06".to_string());
618        let qs = serde_urlencoded::to_string(&params).unwrap();
619        assert!(qs.contains("symbol=GBPUSD-PERP"));
620        assert!(qs.contains("date=2026-07-06"));
621    }
622
623    #[rstest]
624    fn test_get_fills_params_serialization() {
625        let mut params = GetFillsParams::new(1000000000, 2000000000);
626        params.account_id = Some("account-1".to_string());
627        params.symbol = Some(Ustr::from("GBPUSD-PERP"));
628        params.cursor = Some("opaque+/=".to_string());
629        params.limit = Some(100);
630        params.sort_ts = Some("desc".to_string());
631        let qs = serde_urlencoded::to_string(&params).unwrap();
632        assert!(qs.contains("start_timestamp_ns=1000000000"));
633        assert!(qs.contains("end_timestamp_ns=2000000000"));
634        assert!(qs.contains("account_id=account-1"));
635        assert!(qs.contains("symbol=GBPUSD-PERP"));
636        assert!(qs.contains("cursor=opaque%2B%2F%3D"));
637        assert!(qs.contains("limit=100"));
638        assert!(qs.contains("sort_ts=desc"));
639    }
640
641    #[rstest]
642    fn test_get_transactions_params_serialization() {
643        let mut params = GetTransactionsParams::new(
644            vec!["FUNDING".to_string(), "TRADE".to_string()],
645            1000000000,
646            2000000000,
647        );
648        params.account_id = Some("account-1".to_string());
649        params.cursor = Some("opaque+/=".to_string());
650        params.limit = Some(100);
651        params.sort_ts = Some("desc".to_string());
652        let qs = serde_urlencoded::to_string(&params).unwrap();
653
654        assert!(qs.contains("transaction_types=FUNDING%2CTRADE"));
655        assert!(qs.contains("start_timestamp_ns=1000000000"));
656        assert!(qs.contains("end_timestamp_ns=2000000000"));
657        assert!(qs.contains("account_id=account-1"));
658        assert!(qs.contains("cursor=opaque%2B%2F%3D"));
659        assert!(qs.contains("limit=100"));
660        assert!(qs.contains("sort_ts=desc"));
661    }
662
663    #[rstest]
664    fn test_get_trades_params_serialization() {
665        let params = GetTradesParams::new(Ustr::from("BTC-PERP"), Some(50));
666        let qs = serde_urlencoded::to_string(&params).unwrap();
667        assert!(qs.contains("symbol=BTC-PERP"));
668        assert!(qs.contains("limit=50"));
669    }
670
671    #[rstest]
672    fn test_get_trades_params_serialization_no_limit() {
673        let params = GetTradesParams::new(Ustr::from("BTC-PERP"), None);
674        let qs = serde_urlencoded::to_string(&params).unwrap();
675        assert_eq!(qs, "symbol=BTC-PERP");
676    }
677
678    #[rstest]
679    fn test_get_open_orders_params_serialization() {
680        let params = GetOpenOrdersParams {
681            account_id: Some("account-1".to_string()),
682            limit: Some(100),
683            offset: Some(200),
684            sort_ts: Some("desc".to_string()),
685        };
686        let qs = serde_urlencoded::to_string(&params).unwrap();
687
688        assert!(qs.contains("account_id=account-1"));
689        assert!(qs.contains("limit=100"));
690        assert!(qs.contains("offset=200"));
691        assert!(qs.contains("sort_ts=desc"));
692    }
693
694    #[rstest]
695    fn test_get_book_params_serialization() {
696        let params = GetBookParams::new(Ustr::from("EURUSD-PERP"), Some(3));
697        let qs = serde_urlencoded::to_string(&params).unwrap();
698        assert!(qs.contains("symbol=EURUSD-PERP"));
699        assert!(qs.contains("level=3"));
700    }
701
702    #[rstest]
703    fn test_get_order_status_by_order_id_serialization() {
704        let params = GetOrderStatusParams::by_order_id("O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
705        let qs = serde_urlencoded::to_string(&params).unwrap();
706        assert_eq!(qs, "oid=O-01ARZ3NDEKTSV4RRFFQ69G5FAV");
707    }
708
709    #[rstest]
710    fn test_get_order_status_by_client_order_id_serialization() {
711        let params = GetOrderStatusParams::by_client_order_id(12345);
712        let qs = serde_urlencoded::to_string(&params).unwrap();
713        assert_eq!(qs, "cid=12345");
714    }
715
716    #[rstest]
717    fn test_get_orders_params_serialization() {
718        let params = GetOrdersParams {
719            symbol: Some(Ustr::from("EURUSD-PERP")),
720            order_state: Some(AxOrderStatus::Filled),
721            order_id: Some("ORD-1".to_string()),
722            order_ids: vec!["ORD-2".to_string(), "ORD-3".to_string()],
723            account_id: Some("account-1".to_string()),
724            cursor: Some("next".to_string()),
725            start_timestamp_ns: Some(1000000000),
726            end_timestamp_ns: Some(2000000000),
727            limit: Some(100),
728            ..Default::default()
729        };
730        let qs = serde_urlencoded::to_string(&params).unwrap();
731
732        assert!(qs.contains("symbol=EURUSD-PERP"));
733        assert!(qs.contains("order_states=FILLED"));
734        assert!(qs.contains("order_id=ORD-1"));
735        assert!(qs.contains("order_ids=ORD-2%2CORD-3"));
736        assert!(qs.contains("account_id=account-1"));
737        assert!(qs.contains("cursor=next"));
738        assert!(qs.contains("start_timestamp_ns=1000000000"));
739        assert!(qs.contains("end_timestamp_ns=2000000000"));
740        assert!(qs.contains("limit=100"));
741    }
742}