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