nautilus_dydx/grpc/
order.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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//! Order types and builders for dYdX v4.
17//!
18//! This module provides order construction utilities for placing orders on dYdX v4.
19//! dYdX supports two order lifetime types:
20//!
21//! - **Short-term orders**: Expire by block height (max 20 blocks).
22//! - **Long-term orders**: Expire by timestamp.
23//!
24//! See [dYdX order types](https://help.dydx.trade/en/articles/166985-short-term-vs-long-term-order-types).
25
26#[cfg(test)]
27use chrono::Duration;
28use chrono::{DateTime, Utc};
29use nautilus_model::enums::OrderType;
30use rust_decimal::{Decimal, prelude::ToPrimitive};
31
32use crate::proto::dydxprotocol::{
33    clob::{
34        Order, OrderId,
35        order::{ConditionType, GoodTilOneof, Side as OrderSide, TimeInForce as OrderTimeInForce},
36    },
37    subaccounts::SubaccountId,
38};
39
40/// Maximum short-term order lifetime in blocks.
41///
42/// See also [short-term vs long-term orders](https://help.dydx.trade/en/articles/166985-short-term-vs-long-term-order-types).
43pub const SHORT_TERM_ORDER_MAXIMUM_LIFETIME: u32 = 20;
44
45/// Value used to identify the Rust client in order metadata.
46pub const DEFAULT_RUST_CLIENT_METADATA: u32 = 4;
47
48/// Order [expiration types](https://docs.dydx.xyz/concepts/trading/orders#comparison).
49#[derive(Clone, Debug)]
50pub enum OrderGoodUntil {
51    /// Block expiration is used for short-term orders.
52    /// The order expires after the specified block height.
53    Block(u32),
54    /// Time expiration is used for long-term orders.
55    /// The order expires at the specified timestamp.
56    Time(DateTime<Utc>),
57}
58
59/// Order flags indicating order lifetime and execution type.
60///
61/// See <https://docs.dydx.xyz/concepts/trading/orders#short-term-vs-long-term> for details
62/// on short-term vs long-term (stateful) orders.
63#[derive(Clone, Debug)]
64pub enum OrderFlags {
65    /// Short-term order (expires by block height).
66    ShortTerm,
67    /// Long-term order (expires by timestamp).
68    LongTerm,
69    /// Conditional order (triggered by trigger price).
70    ///
71    /// Conditional orders include Stop Market, Stop Limit, Take Profit Market, and Take Profit Limit.
72    /// See <https://docs.dydx.xyz/concepts/trading/orders#types> for details.
73    Conditional,
74}
75
76/// Market parameters required for price and size quantizations.
77///
78/// These quantizations are required for `Order` placement.
79/// See also [how to interpret block data for trades](https://docs.dydx.exchange/api_integration-guides/how_to_interpret_block_data_for_trades).
80#[derive(Clone, Debug)]
81pub struct OrderMarketParams {
82    /// Atomic resolution.
83    pub atomic_resolution: i32,
84    /// CLOB pair ID.
85    pub clob_pair_id: u32,
86    /// Oracle price.
87    pub oracle_price: Option<Decimal>,
88    /// Quantum conversion exponent.
89    pub quantum_conversion_exponent: i32,
90    /// Step base quantums.
91    pub step_base_quantums: u64,
92    /// Subticks per tick.
93    pub subticks_per_tick: u32,
94}
95
96impl OrderMarketParams {
97    /// Convert price into subticks.
98    ///
99    /// # Errors
100    ///
101    /// Returns an error if conversion fails.
102    pub fn quantize_price(&self, price: Decimal) -> Result<u64, anyhow::Error> {
103        const QUOTE_QUANTUMS_ATOMIC_RESOLUTION: i32 = -6;
104        let exponent = -(self.atomic_resolution
105            - self.quantum_conversion_exponent
106            - QUOTE_QUANTUMS_ATOMIC_RESOLUTION);
107
108        // When exponent is negative, we multiply by 10^|exponent|
109        // When exponent is positive, we divide by 10^exponent (multiply by 10^-exponent)
110        let factor = if exponent < 0 {
111            Decimal::from(10_i64.pow(exponent.unsigned_abs()))
112        } else {
113            Decimal::new(1, exponent.unsigned_abs())
114        };
115
116        let raw_subticks = price * factor;
117        let subticks_per_tick = Decimal::from(self.subticks_per_tick);
118        let quantums = Self::quantize(&raw_subticks, &subticks_per_tick);
119        let result = quantums.max(subticks_per_tick);
120
121        result
122            .to_u64()
123            .ok_or_else(|| anyhow::anyhow!("Failed to convert price to u64"))
124    }
125
126    /// Convert decimal into quantums.
127    ///
128    /// # Errors
129    ///
130    /// Returns an error if conversion fails.
131    pub fn quantize_quantity(&self, quantity: Decimal) -> Result<u64, anyhow::Error> {
132        // When atomic_resolution is negative, we multiply by 10^|atomic_resolution|
133        // When atomic_resolution is positive, we divide by 10^atomic_resolution
134        let factor = if self.atomic_resolution < 0 {
135            Decimal::from(10_i64.pow(self.atomic_resolution.unsigned_abs()))
136        } else {
137            Decimal::new(1, self.atomic_resolution.unsigned_abs())
138        };
139
140        let raw_quantums = quantity * factor;
141        let step_base_quantums = Decimal::from(self.step_base_quantums);
142        let quantums = Self::quantize(&raw_quantums, &step_base_quantums);
143        let result = quantums.max(step_base_quantums);
144
145        result
146            .to_u64()
147            .ok_or_else(|| anyhow::anyhow!("Failed to convert quantity to u64"))
148    }
149
150    /// A `round`-like function that quantizes a `value` to the `fraction`.
151    fn quantize(value: &Decimal, fraction: &Decimal) -> Decimal {
152        (value / fraction).round() * fraction
153    }
154
155    /// Get orderbook pair id.
156    #[must_use]
157    pub fn clob_pair_id(&self) -> u32 {
158        self.clob_pair_id
159    }
160}
161
162/// [`Order`] builder.
163///
164/// Note that the price input to the `OrderBuilder` is in the "common" units of the perpetual/currency,
165/// not the quantized/atomic value.
166///
167/// Two main classes of orders in dYdX from persistence perspective are
168/// [short-term and long-term (stateful) orders](https://docs.dydx.xyz/concepts/trading/orders#short-term-vs-long-term).
169///
170/// For different types of orders see also [Stop-Limit Versus Stop-Loss](https://dydx.exchange/crypto-learning/stop-limit-versus-stop-loss)
171/// and [Perpetual order types on dYdX Chain](https://help.dydx.trade/en/articles/166981-perpetual-order-types-on-dydx-chain).
172#[derive(Clone, Debug)]
173pub struct OrderBuilder {
174    market_params: OrderMarketParams,
175    subaccount_owner: String,
176    subaccount_number: u32,
177    client_id: u32,
178    flags: OrderFlags,
179    side: Option<OrderSide>,
180    order_type: Option<OrderType>,
181    size: Option<Decimal>,
182    price: Option<Decimal>,
183    time_in_force: Option<OrderTimeInForce>,
184    reduce_only: Option<bool>,
185    until: Option<OrderGoodUntil>,
186    trigger_price: Option<Decimal>,
187    condition_type: Option<ConditionType>,
188}
189
190impl OrderBuilder {
191    /// Create a new [`Order`] builder.
192    #[must_use]
193    pub fn new(
194        market_params: OrderMarketParams,
195        subaccount_owner: String,
196        subaccount_number: u32,
197        client_id: u32,
198    ) -> Self {
199        Self {
200            market_params,
201            subaccount_owner,
202            subaccount_number,
203            client_id,
204            flags: OrderFlags::ShortTerm,
205            side: Some(OrderSide::Buy),
206            order_type: Some(OrderType::Market),
207            size: None,
208            price: None,
209            time_in_force: None,
210            reduce_only: None,
211            until: None,
212            trigger_price: None,
213            condition_type: None,
214        }
215    }
216
217    /// Set as Market order.
218    ///
219    /// An instruction to immediately buy or sell an asset at the best available price when the order is placed.
220    #[must_use]
221    pub fn market(mut self, side: OrderSide, size: Decimal) -> Self {
222        self.order_type = Some(OrderType::Market);
223        self.side = Some(side);
224        self.size = Some(size);
225        self
226    }
227
228    /// Set as Limit order.
229    ///
230    /// With a limit order, a trader specifies the price at which they're willing to buy or sell an asset.
231    /// Unlike market orders, limit orders don't go into effect until the market price hits a trader's "limit price."
232    #[must_use]
233    pub fn limit(mut self, side: OrderSide, price: Decimal, size: Decimal) -> Self {
234        self.order_type = Some(OrderType::Limit);
235        self.price = Some(price);
236        self.side = Some(side);
237        self.size = Some(size);
238        self
239    }
240
241    /// Set as Stop Limit order.
242    ///
243    /// Stop-limit orders use a stop `trigger_price` and a limit `price` to give investors greater control over their trades.
244    #[must_use]
245    pub fn stop_limit(
246        mut self,
247        side: OrderSide,
248        price: Decimal,
249        trigger_price: Decimal,
250        size: Decimal,
251    ) -> Self {
252        self.order_type = Some(OrderType::StopLimit);
253        self.price = Some(price);
254        self.trigger_price = Some(trigger_price);
255        self.side = Some(side);
256        self.size = Some(size);
257        self.conditional()
258    }
259
260    /// Set as Stop Market order.
261    ///
262    /// When using a stop order, the trader sets a `trigger_price` to trigger a buy or sell order on their exchange.
263    #[must_use]
264    pub fn stop_market(mut self, side: OrderSide, trigger_price: Decimal, size: Decimal) -> Self {
265        self.order_type = Some(OrderType::StopMarket);
266        self.trigger_price = Some(trigger_price);
267        self.side = Some(side);
268        self.size = Some(size);
269        self.conditional()
270    }
271
272    /// Set as Take Profit Limit order.
273    ///
274    /// The order enters in force if the price reaches `trigger_price` and is executed at `price` after that.
275    #[must_use]
276    pub fn take_profit_limit(
277        mut self,
278        side: OrderSide,
279        price: Decimal,
280        trigger_price: Decimal,
281        size: Decimal,
282    ) -> Self {
283        self.order_type = Some(OrderType::LimitIfTouched);
284        self.price = Some(price);
285        self.trigger_price = Some(trigger_price);
286        self.side = Some(side);
287        self.size = Some(size);
288        self.conditional()
289    }
290
291    /// Set as Take Profit Market order.
292    ///
293    /// The order enters in force if the price reaches `trigger_price` and converts to an ordinary market order.
294    #[must_use]
295    pub fn take_profit_market(
296        mut self,
297        side: OrderSide,
298        trigger_price: Decimal,
299        size: Decimal,
300    ) -> Self {
301        self.order_type = Some(OrderType::MarketIfTouched);
302        self.trigger_price = Some(trigger_price);
303        self.side = Some(side);
304        self.size = Some(size);
305        self.conditional()
306    }
307
308    /// Set order as a long-term order.
309    #[must_use]
310    pub fn long_term(mut self) -> Self {
311        self.flags = OrderFlags::LongTerm;
312        self
313    }
314
315    /// Set order as a short-term order.
316    #[must_use]
317    pub fn short_term(mut self) -> Self {
318        self.flags = OrderFlags::ShortTerm;
319        self
320    }
321
322    /// Set order as a conditional order, triggered using `trigger_price`.
323    #[must_use]
324    pub fn conditional(mut self) -> Self {
325        self.flags = OrderFlags::Conditional;
326        self
327    }
328
329    /// Set the limit price for Limit orders.
330    #[must_use]
331    pub fn price(mut self, price: Decimal) -> Self {
332        self.price = Some(price);
333        self
334    }
335
336    /// Set position size.
337    #[must_use]
338    pub fn size(mut self, size: Decimal) -> Self {
339        self.size = Some(size);
340        self
341    }
342
343    /// Set [time execution options](https://docs.dydx.xyz/types/time_in_force#time-in-force).
344    #[must_use]
345    pub fn time_in_force(mut self, tif: OrderTimeInForce) -> Self {
346        self.time_in_force = Some(tif);
347        self
348    }
349
350    /// Set an order as [reduce-only](https://docs.dydx.xyz/concepts/trading/orders#types).
351    #[must_use]
352    pub fn reduce_only(mut self, reduce: bool) -> Self {
353        self.reduce_only = Some(reduce);
354        self
355    }
356
357    /// Set order's expiration.
358    #[must_use]
359    pub fn until(mut self, gtof: OrderGoodUntil) -> Self {
360        self.until = Some(gtof);
361        self
362    }
363
364    /// Build the order.
365    ///
366    /// # Errors
367    ///
368    /// Returns an error if the order parameters are invalid.
369    pub fn build(self) -> Result<Order, anyhow::Error> {
370        let side = self
371            .side
372            .ok_or_else(|| anyhow::anyhow!("Order side not set"))?;
373        let size = self
374            .size
375            .ok_or_else(|| anyhow::anyhow!("Order size not set"))?;
376
377        // Quantize size
378        let quantums = self.market_params.quantize_quantity(size)?;
379
380        // Build order ID
381        let order_id = Some(OrderId {
382            subaccount_id: Some(SubaccountId {
383                owner: self.subaccount_owner.clone(),
384                number: self.subaccount_number,
385            }),
386            client_id: self.client_id,
387            order_flags: match self.flags {
388                OrderFlags::ShortTerm => 0,
389                OrderFlags::LongTerm => 64,
390                OrderFlags::Conditional => 32,
391            },
392            clob_pair_id: self.market_params.clob_pair_id,
393        });
394
395        // Set good til oneof - required for all orders
396        let until = self
397            .until
398            .ok_or_else(|| anyhow::anyhow!("Order expiration (until) not set"))?;
399
400        let good_til_oneof = match until {
401            OrderGoodUntil::Block(height) => Some(GoodTilOneof::GoodTilBlock(height)),
402            OrderGoodUntil::Time(time) => {
403                Some(GoodTilOneof::GoodTilBlockTime(time.timestamp().try_into()?))
404            }
405        };
406
407        // Quantize price if provided
408        let subticks = if let Some(price) = self.price {
409            self.market_params.quantize_price(price)?
410        } else {
411            0
412        };
413
414        Ok(Order {
415            order_id,
416            side: side as i32,
417            quantums,
418            subticks,
419            good_til_oneof,
420            time_in_force: self.time_in_force.map_or(0, |tif| tif as i32),
421            reduce_only: self.reduce_only.unwrap_or(false),
422            client_metadata: DEFAULT_RUST_CLIENT_METADATA,
423            condition_type: self.condition_type.map_or(0, |ct| ct as i32),
424            conditional_order_trigger_subticks: self
425                .trigger_price
426                .map(|tp| self.market_params.quantize_price(tp))
427                .transpose()?
428                .unwrap_or(0),
429            twap_parameters: None,
430            builder_code_parameters: None,
431            order_router_address: String::new(),
432        })
433    }
434}
435
436impl Default for OrderBuilder {
437    fn default() -> Self {
438        Self {
439            market_params: OrderMarketParams {
440                atomic_resolution: -10,
441                clob_pair_id: 0,
442                oracle_price: None,
443                quantum_conversion_exponent: -9,
444                step_base_quantums: 1_000_000,
445                subticks_per_tick: 100_000,
446            },
447            subaccount_owner: String::new(),
448            subaccount_number: 0,
449            client_id: 0,
450            flags: OrderFlags::ShortTerm,
451            side: Some(OrderSide::Buy),
452            order_type: Some(OrderType::Market),
453            size: None,
454            price: None,
455            time_in_force: None,
456            reduce_only: None,
457            until: None,
458            trigger_price: None,
459            condition_type: None,
460        }
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    use rstest::rstest;
467    use rust_decimal_macros::dec;
468
469    use super::*;
470
471    fn sample_market_params() -> OrderMarketParams {
472        OrderMarketParams {
473            atomic_resolution: -10,
474            clob_pair_id: 0,
475            oracle_price: Some(dec!(50000)),
476            quantum_conversion_exponent: -9,
477            step_base_quantums: 1_000_000,
478            subticks_per_tick: 100_000,
479        }
480    }
481
482    #[rstest]
483    fn test_market_params_quantize_price() {
484        let market = sample_market_params();
485        let price = dec!(50000);
486        let subticks = market.quantize_price(price).unwrap();
487        // Expected: 50000 * 10^(-(-10) - (-9) - (-6)) = 50000 * 10^5 = 5_000_000_000
488        // Rounded to subticks_per_tick (100_000)
489        assert_eq!(subticks, 5_000_000_000);
490    }
491
492    #[rstest]
493    fn test_market_params_quantize_quantity() {
494        let market = sample_market_params();
495        let quantity = dec!(0.01);
496        let quantums = market.quantize_quantity(quantity).unwrap();
497        // Expected: 0.01 * 10^10 = 100_000_000
498        // Rounded to step_base_quantums (1_000_000)
499        assert_eq!(quantums, 100_000_000);
500    }
501
502    #[rstest]
503    fn test_quantize_price_rounding_up() {
504        let market = sample_market_params();
505        // Price slightly above 50000 should round to next tick
506        let price = dec!(50000.6);
507        let subticks = market.quantize_price(price).unwrap();
508        assert_eq!(subticks, 5_000_100_000);
509    }
510
511    #[rstest]
512    fn test_quantize_price_rounding_down() {
513        let market = sample_market_params();
514        // Price slightly below 50000 should round down
515        let price = dec!(49999.4);
516        let subticks = market.quantize_price(price).unwrap();
517        assert_eq!(subticks, 4_999_900_000);
518    }
519
520    #[rstest]
521    fn test_quantize_quantity_rounding_up() {
522        let market = sample_market_params();
523        // Quantity with 0.5 or more above quantum should round up
524        let quantity = dec!(0.0105); // 105 quantums, rounds to 105
525        let quantums = market.quantize_quantity(quantity).unwrap();
526        assert_eq!(quantums, 105_000_000);
527    }
528
529    #[rstest]
530    fn test_quantize_quantity_rounding_down() {
531        let market = sample_market_params();
532        // Quantity with less than 0.5 above quantum should round down
533        let quantity = dec!(0.0104); // 104 quantums, rounds to 104
534        let quantums = market.quantize_quantity(quantity).unwrap();
535        assert_eq!(quantums, 104_000_000);
536    }
537
538    #[rstest]
539    fn test_quantize_price_minimum_tick() {
540        let market = sample_market_params();
541        // Very small price should round to minimum (subticks_per_tick)
542        let price = dec!(0.001);
543        let subticks = market.quantize_price(price).unwrap();
544        assert_eq!(subticks, market.subticks_per_tick as u64);
545    }
546
547    #[rstest]
548    fn test_quantize_quantity_minimum_quantum() {
549        let market = sample_market_params();
550        // Very small quantity should round to minimum (step_base_quantums)
551        let quantity = dec!(0.00000001);
552        let quantums = market.quantize_quantity(quantity).unwrap();
553        assert_eq!(quantums, market.step_base_quantums);
554    }
555
556    #[rstest]
557    fn test_quantize_price_large_values() {
558        let market = sample_market_params();
559        // Test large price values don't overflow
560        let price = dec!(100000);
561        let subticks = market.quantize_price(price).unwrap();
562        assert_eq!(subticks, 10_000_000_000);
563    }
564
565    #[rstest]
566    fn test_quantize_quantity_large_values() {
567        let market = sample_market_params();
568        // Test large quantity values don't overflow
569        let quantity = dec!(10);
570        let quantums = market.quantize_quantity(quantity).unwrap();
571        assert_eq!(quantums, 100_000_000_000);
572    }
573
574    #[rstest]
575    fn test_order_builder_market_buy() {
576        let market = sample_market_params();
577        let builder = OrderBuilder::new(market, "dydx1test".to_string(), 0, 1);
578
579        let order = builder
580            .market(OrderSide::Buy, dec!(0.01))
581            .until(OrderGoodUntil::Block(100))
582            .build()
583            .unwrap();
584
585        assert_eq!(order.side, OrderSide::Buy as i32);
586        assert_eq!(order.quantums, 100_000_000); // 0.01 BTC quantized
587        assert_eq!(order.subticks, 0); // Market orders use 0 subticks initially
588        assert!(!order.reduce_only);
589        assert_eq!(order.client_metadata, DEFAULT_RUST_CLIENT_METADATA);
590    }
591
592    #[rstest]
593    fn test_order_builder_market_sell() {
594        let market = sample_market_params();
595        let builder = OrderBuilder::new(market, "dydx1test".to_string(), 0, 2);
596
597        let order = builder
598            .market(OrderSide::Sell, dec!(0.02))
599            .until(OrderGoodUntil::Block(100))
600            .build()
601            .unwrap();
602
603        assert_eq!(order.side, OrderSide::Sell as i32);
604        assert_eq!(order.quantums, 200_000_000); // 0.02 BTC quantized
605    }
606
607    #[rstest]
608    fn test_order_builder_limit_buy() {
609        let market = sample_market_params();
610        let builder = OrderBuilder::new(market, "dydx1test".to_string(), 0, 3);
611
612        let order = builder
613            .limit(OrderSide::Buy, dec!(49000), dec!(0.01))
614            .until(OrderGoodUntil::Block(100))
615            .build()
616            .unwrap();
617
618        assert_eq!(order.side, OrderSide::Buy as i32);
619        assert_eq!(order.quantums, 100_000_000); // 0.01 BTC
620        assert_eq!(order.subticks, 4_900_000_000); // 49000 price quantized
621        assert!(!order.reduce_only);
622    }
623
624    #[rstest]
625    fn test_order_builder_limit_sell() {
626        let market = sample_market_params();
627        let builder = OrderBuilder::new(market, "dydx1test".to_string(), 0, 4);
628
629        let order = builder
630            .limit(OrderSide::Sell, dec!(51000), dec!(0.015))
631            .until(OrderGoodUntil::Block(100))
632            .build()
633            .unwrap();
634
635        assert_eq!(order.side, OrderSide::Sell as i32);
636        assert_eq!(order.quantums, 150_000_000); // 0.015 BTC
637        assert_eq!(order.subticks, 5_100_000_000); // 51000 price quantized
638    }
639
640    #[rstest]
641    fn test_order_builder_limit_with_reduce_only() {
642        let market = sample_market_params();
643        let builder = OrderBuilder::new(market, "dydx1test".to_string(), 0, 5);
644
645        let order = builder
646            .limit(OrderSide::Sell, dec!(50000), dec!(0.01))
647            .reduce_only(true)
648            .until(OrderGoodUntil::Block(100))
649            .build()
650            .unwrap();
651
652        assert!(order.reduce_only);
653    }
654
655    #[rstest]
656    fn test_order_builder_short_term_flag() {
657        let market = sample_market_params();
658        let builder = OrderBuilder::new(market, "dydx1test".to_string(), 0, 6);
659
660        let order = builder
661            .short_term()
662            .market(OrderSide::Buy, dec!(0.01))
663            .until(OrderGoodUntil::Block(100))
664            .build()
665            .unwrap();
666
667        // Short-term flag is 0
668        assert_eq!(order.order_id.as_ref().unwrap().order_flags, 0);
669    }
670
671    #[rstest]
672    fn test_order_builder_long_term_flag() {
673        let market = sample_market_params();
674        let builder = OrderBuilder::new(market, "dydx1test".to_string(), 0, 7);
675
676        let now = Utc::now();
677        let until = now + Duration::hours(1);
678
679        let order = builder
680            .long_term()
681            .limit(OrderSide::Buy, dec!(50000), dec!(0.01))
682            .until(OrderGoodUntil::Time(until))
683            .build()
684            .unwrap();
685
686        // Long-term flag is 64
687        assert_eq!(order.order_id.as_ref().unwrap().order_flags, 64);
688    }
689
690    #[rstest]
691    fn test_order_builder_conditional_flag() {
692        let market = sample_market_params();
693        let builder = OrderBuilder::new(market, "dydx1test".to_string(), 0, 8);
694
695        let order = builder
696            .stop_limit(OrderSide::Sell, dec!(48000), dec!(49000), dec!(0.01))
697            .until(OrderGoodUntil::Block(100))
698            .build()
699            .unwrap();
700
701        // Conditional flag is 32
702        assert_eq!(order.order_id.as_ref().unwrap().order_flags, 32);
703        assert_eq!(order.conditional_order_trigger_subticks, 4_900_000_000);
704    }
705
706    #[rstest]
707    fn test_order_builder_missing_size_error() {
708        let market = sample_market_params();
709        let builder = OrderBuilder::new(market, "dydx1test".to_string(), 0, 9);
710
711        let result = builder.until(OrderGoodUntil::Block(100)).build();
712
713        assert!(result.is_err());
714        assert!(result.unwrap_err().to_string().contains("size"));
715    }
716
717    #[rstest]
718    fn test_order_builder_missing_until_error() {
719        let market = sample_market_params();
720        let builder = OrderBuilder::new(market, "dydx1test".to_string(), 0, 10);
721
722        let result = builder.market(OrderSide::Buy, dec!(0.01)).build();
723
724        assert!(result.is_err());
725    }
726
727    #[rstest]
728    fn test_order_builder_time_in_force() {
729        let market = sample_market_params();
730        let builder = OrderBuilder::new(market, "dydx1test".to_string(), 0, 11);
731
732        let order = builder
733            .limit(OrderSide::Buy, dec!(50000), dec!(0.01))
734            .time_in_force(OrderTimeInForce::Ioc)
735            .until(OrderGoodUntil::Block(100))
736            .build()
737            .unwrap();
738
739        assert_eq!(order.time_in_force, OrderTimeInForce::Ioc as i32);
740    }
741
742    #[rstest]
743    fn test_order_builder_clob_pair_id() {
744        let mut market = sample_market_params();
745        market.clob_pair_id = 5;
746
747        let builder = OrderBuilder::new(market, "dydx1test".to_string(), 0, 12);
748
749        let order = builder
750            .market(OrderSide::Buy, dec!(0.01))
751            .until(OrderGoodUntil::Block(100))
752            .build()
753            .unwrap();
754
755        assert_eq!(order.order_id.as_ref().unwrap().clob_pair_id, 5);
756    }
757}