Skip to main content

rithmic_rs/api/commands/
bracket.rs

1//! Bracket entry orders and the adjustment that moves one of their exit legs.
2
3use super::triggers::RithmicIfTouchedTrigger;
4use super::validate_instrument;
5
6use crate::{
7    error::RithmicError,
8    types::{
9        BracketOperationType, BracketType, ManualOrAutoEntry, OrderSide, OrderType, TimeInForce,
10    },
11};
12
13/// Entry order with linked profit target and stop loss orders.
14///
15/// Supports multiple target and stop legs, triggered entry, break-even, trailing
16/// stops, and timed release/cancel.
17///
18/// # Example: one target, one stop
19///
20/// ```
21/// use rithmic_rs::{OrderSide, OrderType, RithmicBracketOrder};
22/// # fn main() -> Result<(), rithmic_rs::RithmicError> {
23/// let order = RithmicBracketOrder::new()
24///     .symbol("ESH6")
25///     .exchange("CME")
26///     .quantity(1)
27///     .action(OrderSide::Buy)
28///     .price_type(OrderType::Limit)
29///     .price(5000.0)
30///     .target(20)
31///     .stop(10)
32///     .localid("my-order-1")
33///     .build()?;
34/// # Ok(())
35/// # }
36/// ```
37///
38/// # Example: staggered targets
39///
40/// ```
41/// use rithmic_rs::{OrderSide, OrderType, RithmicBracketOrder};
42/// # fn main() -> Result<(), rithmic_rs::RithmicError> {
43/// let order = RithmicBracketOrder::new()
44///     .symbol("ESM6")
45///     .exchange("CME")
46///     .quantity(3)
47///     .action(OrderSide::Buy)
48///     .price_type(OrderType::StopLimit)
49///     .price(5000.25)
50///     .trigger_price(4999.75)
51///     .targets([(2, 16), (1, 24)])
52///     .stops([(3, 8)])
53///     .break_even_ticks(2)
54///     .build()?;
55/// # Ok(())
56/// # }
57/// ```
58#[derive(Debug, Clone, Default, PartialEq)]
59#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
60#[non_exhaustive]
61#[must_use = "an order does nothing until passed to a plant handle"]
62pub struct RithmicBracketOrder {
63    /// Buy or Sell.
64    pub action: OrderSide,
65    /// Order duration.
66    pub duration: TimeInForce,
67    /// Exchange code (e.g., "CME").
68    pub exchange: String,
69    /// Your identifier for tracking this order.
70    pub localid: String,
71    /// Order type.
72    pub price_type: OrderType,
73    /// Entry price. A market entry does not need one.
74    pub price: Option<f64>,
75    /// Trigger price. Only a stop or if-touched entry needs one.
76    pub trigger_price: Option<f64>,
77    /// Entry order size (number of contracts).
78    ///
79    /// For a coherent bracket, this should equal the sum of
80    /// `target_quantity` across all target legs. The crate does not validate
81    /// this invariant.
82    pub quantity: i32,
83    /// Trading symbol (e.g., "ESH6").
84    pub symbol: String,
85    /// Rithmic bracket shape. `None` means "derive it from the legs supplied";
86    /// [`Self::build`] resolves it from the target and stop legs, and leaves it
87    /// unset when there are none.
88    pub bracket_type: Option<BracketType>,
89    /// Exit target quantities, one value per target leg.
90    pub target_quantity: Vec<i32>,
91    /// Exit target distances in ticks.
92    pub target_ticks: Vec<i32>,
93    /// Exit stop quantities.
94    pub stop_quantity: Vec<i32>,
95    /// Exit stop distances in ticks.
96    pub stop_ticks: Vec<i32>,
97    /// Optional if-touched trigger settings.
98    pub if_touched: Option<RithmicIfTouchedTrigger>,
99    /// Move stop to break-even by this many ticks.
100    pub break_even_ticks: Option<i32>,
101    /// Trigger break-even once the position reaches this many ticks.
102    pub break_even_trigger_ticks: Option<i32>,
103    /// Enable a trailing stop after this many ticks.
104    pub trailing_stop_trigger_ticks: Option<i32>,
105    /// Use last trade instead of bid/offer for trailing stop tracking.
106    pub trailing_stop_by_last_trade_price: Option<bool>,
107    /// Convert target to MIT once touched.
108    pub target_market_order_if_touched: Option<bool>,
109    /// Convert stop to market if the current stop order is rejected.
110    pub stop_market_on_reject: Option<bool>,
111    /// Convert target to market at this second-since-beginning-of-epoch value.
112    pub target_market_at_ssboe: Option<i32>,
113    /// Microsecond component for `target_market_at_ssboe`.
114    pub target_market_at_usecs: Option<i32>,
115    /// Convert stop to market at this second-since-beginning-of-epoch value.
116    pub stop_market_at_ssboe: Option<i32>,
117    /// Microsecond component for `stop_market_at_ssboe`.
118    pub stop_market_at_usecs: Option<i32>,
119    /// Convert target to market after this many seconds.
120    pub target_market_order_after_secs: Option<i32>,
121    /// Release order at this second-since-beginning-of-epoch value.
122    pub release_at_ssboe: Option<i32>,
123    /// Microsecond component for `release_at_ssboe`.
124    pub release_at_usecs: Option<i32>,
125    /// Cancel order at this second-since-beginning-of-epoch value.
126    pub cancel_at_ssboe: Option<i32>,
127    /// Microsecond component for `cancel_at_ssboe`.
128    pub cancel_at_usecs: Option<i32>,
129    /// Cancel order after this many seconds.
130    pub cancel_after_secs: Option<i32>,
131    /// Route to send on. `None` uses the route the server published for `exchange`.
132    pub trade_route: Option<String>,
133    /// Whether the order was placed by a human or automatically.
134    pub manual_or_auto: ManualOrAutoEntry,
135    /// Originating window name reported to Rithmic.
136    pub window_name: Option<String>,
137    /// The `order_operation_type` sent to Rithmic. `None` leaves the choice
138    /// to the server.
139    pub operation_type: Option<BracketOperationType>,
140}
141
142/// The exit-leg setters come in singular and plural. Singular sets one leg
143/// sized to the entry quantity; plural takes explicit `(quantity, ticks)`
144/// pairs.
145///
146/// ```
147/// use rithmic_rs::RithmicBracketOrder;
148///
149/// let sized = RithmicBracketOrder::new().quantity(2).target(8).stop(4);
150/// let explicit = RithmicBracketOrder::new()
151///     .targets([(1, 8), (1, 16)])
152///     .stops([(2, 4)]);
153/// ```
154impl RithmicBracketOrder {
155    /// Start from the defaults.
156    pub fn new() -> Self {
157        Self::default()
158    }
159
160    /// Instrument symbol.
161    pub fn symbol(mut self, symbol: impl Into<String>) -> Self {
162        self.symbol = symbol.into();
163        self
164    }
165
166    /// Exchange the instrument trades on.
167    pub fn exchange(mut self, exchange: impl Into<String>) -> Self {
168        self.exchange = exchange.into();
169        self
170    }
171
172    /// Number of contracts on the entry.
173    ///
174    /// Set this before [`Self::target`] or [`Self::stop`], which size their
175    /// leg to whatever the quantity is when they are called.
176    pub fn quantity(mut self, quantity: i32) -> Self {
177        self.quantity = quantity;
178        self
179    }
180
181    /// Buy or sell on the entry.
182    pub fn action(mut self, action: OrderSide) -> Self {
183        self.action = action;
184        self
185    }
186
187    /// Market, limit, stop, or if-touched entry.
188    pub fn price_type(mut self, price_type: OrderType) -> Self {
189        self.price_type = price_type;
190        self
191    }
192
193    /// How long the entry stays working.
194    pub fn duration(mut self, duration: TimeInForce) -> Self {
195        self.duration = duration;
196        self
197    }
198
199    /// Your identifier for tracking this order.
200    pub fn localid(mut self, localid: impl Into<String>) -> Self {
201        self.localid = localid.into();
202        self
203    }
204
205    /// Entry price.
206    pub fn price(mut self, price: f64) -> Self {
207        self.price = Some(price);
208        self
209    }
210
211    /// Trigger price for stop and if-touched entry types.
212    pub fn trigger_price(mut self, trigger_price: f64) -> Self {
213        self.trigger_price = Some(trigger_price);
214        self
215    }
216
217    /// Bracket shape, overriding what `build()` would derive from the legs.
218    pub fn bracket_type(mut self, bracket_type: BracketType) -> Self {
219        self.bracket_type = Some(bracket_type);
220        self
221    }
222
223    /// One target leg at this tick distance, sized to the entry quantity.
224    ///
225    /// Reads [`Self::quantity`] as it stands right now, so set the quantity
226    /// first — otherwise the leg is sized to 0 and [`Self::build`] rejects it.
227    pub fn target(mut self, ticks: i32) -> Self {
228        self.target_quantity = vec![self.quantity];
229        self.target_ticks = vec![ticks];
230        self
231    }
232
233    /// One stop leg at this tick distance, sized to the entry quantity.
234    ///
235    /// Reads [`Self::quantity`] as it stands right now, so set the quantity
236    /// first — otherwise the leg is sized to 0 and [`Self::build`] rejects it.
237    pub fn stop(mut self, ticks: i32) -> Self {
238        self.stop_quantity = vec![self.quantity];
239        self.stop_ticks = vec![ticks];
240        self
241    }
242
243    /// Target legs as `(quantity, ticks)` pairs, replacing any already set.
244    pub fn targets(mut self, legs: impl IntoIterator<Item = (i32, i32)>) -> Self {
245        let (quantities, ticks): (Vec<i32>, Vec<i32>) = legs.into_iter().unzip();
246        self.target_quantity = quantities;
247        self.target_ticks = ticks;
248        self
249    }
250
251    /// Stop legs as `(quantity, ticks)` pairs, replacing any already set.
252    pub fn stops(mut self, legs: impl IntoIterator<Item = (i32, i32)>) -> Self {
253        let (quantities, ticks): (Vec<i32>, Vec<i32>) = legs.into_iter().unzip();
254        self.stop_quantity = quantities;
255        self.stop_ticks = ticks;
256        self
257    }
258
259    /// Conditional trigger that releases the entry once touched.
260    pub fn if_touched(mut self, if_touched: RithmicIfTouchedTrigger) -> Self {
261        self.if_touched = Some(if_touched);
262        self
263    }
264
265    /// Move the stop to break-even by this many ticks.
266    pub fn break_even_ticks(mut self, ticks: i32) -> Self {
267        self.break_even_ticks = Some(ticks);
268        self
269    }
270
271    /// Trigger break-even once the position reaches this many ticks.
272    pub fn break_even_trigger_ticks(mut self, ticks: i32) -> Self {
273        self.break_even_trigger_ticks = Some(ticks);
274        self
275    }
276
277    /// Enable a trailing stop after this many ticks.
278    pub fn trailing_stop_trigger_ticks(mut self, ticks: i32) -> Self {
279        self.trailing_stop_trigger_ticks = Some(ticks);
280        self
281    }
282
283    /// Track the trailing stop against the last trade instead of bid/offer.
284    pub fn trailing_stop_by_last_trade_price(mut self, by_last_trade_price: bool) -> Self {
285        self.trailing_stop_by_last_trade_price = Some(by_last_trade_price);
286        self
287    }
288
289    /// Convert the target to market-if-touched once touched.
290    pub fn target_market_order_if_touched(mut self, market_if_touched: bool) -> Self {
291        self.target_market_order_if_touched = Some(market_if_touched);
292        self
293    }
294
295    /// Convert the stop to market if the resting stop order is rejected.
296    pub fn stop_market_on_reject(mut self, market_on_reject: bool) -> Self {
297        self.stop_market_on_reject = Some(market_on_reject);
298        self
299    }
300
301    /// Convert the target to market at this second-since-beginning-of-epoch value.
302    pub fn target_market_at_ssboe(mut self, ssboe: i32) -> Self {
303        self.target_market_at_ssboe = Some(ssboe);
304        self
305    }
306
307    /// Microsecond component of the target's market-conversion time.
308    pub fn target_market_at_usecs(mut self, usecs: i32) -> Self {
309        self.target_market_at_usecs = Some(usecs);
310        self
311    }
312
313    /// Set both halves of the target's market-conversion time.
314    pub fn target_market_at(self, ssboe: i32, usecs: i32) -> Self {
315        self.target_market_at_ssboe(ssboe)
316            .target_market_at_usecs(usecs)
317    }
318
319    /// Convert the stop to market at this second-since-beginning-of-epoch value.
320    pub fn stop_market_at_ssboe(mut self, ssboe: i32) -> Self {
321        self.stop_market_at_ssboe = Some(ssboe);
322        self
323    }
324
325    /// Microsecond component of the stop's market-conversion time.
326    pub fn stop_market_at_usecs(mut self, usecs: i32) -> Self {
327        self.stop_market_at_usecs = Some(usecs);
328        self
329    }
330
331    /// Set both halves of the stop's market-conversion time.
332    pub fn stop_market_at(self, ssboe: i32, usecs: i32) -> Self {
333        self.stop_market_at_ssboe(ssboe).stop_market_at_usecs(usecs)
334    }
335
336    /// Convert the target to market after this many seconds.
337    pub fn target_market_order_after_secs(mut self, secs: i32) -> Self {
338        self.target_market_order_after_secs = Some(secs);
339        self
340    }
341
342    /// Release the order at this second-since-beginning-of-epoch value.
343    pub fn release_at_ssboe(mut self, ssboe: i32) -> Self {
344        self.release_at_ssboe = Some(ssboe);
345        self
346    }
347
348    /// Microsecond component of the release time.
349    pub fn release_at_usecs(mut self, usecs: i32) -> Self {
350        self.release_at_usecs = Some(usecs);
351        self
352    }
353
354    /// Set both halves of the release time.
355    pub fn release_at(self, ssboe: i32, usecs: i32) -> Self {
356        self.release_at_ssboe(ssboe).release_at_usecs(usecs)
357    }
358
359    /// Cancel the order at this second-since-beginning-of-epoch value.
360    pub fn cancel_at_ssboe(mut self, ssboe: i32) -> Self {
361        self.cancel_at_ssboe = Some(ssboe);
362        self
363    }
364
365    /// Microsecond component of the cancel time.
366    pub fn cancel_at_usecs(mut self, usecs: i32) -> Self {
367        self.cancel_at_usecs = Some(usecs);
368        self
369    }
370
371    /// Set both halves of the cancel time.
372    pub fn cancel_at(self, ssboe: i32, usecs: i32) -> Self {
373        self.cancel_at_ssboe(ssboe).cancel_at_usecs(usecs)
374    }
375
376    /// Cancel the order after this many seconds.
377    pub fn cancel_after_secs(mut self, secs: i32) -> Self {
378        self.cancel_after_secs = Some(secs);
379        self
380    }
381
382    /// Route to send on, overriding the route published for the exchange.
383    pub fn trade_route(mut self, trade_route: impl Into<String>) -> Self {
384        self.trade_route = Some(trade_route.into());
385        self
386    }
387
388    /// Whether this was done by a human or automatically.
389    pub fn manual_or_auto(mut self, manual_or_auto: ManualOrAutoEntry) -> Self {
390        self.manual_or_auto = manual_or_auto;
391        self
392    }
393
394    /// Window name to report this order under.
395    pub fn window_name(mut self, window_name: impl Into<String>) -> Self {
396        self.window_name = Some(window_name.into());
397        self
398    }
399
400    /// The `order_operation_type` sent to Rithmic.
401    pub fn operation_type(mut self, operation_type: BracketOperationType) -> Self {
402        self.operation_type = Some(operation_type);
403        self
404    }
405
406    /// Check the entry carries the prices its [`Self::price_type`] requires:
407    /// `Limit`, `StopLimit` and `LimitIfTouched` need [`Self::price`];
408    /// `StopMarket`, `StopLimit`, `MarketIfTouched` and `LimitIfTouched` need
409    /// [`Self::trigger_price`]. `Market` needs neither.
410    ///
411    /// Also check the exit legs hold together: each side's quantities and tick
412    /// distances pair up one to one, every leg's quantity is positive, and a
413    /// [`Self::bracket_type`] set by hand names the sides the legs actually
414    /// form. Tick distances themselves are not judged — Rithmic is the
415    /// authority on what it accepts.
416    pub fn validate(&self) -> Result<(), RithmicError> {
417        validate_instrument(&self.symbol, &self.exchange, self.quantity)?;
418
419        super::require_prices(self.price_type, self.price, self.trigger_price)?;
420
421        for (side, quantities, ticks) in [
422            ("target", &self.target_quantity, &self.target_ticks),
423            ("stop", &self.stop_quantity, &self.stop_ticks),
424        ] {
425            if quantities.len() != ticks.len() {
426                return Err(RithmicError::InvalidArgument(format!(
427                    "{side} legs are ragged: {} quantities for {} tick distances",
428                    quantities.len(),
429                    ticks.len()
430                )));
431            }
432
433            if let Some(quantity) = quantities.iter().find(|quantity| **quantity <= 0) {
434                return Err(RithmicError::InvalidArgument(format!(
435                    "every {side} leg needs a positive quantity, got {quantity} — \
436                     `target(..)`/`stop(..)` size the leg to the quantity set so far"
437                )));
438            }
439        }
440
441        if let Some(bracket_type) = self.bracket_type {
442            let wants = match bracket_type {
443                BracketType::TargetOnly | BracketType::TargetOnlyStatic => (true, false),
444                BracketType::StopOnly | BracketType::StopOnlyStatic => (false, true),
445                BracketType::TargetAndStop | BracketType::TargetAndStopStatic => (true, true),
446            };
447
448            let has = (!self.target_ticks.is_empty(), !self.stop_ticks.is_empty());
449
450            if has != wants {
451                return Err(RithmicError::InvalidArgument(format!(
452                    "bracket_type {} does not match the exit legs: {} target and {} stop",
453                    bracket_type.as_str_name(),
454                    self.target_ticks.len(),
455                    self.stop_ticks.len()
456                )));
457            }
458        }
459
460        Ok(())
461    }
462
463    /// Validate and return the order, deriving an unset [`Self::bracket_type`]
464    /// from the exit legs supplied.
465    pub fn build(mut self) -> Result<Self, RithmicError> {
466        self.validate()?;
467
468        if self.bracket_type.is_none() {
469            let has_targets = !self.target_ticks.is_empty();
470            let has_stops = !self.stop_ticks.is_empty();
471
472            self.bracket_type = match (has_targets, has_stops) {
473                (true, true) => Some(BracketType::TargetAndStopStatic),
474                (true, false) => Some(BracketType::TargetOnlyStatic),
475                (false, true) => Some(BracketType::StopOnlyStatic),
476                // No exit legs to describe, so invent no shape.
477                (false, false) => None,
478            };
479        }
480
481        Ok(self)
482    }
483}
484
485/// Adjust one leg of a bracket's profit target or stop loss.
486///
487/// The same shape serves `adjust_target` and `adjust_stop`.
488///
489/// # Example
490///
491/// ```
492/// use rithmic_rs::RithmicBracketLevelAdjustment;
493/// # fn main() -> Result<(), rithmic_rs::RithmicError> {
494/// // "123456" is the basket_id from the order notification.
495/// let adjustment = RithmicBracketLevelAdjustment::new()
496///     .id("123456")
497///     .ticks(16)
498///     .level(2)
499///     .build()?;
500/// # Ok(())
501/// # }
502/// ```
503#[derive(Debug, Clone, Default, PartialEq)]
504#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
505#[non_exhaustive]
506#[must_use = "an adjustment does nothing until passed to a plant handle"]
507pub struct RithmicBracketLevelAdjustment {
508    /// The `basket_id` from the order notification
509    pub id: String,
510    /// The new distance in ticks
511    pub ticks: i32,
512    /// Which bracket leg to adjust — a target leg via `adjust_target`, a stop
513    /// leg via `adjust_stop`.
514    pub level: Option<i32>,
515}
516
517impl RithmicBracketLevelAdjustment {
518    /// Start from the defaults.
519    pub fn new() -> Self {
520        Self::default()
521    }
522
523    /// The `basket_id` of the bracket to adjust.
524    pub fn id(mut self, id: impl Into<String>) -> Self {
525        self.id = id.into();
526        self
527    }
528
529    /// The new distance in ticks.
530    pub fn ticks(mut self, ticks: i32) -> Self {
531        self.ticks = ticks;
532        self
533    }
534
535    /// Which bracket leg to adjust — a target leg via `adjust_target`, a stop
536    /// leg via `adjust_stop`.
537    pub fn level(mut self, level: i32) -> Self {
538        self.level = Some(level);
539        self
540    }
541
542    /// Requires the basket_id of the bracket to adjust.
543    pub fn validate(&self) -> Result<(), RithmicError> {
544        if self.id.is_empty() {
545            return Err(RithmicError::InvalidArgument(
546                "an adjustment requires the basket_id of the bracket it adjusts".to_string(),
547            ));
548        }
549        Ok(())
550    }
551
552    /// Requires the basket_id of the bracket to adjust.
553    pub fn build(self) -> Result<Self, RithmicError> {
554        self.validate()?;
555        Ok(self)
556    }
557}
558
559#[cfg(test)]
560mod tests {
561    use super::*;
562
563    fn bracket(quantity: i32, price_type: OrderType) -> RithmicBracketOrder {
564        RithmicBracketOrder::new()
565            .symbol("ESH6")
566            .exchange("CME")
567            .quantity(quantity)
568            .action(OrderSide::Buy)
569            .price_type(price_type)
570    }
571
572    #[test]
573    fn a_bracket_validates_its_entry_leg() {
574        let mut order = RithmicBracketOrder {
575            price_type: OrderType::Limit,
576            ..bracket(1, OrderType::Limit)
577        };
578
579        assert!(order.validate().is_err());
580
581        order.price = Some(5000.0);
582        assert!(order.validate().is_ok());
583    }
584
585    /// The exit legs have to hold together as a structure — paired vectors,
586    /// positive sizes, a `bracket_type` that names the sides supplied. Tick
587    /// distances themselves are left to Rithmic to judge.
588    #[test]
589    fn a_bracket_checks_its_exit_legs_hold_together() {
590        // Mismatched vector lengths.
591        let ragged = RithmicBracketOrder {
592            target_quantity: vec![1],
593            target_ticks: vec![16, 24],
594            ..bracket(1, OrderType::Market)
595        };
596        assert!(ragged.validate().is_err());
597
598        // A zero-quantity leg.
599        let zero_sized = RithmicBracketOrder {
600            stop_quantity: vec![0],
601            stop_ticks: vec![10],
602            ..bracket(1, OrderType::Market)
603        };
604        assert!(zero_sized.validate().is_err());
605
606        // A bracket_type that disagrees with the legs supplied.
607        let mismatched = RithmicBracketOrder {
608            bracket_type: Some(BracketType::TargetOnly),
609            stop_quantity: vec![1],
610            stop_ticks: vec![10],
611            ..bracket(1, OrderType::Market)
612        };
613        assert!(mismatched.validate().is_err());
614
615        // No exit legs at all is still fine: template 330 carries the entry.
616        let bare = bracket(1, OrderType::Market);
617        assert!(bare.validate().is_ok());
618
619        // A hand-set bracket_type that agrees with the legs passes.
620        let matched = RithmicBracketOrder {
621            bracket_type: Some(BracketType::StopOnly),
622            stop_quantity: vec![1],
623            stop_ticks: vec![10],
624            ..bracket(1, OrderType::Market)
625        };
626        assert!(matched.validate().is_ok());
627    }
628
629    /// The ergonomic one-target/one-stop path has to produce exactly what the
630    /// explicit vectors produce, including the `Static` shape the crate has
631    /// always sent for a simple bracket.
632    #[test]
633    fn the_bracket_sugar_matches_the_explicit_vectors() {
634        let sugar = bracket(2, OrderType::Limit)
635            .price(5000.0)
636            .target(20)
637            .stop(10)
638            .build()
639            .unwrap();
640
641        let explicit = bracket(2, OrderType::Limit)
642            .price(5000.0)
643            .targets([(2, 20)])
644            .stops([(2, 10)])
645            .build()
646            .unwrap();
647
648        assert_eq!(sugar.target_quantity, explicit.target_quantity);
649        assert_eq!(sugar.target_ticks, explicit.target_ticks);
650        assert_eq!(sugar.stop_quantity, explicit.stop_quantity);
651        assert_eq!(sugar.stop_ticks, explicit.stop_ticks);
652        assert_eq!(sugar.bracket_type, explicit.bracket_type);
653        assert_eq!(
654            sugar.bracket_type,
655            Some(BracketType::TargetAndStopStatic),
656            "the simple path must stay byte-identical to what it sent before"
657        );
658    }
659
660    /// The sizing reads `quantity` where it stands, so the setter order that
661    /// looks equivalent is not — and the zero-sized leg the wrong order
662    /// produces is refused rather than sent.
663    #[test]
664    fn the_bracket_sugar_sizes_its_leg_to_the_quantity_set_so_far() {
665        let after = RithmicBracketOrder::new()
666            .symbol("ESH6")
667            .exchange("CME")
668            .price_type(OrderType::Market)
669            .quantity(3)
670            .target(20)
671            .build()
672            .unwrap();
673        assert_eq!(after.target_quantity, vec![3]);
674
675        let before = RithmicBracketOrder::new()
676            .symbol("ESH6")
677            .exchange("CME")
678            .price_type(OrderType::Market)
679            .target(20)
680            .quantity(3)
681            .build();
682        assert!(
683            before.is_err(),
684            "quantity set after the leg cannot reach back and resize it, \
685             so the zero-sized leg fails the build"
686        );
687    }
688
689    #[test]
690    fn the_bracket_derives_the_shape_from_the_legs() {
691        let target_only = bracket(1, OrderType::Market).target(20).build().unwrap();
692        assert_eq!(
693            target_only.bracket_type,
694            Some(BracketType::TargetOnlyStatic)
695        );
696
697        let stop_only = bracket(1, OrderType::Market).stop(10).build().unwrap();
698        assert_eq!(stop_only.bracket_type, Some(BracketType::StopOnlyStatic));
699
700        let explicit = bracket(1, OrderType::Market)
701            .stop(10)
702            .bracket_type(BracketType::StopOnly)
703            .build()
704            .unwrap();
705        assert_eq!(explicit.bracket_type, Some(BracketType::StopOnly));
706    }
707
708    /// With no exit legs there is no shape to derive, so `bracket_type` is left
709    /// unset rather than guessed at.
710    #[test]
711    fn a_bracket_leaves_the_shape_unset_when_there_are_no_exit_legs() {
712        assert_eq!(
713            bracket(1, OrderType::Market).build().unwrap().bracket_type,
714            None
715        );
716    }
717    #[test]
718    fn an_adjustment_requires_the_basket_id() {
719        assert!(
720            RithmicBracketLevelAdjustment::new()
721                .ticks(10)
722                .build()
723                .is_err()
724        );
725        assert!(
726            RithmicBracketLevelAdjustment::new()
727                .id("123456")
728                .ticks(10)
729                .build()
730                .is_ok()
731        );
732    }
733
734    #[test]
735    fn a_bracket_requires_its_identity() {
736        assert!(bracket(0, OrderType::Market).build().is_err());
737        assert!(bracket(1, OrderType::Market).symbol("").build().is_err());
738        assert!(bracket(1, OrderType::Market).build().is_ok());
739    }
740}