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