Skip to main content

nautilus_dydx/grpc/
order.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//! 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 40 blocks).
22//! - **Long-term orders**: Expire by timestamp.
23//!
24//! See [dYdX order types](https://docs.dydx.xyz/concepts/trading/orders).
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::{
33    common::consts::DYDX_NAUTILUS_ORDER_ROUTER_ADDRESS,
34    proto::dydxprotocol::{
35        clob::{
36            Order, OrderId,
37            order::{
38                ConditionType, GoodTilOneof, Side as OrderSide, TimeInForce as OrderTimeInForce,
39            },
40        },
41        subaccounts::SubaccountId,
42    },
43};
44
45/// Maximum short-term order lifetime in blocks.
46///
47/// See also [short-term vs long-term orders](https://docs.dydx.xyz/concepts/trading/orders).
48pub const SHORT_TERM_ORDER_MAXIMUM_LIFETIME: u32 = 40;
49
50/// Default slippage (5%) applied to oracle price for market order pay-through price.
51///
52/// Market orders are submitted as IOC limits, so unfilled slippage is not consumed.
53/// The buffer sets the worst-case bound to guarantee fills in volatile conditions.
54// Decimal::new(5, 2) = 0.05
55pub const DEFAULT_MARKET_ORDER_SLIPPAGE: Decimal = Decimal::from_parts(5, 0, 0, false, 2);
56
57/// Value used to identify the Rust client in order metadata.
58pub const DEFAULT_RUST_CLIENT_METADATA: u32 = 4;
59
60/// Order [expiration types](https://docs.dydx.xyz/concepts/trading/orders#comparison).
61#[derive(Clone, Debug)]
62pub enum OrderGoodUntil {
63    /// Block expiration is used for short-term orders.
64    /// The order expires after the specified block height.
65    Block(u32),
66    /// Time expiration is used for long-term orders.
67    /// The order expires at the specified timestamp.
68    Time(DateTime<Utc>),
69}
70
71/// Order flags indicating order lifetime and execution type.
72///
73/// See <https://docs.dydx.xyz/concepts/trading/orders#short-term-vs-long-term> for details
74/// on short-term vs long-term (stateful) orders.
75#[derive(Clone, Debug)]
76pub enum OrderFlags {
77    /// Short-term order (expires by block height).
78    ShortTerm,
79    /// Long-term order (expires by timestamp).
80    LongTerm,
81    /// Conditional order (triggered by trigger price).
82    ///
83    /// Conditional orders include Stop Market, Stop Limit, Take Profit Market, and Take Profit Limit.
84    /// See <https://docs.dydx.xyz/concepts/trading/orders#types> for details.
85    Conditional,
86}
87
88/// Market parameters required for price and size quantizations.
89///
90/// These quantizations are required for `Order` placement.
91/// See also [dYdX trading concepts](https://docs.dydx.xyz/concepts/trading/orders).
92#[derive(Clone, Debug)]
93pub struct OrderMarketParams {
94    /// Atomic resolution.
95    pub atomic_resolution: i32,
96    /// CLOB pair ID.
97    pub clob_pair_id: u32,
98    /// Oracle price.
99    pub oracle_price: Option<Decimal>,
100    /// Quantum conversion exponent.
101    pub quantum_conversion_exponent: i32,
102    /// Step base quantums.
103    pub step_base_quantums: u64,
104    /// Subticks per tick.
105    pub subticks_per_tick: u32,
106}
107
108impl OrderMarketParams {
109    /// Convert price into subticks.
110    ///
111    /// # Errors
112    ///
113    /// Returns an error if conversion fails.
114    pub fn quantize_price(&self, price: Decimal) -> Result<u64, anyhow::Error> {
115        const QUOTE_QUANTUMS_ATOMIC_RESOLUTION: i32 = -6;
116        let exponent = -(self.atomic_resolution
117            - self.quantum_conversion_exponent
118            - QUOTE_QUANTUMS_ATOMIC_RESOLUTION);
119
120        // When exponent is negative, we multiply by 10^|exponent|
121        // When exponent is positive, we divide by 10^exponent (multiply by 10^-exponent)
122        let factor = if exponent < 0 {
123            Decimal::from(10_i64.pow(exponent.unsigned_abs()))
124        } else {
125            Decimal::new(1, exponent.unsigned_abs())
126        };
127
128        let raw_subticks = price * factor;
129        let subticks_per_tick = Decimal::from(self.subticks_per_tick);
130        let quantums = Self::quantize(&raw_subticks, &subticks_per_tick);
131        let result = quantums.max(subticks_per_tick);
132
133        result
134            .to_u64()
135            .ok_or_else(|| anyhow::anyhow!("Failed to convert price to u64"))
136    }
137
138    /// Convert decimal into quantums.
139    ///
140    /// # Errors
141    ///
142    /// Returns an error if conversion fails.
143    pub fn quantize_quantity(&self, quantity: Decimal) -> Result<u64, anyhow::Error> {
144        // When atomic_resolution is negative, we multiply by 10^|atomic_resolution|
145        // When atomic_resolution is positive, we divide by 10^atomic_resolution
146        let factor = if self.atomic_resolution < 0 {
147            Decimal::from(10_i64.pow(self.atomic_resolution.unsigned_abs()))
148        } else {
149            Decimal::new(1, self.atomic_resolution.unsigned_abs())
150        };
151
152        let raw_quantums = quantity * factor;
153        let step_base_quantums = Decimal::from(self.step_base_quantums);
154        let quantums = Self::quantize(&raw_quantums, &step_base_quantums);
155        let result = quantums.max(step_base_quantums);
156
157        result
158            .to_u64()
159            .ok_or_else(|| anyhow::anyhow!("Failed to convert quantity to u64"))
160    }
161
162    /// A `round`-like function that quantizes a `value` to the `fraction`.
163    fn quantize(value: &Decimal, fraction: &Decimal) -> Decimal {
164        (value / fraction).round() * fraction
165    }
166
167    /// Compute worst-case subticks for a market order using oracle price + slippage.
168    ///
169    /// # Errors
170    ///
171    /// Returns an error if oracle price is not available or conversion fails.
172    pub fn market_order_subticks(&self, side: OrderSide) -> Result<u64, anyhow::Error> {
173        let oracle = self
174            .oracle_price
175            .ok_or_else(|| anyhow::anyhow!("Oracle price required for market orders"))?;
176        let worst_price = match side {
177            OrderSide::Buy => oracle * (Decimal::ONE + DEFAULT_MARKET_ORDER_SLIPPAGE),
178            OrderSide::Sell => oracle * (Decimal::ONE - DEFAULT_MARKET_ORDER_SLIPPAGE),
179            _ => oracle,
180        };
181        self.quantize_price(worst_price)
182    }
183
184    /// Get orderbook pair id.
185    #[must_use]
186    pub fn clob_pair_id(&self) -> u32 {
187        self.clob_pair_id
188    }
189}
190
191/// [`Order`] builder.
192///
193/// Note that the price input to the `OrderBuilder` is in the "common" units of the perpetual/currency,
194/// not the quantized/atomic value.
195///
196/// Two main classes of orders in dYdX from persistence perspective are
197/// [short-term and long-term (stateful) orders](https://docs.dydx.xyz/concepts/trading/orders#short-term-vs-long-term).
198///
199/// For different types of orders see also [Stop-Limit Versus Stop-Loss](https://dydx.exchange/crypto-learning/stop-limit-versus-stop-loss)
200/// and [dYdX order types](https://docs.dydx.xyz/concepts/trading/orders).
201#[derive(Clone, Debug)]
202pub struct OrderBuilder {
203    market_params: OrderMarketParams,
204    subaccount_owner: String,
205    subaccount_number: u32,
206    client_id: u32,
207    /// Client metadata for bidirectional ClientOrderId encoding.
208    /// Used to store identity bits (trader/strategy/count) for deterministic decoding.
209    client_metadata: u32,
210    flags: OrderFlags,
211    side: Option<OrderSide>,
212    order_type: Option<OrderType>,
213    size: Option<Decimal>,
214    price: Option<Decimal>,
215    time_in_force: Option<OrderTimeInForce>,
216    reduce_only: Option<bool>,
217    until: Option<OrderGoodUntil>,
218    trigger_price: Option<Decimal>,
219    condition_type: Option<ConditionType>,
220}
221
222impl OrderBuilder {
223    /// Create a new [`Order`] builder.
224    ///
225    /// # Arguments
226    ///
227    /// * `market_params` - Market parameters for price/quantity quantization
228    /// * `subaccount_owner` - The wallet address that owns the subaccount
229    /// * `subaccount_number` - The subaccount number (usually 0)
230    /// * `client_id` - The primary client order ID (u32)
231    /// * `client_metadata` - Metadata for bidirectional ClientOrderId encoding
232    #[must_use]
233    pub fn new(
234        market_params: OrderMarketParams,
235        subaccount_owner: String,
236        subaccount_number: u32,
237        client_id: u32,
238        client_metadata: u32,
239    ) -> Self {
240        Self {
241            market_params,
242            subaccount_owner,
243            subaccount_number,
244            client_id,
245            client_metadata,
246            flags: OrderFlags::ShortTerm,
247            side: Some(OrderSide::Buy),
248            order_type: Some(OrderType::Market),
249            size: None,
250            price: None,
251            time_in_force: None,
252            reduce_only: None,
253            until: None,
254            trigger_price: None,
255            condition_type: None,
256        }
257    }
258
259    /// Set as Market order.
260    ///
261    /// An instruction to immediately buy or sell an asset at the best available price when the order is placed.
262    /// dYdX implements market orders as IOC limit orders with a slippage-adjusted worst-case price.
263    #[must_use]
264    pub fn market(mut self, side: OrderSide, size: Decimal) -> Self {
265        self.order_type = Some(OrderType::Market);
266        self.side = Some(side);
267        self.size = Some(size);
268        self.time_in_force = Some(OrderTimeInForce::Ioc);
269        self
270    }
271
272    /// Set as Limit order.
273    ///
274    /// With a limit order, a trader specifies the price at which they're willing to buy or sell an asset.
275    /// Unlike market orders, limit orders don't go into effect until the market price hits a trader's "limit price."
276    #[must_use]
277    pub fn limit(mut self, side: OrderSide, price: Decimal, size: Decimal) -> Self {
278        self.order_type = Some(OrderType::Limit);
279        self.price = Some(price);
280        self.side = Some(side);
281        self.size = Some(size);
282        self
283    }
284
285    /// Set as Stop Limit order.
286    ///
287    /// Stop-limit orders use a stop `trigger_price` and a limit `price` to give investors greater control over their trades.
288    #[must_use]
289    pub fn stop_limit(
290        mut self,
291        side: OrderSide,
292        price: Decimal,
293        trigger_price: Decimal,
294        size: Decimal,
295    ) -> Self {
296        self.order_type = Some(OrderType::StopLimit);
297        self.price = Some(price);
298        self.trigger_price = Some(trigger_price);
299        self.side = Some(side);
300        self.size = Some(size);
301        self.condition_type = Some(ConditionType::StopLoss);
302        self.conditional()
303    }
304
305    /// Set as Stop Market order.
306    ///
307    /// When using a stop order, the trader sets a `trigger_price` to trigger a buy or sell order on their exchange.
308    #[must_use]
309    pub fn stop_market(mut self, side: OrderSide, trigger_price: Decimal, size: Decimal) -> Self {
310        self.order_type = Some(OrderType::StopMarket);
311        self.trigger_price = Some(trigger_price);
312        self.side = Some(side);
313        self.size = Some(size);
314        self.condition_type = Some(ConditionType::StopLoss);
315        self.conditional()
316    }
317
318    /// Set as Take Profit Limit order.
319    ///
320    /// The order enters in force if the price reaches `trigger_price` and is executed at `price` after that.
321    #[must_use]
322    pub fn take_profit_limit(
323        mut self,
324        side: OrderSide,
325        price: Decimal,
326        trigger_price: Decimal,
327        size: Decimal,
328    ) -> Self {
329        self.order_type = Some(OrderType::LimitIfTouched);
330        self.price = Some(price);
331        self.trigger_price = Some(trigger_price);
332        self.side = Some(side);
333        self.size = Some(size);
334        self.condition_type = Some(ConditionType::TakeProfit);
335        self.conditional()
336    }
337
338    /// Set as Take Profit Market order.
339    ///
340    /// The order enters in force if the price reaches `trigger_price` and converts to an ordinary market order.
341    #[must_use]
342    pub fn take_profit_market(
343        mut self,
344        side: OrderSide,
345        trigger_price: Decimal,
346        size: Decimal,
347    ) -> Self {
348        self.order_type = Some(OrderType::MarketIfTouched);
349        self.trigger_price = Some(trigger_price);
350        self.side = Some(side);
351        self.size = Some(size);
352        self.condition_type = Some(ConditionType::TakeProfit);
353        self.conditional()
354    }
355
356    /// Set order as a long-term order.
357    #[must_use]
358    pub fn long_term(mut self) -> Self {
359        self.flags = OrderFlags::LongTerm;
360        self
361    }
362
363    /// Set order as a short-term order.
364    #[must_use]
365    pub fn short_term(mut self) -> Self {
366        self.flags = OrderFlags::ShortTerm;
367        self
368    }
369
370    /// Set order as a conditional order, triggered using `trigger_price`.
371    #[must_use]
372    pub fn conditional(mut self) -> Self {
373        self.flags = OrderFlags::Conditional;
374        self
375    }
376
377    /// Set the limit price for Limit orders.
378    #[must_use]
379    pub fn price(mut self, price: Decimal) -> Self {
380        self.price = Some(price);
381        self
382    }
383
384    /// Set position size.
385    #[must_use]
386    pub fn size(mut self, size: Decimal) -> Self {
387        self.size = Some(size);
388        self
389    }
390
391    /// Set [time execution options](https://docs.dydx.xyz/types/time_in_force#time-in-force).
392    #[must_use]
393    pub fn time_in_force(mut self, tif: OrderTimeInForce) -> Self {
394        self.time_in_force = Some(tif);
395        self
396    }
397
398    /// Set an order as [reduce-only](https://docs.dydx.xyz/concepts/trading/orders#types).
399    #[must_use]
400    pub fn reduce_only(mut self, reduce: bool) -> Self {
401        self.reduce_only = Some(reduce);
402        self
403    }
404
405    /// Set order's expiration.
406    #[must_use]
407    pub fn until(mut self, gtof: OrderGoodUntil) -> Self {
408        self.until = Some(gtof);
409        self
410    }
411
412    /// Build the order.
413    ///
414    /// # Errors
415    ///
416    /// Returns an error if the order parameters are invalid.
417    pub fn build(self) -> Result<Order, anyhow::Error> {
418        let side = self
419            .side
420            .ok_or_else(|| anyhow::anyhow!("Order side not set"))?;
421        let size = self
422            .size
423            .ok_or_else(|| anyhow::anyhow!("Order size not set"))?;
424
425        // Quantize size
426        let quantums = self.market_params.quantize_quantity(size)?;
427
428        // Build order ID
429        let order_id = Some(OrderId {
430            subaccount_id: Some(SubaccountId {
431                owner: self.subaccount_owner.clone(),
432                number: self.subaccount_number,
433            }),
434            client_id: self.client_id,
435            order_flags: match self.flags {
436                OrderFlags::ShortTerm => 0,
437                OrderFlags::LongTerm => 64,
438                OrderFlags::Conditional => 32,
439            },
440            clob_pair_id: self.market_params.clob_pair_id,
441        });
442
443        // Set good til oneof - required for all orders
444        let until = self
445            .until
446            .ok_or_else(|| anyhow::anyhow!("Order expiration (until) not set"))?;
447
448        let good_til_oneof = match until {
449            OrderGoodUntil::Block(height) => Some(GoodTilOneof::GoodTilBlock(height)),
450            OrderGoodUntil::Time(time) => {
451                Some(GoodTilOneof::GoodTilBlockTime(time.timestamp().try_into()?))
452            }
453        };
454
455        // Quantize price: use explicit price if set, otherwise compute worst-case for market orders
456        let subticks = if let Some(price) = self.price {
457            self.market_params.quantize_price(price)?
458        } else if matches!(
459            self.order_type,
460            Some(OrderType::Market | OrderType::StopMarket | OrderType::MarketIfTouched)
461        ) {
462            let side = self
463                .side
464                .ok_or_else(|| anyhow::anyhow!("Order side not set"))?;
465            self.market_params.market_order_subticks(side)?
466        } else {
467            0
468        };
469
470        Ok(Order {
471            order_id,
472            side: side as i32,
473            quantums,
474            subticks,
475            good_til_oneof,
476            time_in_force: self.time_in_force.map_or(0, |tif| tif as i32),
477            reduce_only: self.reduce_only.unwrap_or(false),
478            client_metadata: self.client_metadata,
479            condition_type: self.condition_type.map_or(0, |ct| ct as i32),
480            conditional_order_trigger_subticks: self
481                .trigger_price
482                .map(|tp| self.market_params.quantize_price(tp))
483                .transpose()?
484                .unwrap_or(0),
485            twap_parameters: None,
486            builder_code_parameters: None,
487            order_router_address: DYDX_NAUTILUS_ORDER_ROUTER_ADDRESS.to_string(),
488        })
489    }
490}
491
492impl Default for OrderBuilder {
493    fn default() -> Self {
494        Self {
495            market_params: OrderMarketParams {
496                atomic_resolution: -10,
497                clob_pair_id: 0,
498                oracle_price: Some(Decimal::from(50_000)),
499                quantum_conversion_exponent: -9,
500                step_base_quantums: 1_000_000,
501                subticks_per_tick: 100_000,
502            },
503            subaccount_owner: String::new(),
504            subaccount_number: 0,
505            client_id: 0,
506            client_metadata: DEFAULT_RUST_CLIENT_METADATA,
507            flags: OrderFlags::ShortTerm,
508            side: Some(OrderSide::Buy),
509            order_type: Some(OrderType::Market),
510            size: None,
511            price: None,
512            time_in_force: None,
513            reduce_only: None,
514            until: None,
515            trigger_price: None,
516            condition_type: None,
517        }
518    }
519}
520
521#[cfg(test)]
522mod tests {
523    use rstest::rstest;
524    use rust_decimal_macros::dec;
525
526    use super::*;
527
528    fn sample_market_params() -> OrderMarketParams {
529        OrderMarketParams {
530            atomic_resolution: -10,
531            clob_pair_id: 0,
532            oracle_price: Some(dec!(50000)),
533            quantum_conversion_exponent: -9,
534            step_base_quantums: 1_000_000,
535            subticks_per_tick: 100_000,
536        }
537    }
538
539    #[rstest]
540    fn test_market_params_quantize_price() {
541        let market = sample_market_params();
542        let price = dec!(50000);
543        let subticks = market.quantize_price(price).unwrap();
544        // Expected: 50000 * 10^(-(-10) - (-9) - (-6)) = 50000 * 10^5 = 5_000_000_000
545        // Rounded to subticks_per_tick (100_000)
546        assert_eq!(subticks, 5_000_000_000);
547    }
548
549    #[rstest]
550    fn test_market_params_quantize_quantity() {
551        let market = sample_market_params();
552        let quantity = dec!(0.01);
553        let quantums = market.quantize_quantity(quantity).unwrap();
554        // Expected: 0.01 * 10^10 = 100_000_000
555        // Rounded to step_base_quantums (1_000_000)
556        assert_eq!(quantums, 100_000_000);
557    }
558
559    #[rstest]
560    fn test_quantize_price_rounding_up() {
561        let market = sample_market_params();
562        // Price slightly above 50000 should round to next tick
563        let price = dec!(50000.6);
564        let subticks = market.quantize_price(price).unwrap();
565        assert_eq!(subticks, 5_000_100_000);
566    }
567
568    #[rstest]
569    fn test_quantize_price_rounding_down() {
570        let market = sample_market_params();
571        // Price slightly below 50000 should round down
572        let price = dec!(49999.4);
573        let subticks = market.quantize_price(price).unwrap();
574        assert_eq!(subticks, 4_999_900_000);
575    }
576
577    #[rstest]
578    fn test_quantize_quantity_rounding_up() {
579        let market = sample_market_params();
580        // Quantity with 0.5 or more above quantum should round up
581        let quantity = dec!(0.0105); // 105 quantums, rounds to 105
582        let quantums = market.quantize_quantity(quantity).unwrap();
583        assert_eq!(quantums, 105_000_000);
584    }
585
586    #[rstest]
587    fn test_quantize_quantity_rounding_down() {
588        let market = sample_market_params();
589        // Quantity with less than 0.5 above quantum should round down
590        let quantity = dec!(0.0104); // 104 quantums, rounds to 104
591        let quantums = market.quantize_quantity(quantity).unwrap();
592        assert_eq!(quantums, 104_000_000);
593    }
594
595    #[rstest]
596    fn test_quantize_price_minimum_tick() {
597        let market = sample_market_params();
598        // Very small price should round to minimum (subticks_per_tick)
599        let price = dec!(0.001);
600        let subticks = market.quantize_price(price).unwrap();
601        assert_eq!(subticks, market.subticks_per_tick as u64);
602    }
603
604    #[rstest]
605    fn test_quantize_quantity_minimum_quantum() {
606        let market = sample_market_params();
607        // Very small quantity should round to minimum (step_base_quantums)
608        let quantity = dec!(0.00000001);
609        let quantums = market.quantize_quantity(quantity).unwrap();
610        assert_eq!(quantums, market.step_base_quantums);
611    }
612
613    #[rstest]
614    fn test_quantize_price_large_values() {
615        let market = sample_market_params();
616        // Test large price values don't overflow
617        let price = dec!(100000);
618        let subticks = market.quantize_price(price).unwrap();
619        assert_eq!(subticks, 10_000_000_000);
620    }
621
622    #[rstest]
623    fn test_quantize_quantity_large_values() {
624        let market = sample_market_params();
625        // Test large quantity values don't overflow
626        let quantity = dec!(10);
627        let quantums = market.quantize_quantity(quantity).unwrap();
628        assert_eq!(quantums, 100_000_000_000);
629    }
630
631    #[rstest]
632    fn test_order_builder_market_buy() {
633        let market = sample_market_params();
634        let builder = OrderBuilder::new(
635            market,
636            "dydx1test".to_string(),
637            0,
638            1,
639            DEFAULT_RUST_CLIENT_METADATA,
640        );
641
642        let order = builder
643            .market(OrderSide::Buy, dec!(0.01))
644            .until(OrderGoodUntil::Block(100))
645            .build()
646            .unwrap();
647
648        assert_eq!(order.side, OrderSide::Buy as i32);
649        assert_eq!(order.quantums, 100_000_000); // 0.01 BTC quantized
650        assert_eq!(order.subticks, 5_250_000_000); // 50000 * 1.05 = 52500 worst-case buy price
651        assert_eq!(order.time_in_force, OrderTimeInForce::Ioc as i32);
652        assert!(!order.reduce_only);
653        assert_eq!(order.client_metadata, DEFAULT_RUST_CLIENT_METADATA);
654    }
655
656    #[rstest]
657    fn test_order_builder_market_sell() {
658        let market = sample_market_params();
659        let builder = OrderBuilder::new(
660            market,
661            "dydx1test".to_string(),
662            0,
663            2,
664            DEFAULT_RUST_CLIENT_METADATA,
665        );
666
667        let order = builder
668            .market(OrderSide::Sell, dec!(0.02))
669            .until(OrderGoodUntil::Block(100))
670            .build()
671            .unwrap();
672
673        assert_eq!(order.side, OrderSide::Sell as i32);
674        assert_eq!(order.quantums, 200_000_000); // 0.02 BTC quantized
675        assert_eq!(order.subticks, 4_750_000_000); // 50000 * 0.95 = 47500 worst-case sell price
676        assert_eq!(order.time_in_force, OrderTimeInForce::Ioc as i32);
677    }
678
679    #[rstest]
680    fn test_order_builder_market_no_oracle_price_error() {
681        let mut market = sample_market_params();
682        market.oracle_price = None;
683
684        let builder = OrderBuilder::new(
685            market,
686            "dydx1test".to_string(),
687            0,
688            13,
689            DEFAULT_RUST_CLIENT_METADATA,
690        );
691
692        let result = builder
693            .market(OrderSide::Buy, dec!(0.01))
694            .until(OrderGoodUntil::Block(100))
695            .build();
696
697        assert!(result.is_err());
698        assert!(
699            result
700                .unwrap_err()
701                .to_string()
702                .contains("Oracle price required")
703        );
704    }
705
706    #[rstest]
707    fn test_order_builder_limit_buy() {
708        let market = sample_market_params();
709        let builder = OrderBuilder::new(
710            market,
711            "dydx1test".to_string(),
712            0,
713            3,
714            DEFAULT_RUST_CLIENT_METADATA,
715        );
716
717        let order = builder
718            .limit(OrderSide::Buy, dec!(49000), dec!(0.01))
719            .until(OrderGoodUntil::Block(100))
720            .build()
721            .unwrap();
722
723        assert_eq!(order.side, OrderSide::Buy as i32);
724        assert_eq!(order.quantums, 100_000_000); // 0.01 BTC
725        assert_eq!(order.subticks, 4_900_000_000); // 49000 price quantized
726        assert!(!order.reduce_only);
727    }
728
729    #[rstest]
730    fn test_order_builder_limit_sell() {
731        let market = sample_market_params();
732        let builder = OrderBuilder::new(
733            market,
734            "dydx1test".to_string(),
735            0,
736            4,
737            DEFAULT_RUST_CLIENT_METADATA,
738        );
739
740        let order = builder
741            .limit(OrderSide::Sell, dec!(51000), dec!(0.015))
742            .until(OrderGoodUntil::Block(100))
743            .build()
744            .unwrap();
745
746        assert_eq!(order.side, OrderSide::Sell as i32);
747        assert_eq!(order.quantums, 150_000_000); // 0.015 BTC
748        assert_eq!(order.subticks, 5_100_000_000); // 51000 price quantized
749    }
750
751    #[rstest]
752    fn test_order_builder_limit_with_reduce_only() {
753        let market = sample_market_params();
754        let builder = OrderBuilder::new(
755            market,
756            "dydx1test".to_string(),
757            0,
758            5,
759            DEFAULT_RUST_CLIENT_METADATA,
760        );
761
762        let order = builder
763            .limit(OrderSide::Sell, dec!(50000), dec!(0.01))
764            .reduce_only(true)
765            .until(OrderGoodUntil::Block(100))
766            .build()
767            .unwrap();
768
769        assert!(order.reduce_only);
770    }
771
772    #[rstest]
773    fn test_order_builder_short_term_flag() {
774        let market = sample_market_params();
775        let builder = OrderBuilder::new(
776            market,
777            "dydx1test".to_string(),
778            0,
779            6,
780            DEFAULT_RUST_CLIENT_METADATA,
781        );
782
783        let order = builder
784            .short_term()
785            .market(OrderSide::Buy, dec!(0.01))
786            .until(OrderGoodUntil::Block(100))
787            .build()
788            .unwrap();
789
790        // Short-term flag is 0
791        assert_eq!(order.order_id.as_ref().unwrap().order_flags, 0);
792    }
793
794    #[rstest]
795    fn test_order_builder_long_term_flag() {
796        let market = sample_market_params();
797        let builder = OrderBuilder::new(
798            market,
799            "dydx1test".to_string(),
800            0,
801            7,
802            DEFAULT_RUST_CLIENT_METADATA,
803        );
804
805        let now = Utc::now();
806        let until = now + Duration::hours(1);
807
808        let order = builder
809            .long_term()
810            .limit(OrderSide::Buy, dec!(50000), dec!(0.01))
811            .until(OrderGoodUntil::Time(until))
812            .build()
813            .unwrap();
814
815        // Long-term flag is 64
816        assert_eq!(order.order_id.as_ref().unwrap().order_flags, 64);
817    }
818
819    #[rstest]
820    fn test_order_builder_conditional_flag() {
821        let market = sample_market_params();
822        let builder = OrderBuilder::new(
823            market,
824            "dydx1test".to_string(),
825            0,
826            8,
827            DEFAULT_RUST_CLIENT_METADATA,
828        );
829
830        let order = builder
831            .stop_limit(OrderSide::Sell, dec!(48000), dec!(49000), dec!(0.01))
832            .until(OrderGoodUntil::Block(100))
833            .build()
834            .unwrap();
835
836        // Conditional flag is 32
837        assert_eq!(order.order_id.as_ref().unwrap().order_flags, 32);
838        assert_eq!(order.conditional_order_trigger_subticks, 4_900_000_000);
839    }
840
841    #[rstest]
842    fn test_stop_limit_sets_condition_type() {
843        let market = sample_market_params();
844        let builder = OrderBuilder::new(
845            market,
846            "dydx1test".to_string(),
847            0,
848            100,
849            DEFAULT_RUST_CLIENT_METADATA,
850        );
851
852        let order = builder
853            .stop_limit(OrderSide::Sell, dec!(48000), dec!(49000), dec!(0.01))
854            .until(OrderGoodUntil::Block(100))
855            .build()
856            .unwrap();
857
858        assert_eq!(order.condition_type, ConditionType::StopLoss as i32);
859    }
860
861    #[rstest]
862    fn test_stop_market_sets_condition_type() {
863        let market = sample_market_params();
864        let builder = OrderBuilder::new(
865            market,
866            "dydx1test".to_string(),
867            0,
868            101,
869            DEFAULT_RUST_CLIENT_METADATA,
870        );
871
872        let order = builder
873            .stop_market(OrderSide::Sell, dec!(49000), dec!(0.01))
874            .until(OrderGoodUntil::Block(100))
875            .build()
876            .unwrap();
877
878        assert_eq!(order.condition_type, ConditionType::StopLoss as i32);
879    }
880
881    #[rstest]
882    fn test_take_profit_limit_sets_condition_type() {
883        let market = sample_market_params();
884        let builder = OrderBuilder::new(
885            market,
886            "dydx1test".to_string(),
887            0,
888            102,
889            DEFAULT_RUST_CLIENT_METADATA,
890        );
891
892        let order = builder
893            .take_profit_limit(OrderSide::Sell, dec!(52000), dec!(51000), dec!(0.01))
894            .until(OrderGoodUntil::Block(100))
895            .build()
896            .unwrap();
897
898        assert_eq!(order.condition_type, ConditionType::TakeProfit as i32);
899    }
900
901    #[rstest]
902    fn test_take_profit_market_sets_condition_type() {
903        let market = sample_market_params();
904        let builder = OrderBuilder::new(
905            market,
906            "dydx1test".to_string(),
907            0,
908            103,
909            DEFAULT_RUST_CLIENT_METADATA,
910        );
911
912        let order = builder
913            .take_profit_market(OrderSide::Sell, dec!(51000), dec!(0.01))
914            .until(OrderGoodUntil::Block(100))
915            .build()
916            .unwrap();
917
918        assert_eq!(order.condition_type, ConditionType::TakeProfit as i32);
919    }
920
921    #[rstest]
922    fn test_order_builder_missing_size_error() {
923        let market = sample_market_params();
924        let builder = OrderBuilder::new(
925            market,
926            "dydx1test".to_string(),
927            0,
928            9,
929            DEFAULT_RUST_CLIENT_METADATA,
930        );
931
932        let result = builder.until(OrderGoodUntil::Block(100)).build();
933
934        assert!(result.is_err());
935        assert!(result.unwrap_err().to_string().contains("size"));
936    }
937
938    #[rstest]
939    fn test_order_builder_missing_until_error() {
940        let market = sample_market_params();
941        let builder = OrderBuilder::new(
942            market,
943            "dydx1test".to_string(),
944            0,
945            10,
946            DEFAULT_RUST_CLIENT_METADATA,
947        );
948
949        let result = builder.market(OrderSide::Buy, dec!(0.01)).build();
950
951        assert!(result.is_err());
952    }
953
954    #[rstest]
955    fn test_order_builder_time_in_force() {
956        let market = sample_market_params();
957        let builder = OrderBuilder::new(
958            market,
959            "dydx1test".to_string(),
960            0,
961            11,
962            DEFAULT_RUST_CLIENT_METADATA,
963        );
964
965        let order = builder
966            .limit(OrderSide::Buy, dec!(50000), dec!(0.01))
967            .time_in_force(OrderTimeInForce::Ioc)
968            .until(OrderGoodUntil::Block(100))
969            .build()
970            .unwrap();
971
972        assert_eq!(order.time_in_force, OrderTimeInForce::Ioc as i32);
973    }
974
975    #[rstest]
976    fn test_order_builder_clob_pair_id() {
977        let mut market = sample_market_params();
978        market.clob_pair_id = 5;
979
980        let builder = OrderBuilder::new(
981            market,
982            "dydx1test".to_string(),
983            0,
984            12,
985            DEFAULT_RUST_CLIENT_METADATA,
986        );
987
988        let order = builder
989            .market(OrderSide::Buy, dec!(0.01))
990            .until(OrderGoodUntil::Block(100))
991            .build()
992            .unwrap();
993
994        assert_eq!(order.order_id.as_ref().unwrap().clob_pair_id, 5);
995    }
996}