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