1use 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#[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 pub action: OrderSide,
65 pub duration: TimeInForce,
67 pub exchange: String,
69 pub localid: String,
71 pub price_type: OrderType,
73 pub price: Option<f64>,
75 pub trigger_price: Option<f64>,
77 pub quantity: i32,
83 pub symbol: String,
85 pub bracket_type: Option<BracketType>,
89 pub target_quantity: Vec<i32>,
91 pub target_ticks: Vec<i32>,
93 pub stop_quantity: Vec<i32>,
95 pub stop_ticks: Vec<i32>,
97 pub if_touched: Option<RithmicIfTouchedTrigger>,
99 pub break_even_ticks: Option<i32>,
101 pub break_even_trigger_ticks: Option<i32>,
103 pub trailing_stop_trigger_ticks: Option<i32>,
105 pub trailing_stop_by_last_trade_price: Option<bool>,
107 pub target_market_order_if_touched: Option<bool>,
109 pub stop_market_on_reject: Option<bool>,
111 pub target_market_at_ssboe: Option<i32>,
113 pub target_market_at_usecs: Option<i32>,
115 pub stop_market_at_ssboe: Option<i32>,
117 pub stop_market_at_usecs: Option<i32>,
119 pub target_market_order_after_secs: Option<i32>,
121 pub release_at_ssboe: Option<i32>,
123 pub release_at_usecs: Option<i32>,
125 pub cancel_at_ssboe: Option<i32>,
127 pub cancel_at_usecs: Option<i32>,
129 pub cancel_after_secs: Option<i32>,
131 pub trade_route: Option<String>,
133 pub manual_or_auto: ManualOrAutoEntry,
135 pub window_name: Option<String>,
137 pub operation_type: Option<BracketOperationType>,
140}
141
142impl RithmicBracketOrder {
155 pub fn new() -> Self {
157 Self::default()
158 }
159
160 pub fn symbol(mut self, symbol: impl Into<String>) -> Self {
162 self.symbol = symbol.into();
163 self
164 }
165
166 pub fn exchange(mut self, exchange: impl Into<String>) -> Self {
168 self.exchange = exchange.into();
169 self
170 }
171
172 pub fn quantity(mut self, quantity: i32) -> Self {
177 self.quantity = quantity;
178 self
179 }
180
181 pub fn action(mut self, action: OrderSide) -> Self {
183 self.action = action;
184 self
185 }
186
187 pub fn price_type(mut self, price_type: OrderType) -> Self {
189 self.price_type = price_type;
190 self
191 }
192
193 pub fn duration(mut self, duration: TimeInForce) -> Self {
195 self.duration = duration;
196 self
197 }
198
199 pub fn localid(mut self, localid: impl Into<String>) -> Self {
201 self.localid = localid.into();
202 self
203 }
204
205 pub fn price(mut self, price: f64) -> Self {
207 self.price = Some(price);
208 self
209 }
210
211 pub fn trigger_price(mut self, trigger_price: f64) -> Self {
213 self.trigger_price = Some(trigger_price);
214 self
215 }
216
217 pub fn bracket_type(mut self, bracket_type: BracketType) -> Self {
219 self.bracket_type = Some(bracket_type);
220 self
221 }
222
223 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 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 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 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 pub fn if_touched(mut self, if_touched: RithmicIfTouchedTrigger) -> Self {
261 self.if_touched = Some(if_touched);
262 self
263 }
264
265 pub fn break_even_ticks(mut self, ticks: i32) -> Self {
267 self.break_even_ticks = Some(ticks);
268 self
269 }
270
271 pub fn break_even_trigger_ticks(mut self, ticks: i32) -> Self {
273 self.break_even_trigger_ticks = Some(ticks);
274 self
275 }
276
277 pub fn trailing_stop_trigger_ticks(mut self, ticks: i32) -> Self {
279 self.trailing_stop_trigger_ticks = Some(ticks);
280 self
281 }
282
283 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 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 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 pub fn target_market_at_ssboe(mut self, ssboe: i32) -> Self {
303 self.target_market_at_ssboe = Some(ssboe);
304 self
305 }
306
307 pub fn target_market_at_usecs(mut self, usecs: i32) -> Self {
309 self.target_market_at_usecs = Some(usecs);
310 self
311 }
312
313 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 pub fn stop_market_at_ssboe(mut self, ssboe: i32) -> Self {
321 self.stop_market_at_ssboe = Some(ssboe);
322 self
323 }
324
325 pub fn stop_market_at_usecs(mut self, usecs: i32) -> Self {
327 self.stop_market_at_usecs = Some(usecs);
328 self
329 }
330
331 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 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 pub fn release_at_ssboe(mut self, ssboe: i32) -> Self {
344 self.release_at_ssboe = Some(ssboe);
345 self
346 }
347
348 pub fn release_at_usecs(mut self, usecs: i32) -> Self {
350 self.release_at_usecs = Some(usecs);
351 self
352 }
353
354 pub fn release_at(self, ssboe: i32, usecs: i32) -> Self {
356 self.release_at_ssboe(ssboe).release_at_usecs(usecs)
357 }
358
359 pub fn cancel_at_ssboe(mut self, ssboe: i32) -> Self {
361 self.cancel_at_ssboe = Some(ssboe);
362 self
363 }
364
365 pub fn cancel_at_usecs(mut self, usecs: i32) -> Self {
367 self.cancel_at_usecs = Some(usecs);
368 self
369 }
370
371 pub fn cancel_at(self, ssboe: i32, usecs: i32) -> Self {
373 self.cancel_at_ssboe(ssboe).cancel_at_usecs(usecs)
374 }
375
376 pub fn cancel_after_secs(mut self, secs: i32) -> Self {
378 self.cancel_after_secs = Some(secs);
379 self
380 }
381
382 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 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 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 pub fn operation_type(mut self, operation_type: BracketOperationType) -> Self {
402 self.operation_type = Some(operation_type);
403 self
404 }
405
406 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 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 (false, false) => None,
478 };
479 }
480
481 Ok(self)
482 }
483}
484
485#[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 pub id: String,
510 pub ticks: i32,
512 pub level: Option<i32>,
515}
516
517impl RithmicBracketLevelAdjustment {
518 pub fn new() -> Self {
520 Self::default()
521 }
522
523 pub fn id(mut self, id: impl Into<String>) -> Self {
525 self.id = id.into();
526 self
527 }
528
529 pub fn ticks(mut self, ticks: i32) -> Self {
531 self.ticks = ticks;
532 self
533 }
534
535 pub fn level(mut self, level: i32) -> Self {
538 self.level = Some(level);
539 self
540 }
541
542 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 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 #[test]
589 fn a_bracket_checks_its_exit_legs_hold_together() {
590 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 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 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 let bare = bracket(1, OrderType::Market);
617 assert!(bare.validate().is_ok());
618
619 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 #[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 #[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 #[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}