diff --git a/Cargo.toml b/Cargo.toml index 73033333..8ea17cb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ibapi" -version = "2.6.1" +version = "2.6.2" edition = "2021" authors = ["Wil Boayue "] description = "A Rust implementation of the Interactive Brokers TWS API, providing a reliable and user friendly interface for TWS and IB Gateway. Designed with a focus on simplicity and performance." diff --git a/src/orders/builder/order_builder.rs b/src/orders/builder/order_builder.rs index 00d4f3c6..072262fc 100644 --- a/src/orders/builder/order_builder.rs +++ b/src/orders/builder/order_builder.rs @@ -928,10 +928,19 @@ fn set_conjunction(condition: &mut OrderCondition, is_conjunction: bool) { } } +/// Entry order type for bracket orders +#[derive(Default)] +enum BracketEntryType { + #[default] + None, + Limit(f64), + Market, +} + /// Builder for bracket orders pub struct BracketOrderBuilder<'a, C> { pub(crate) parent_builder: OrderBuilder<'a, C>, - entry_price: Option, + entry_type: BracketEntryType, take_profit_price: Option, stop_loss_price: Option, } @@ -940,15 +949,21 @@ impl<'a, C> BracketOrderBuilder<'a, C> { fn new(parent_builder: OrderBuilder<'a, C>) -> Self { Self { parent_builder, - entry_price: None, + entry_type: BracketEntryType::None, take_profit_price: None, stop_loss_price: None, } } + /// Set entry as market order (immediate execution) + pub fn entry_market(mut self) -> Self { + self.entry_type = BracketEntryType::Market; + self + } + /// Set entry limit price pub fn entry_limit(mut self, price: impl Into) -> Self { - self.entry_price = Some(price.into()); + self.entry_type = BracketEntryType::Limit(price.into()); self } @@ -966,26 +981,35 @@ impl<'a, C> BracketOrderBuilder<'a, C> { /// Build bracket orders with full validation pub fn build(mut self) -> Result, ValidationError> { - // Validate and convert prices - let entry_price_raw = self.entry_price.ok_or(ValidationError::MissingRequiredField("entry_price"))?; + // Validate and convert take profit and stop loss prices let take_profit_raw = self.take_profit_price.ok_or(ValidationError::MissingRequiredField("take_profit"))?; let stop_loss_raw = self.stop_loss_price.ok_or(ValidationError::MissingRequiredField("stop_loss"))?; - let entry_price = Price::new(entry_price_raw)?; let take_profit = Price::new(take_profit_raw)?; let stop_loss = Price::new(stop_loss_raw)?; - // Validate bracket order prices - validation::validate_bracket_prices( - self.parent_builder.action.as_ref(), - entry_price.value(), - take_profit.value(), - stop_loss.value(), - )?; - - // Set the entry limit price on parent builder - self.parent_builder.order_type = Some(OrderType::Limit); - self.parent_builder.limit_price = Some(entry_price.value()); + // Set order type based on entry type + match self.entry_type { + BracketEntryType::None => { + return Err(ValidationError::MissingRequiredField("entry (use entry_limit() or entry_market())")); + } + BracketEntryType::Limit(price) => { + let entry_price = Price::new(price)?; + // Validate bracket order prices + validation::validate_bracket_prices( + self.parent_builder.action.as_ref(), + entry_price.value(), + take_profit.value(), + stop_loss.value(), + )?; + self.parent_builder.order_type = Some(OrderType::Limit); + self.parent_builder.limit_price = Some(entry_price.value()); + } + BracketEntryType::Market => { + // Skip price relationship validation for market orders + self.parent_builder.order_type = Some(OrderType::Market); + } + } // Build parent order let mut parent = self.parent_builder.build()?; diff --git a/src/orders/builder/order_builder/tests.rs b/src/orders/builder/order_builder/tests.rs index 759cb73c..02ec1674 100644 --- a/src/orders/builder/order_builder/tests.rs +++ b/src/orders/builder/order_builder/tests.rs @@ -542,7 +542,7 @@ fn test_bracket_order_validation_buy() { } #[test] -fn test_bracket_order_missing_entry_price() { +fn test_bracket_order_missing_entry() { let client = MockClient; let contract = create_test_contract(); @@ -550,7 +550,7 @@ fn test_bracket_order_missing_entry_price() { let result = bracket.build(); assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("entry_price")); + assert!(result.unwrap_err().to_string().contains("entry")); } #[test] @@ -799,6 +799,135 @@ fn test_bracket_order_with_missing_action() { assert!(result.is_err()); } +// ===== Market Entry Bracket Order Tests ===== + +#[test] +fn test_bracket_order_market_entry_buy() { + let client = MockClient; + let contract = create_test_contract(); + + let bracket = OrderBuilder::new(&client, &contract) + .buy(100) + .bracket() + .entry_market() + .take_profit(55.0) + .stop_loss(45.0); + + let orders = bracket.build().unwrap(); + assert_eq!(orders.len(), 3); + + // Verify parent order is market order + let parent = &orders[0]; + assert_eq!(parent.action, Action::Buy); + assert_eq!(parent.order_type, "MKT"); + assert_eq!(parent.limit_price, None); + assert!(!parent.transmit); + + // Verify take profit details + let tp = &orders[1]; + assert_eq!(tp.action, Action::Sell); + assert_eq!(tp.order_type, "LMT"); + assert_eq!(tp.limit_price, Some(55.0)); + assert_eq!(tp.parent_id, parent.order_id); + assert!(!tp.transmit); + + // Verify stop loss details + let sl = &orders[2]; + assert_eq!(sl.action, Action::Sell); + assert_eq!(sl.order_type, "STP"); + assert_eq!(sl.aux_price, Some(45.0)); + assert_eq!(sl.parent_id, parent.order_id); + assert!(sl.transmit); +} + +#[test] +fn test_bracket_order_market_entry_sell() { + let client = MockClient; + let contract = create_test_contract(); + + let bracket = OrderBuilder::new(&client, &contract) + .sell(100) + .bracket() + .entry_market() + .take_profit(45.0) + .stop_loss(55.0); + + let orders = bracket.build().unwrap(); + assert_eq!(orders.len(), 3); + + // Verify parent order is market order with Sell action + let parent = &orders[0]; + assert_eq!(parent.action, Action::Sell); + assert_eq!(parent.order_type, "MKT"); + + // Verify child orders have reversed action + let tp = &orders[1]; + assert_eq!(tp.action, Action::Buy); + + let sl = &orders[2]; + assert_eq!(sl.action, Action::Buy); +} + +#[test] +fn test_bracket_order_market_entry_inherits_outside_rth() { + let client = MockClient; + let contract = create_test_contract(); + + let bracket = OrderBuilder::new(&client, &contract) + .buy(100) + .outside_rth() + .bracket() + .entry_market() + .take_profit(55.0) + .stop_loss(45.0); + + let orders = bracket.build().unwrap(); + + // All orders should inherit outside_rth from parent + assert!(orders[0].outside_rth, "Parent should have outside_rth"); + assert!(orders[1].outside_rth, "Take profit should inherit outside_rth"); + assert!(orders[2].outside_rth, "Stop loss should inherit outside_rth"); +} + +#[test] +fn test_bracket_order_market_entry_quantity_propagation() { + let client = MockClient; + let contract = create_test_contract(); + + let bracket = OrderBuilder::new(&client, &contract) + .buy(500) + .bracket() + .entry_market() + .take_profit(55.0) + .stop_loss(45.0); + + let orders = bracket.build().unwrap(); + + // All orders should have the same quantity + assert_eq!(orders[0].total_quantity, 500.0); + assert_eq!(orders[1].total_quantity, 500.0); + assert_eq!(orders[2].total_quantity, 500.0); +} + +#[test] +fn test_bracket_order_market_entry_parent_id_propagation() { + let client = MockClient; + let contract = create_test_contract(); + + let bracket = OrderBuilder::new(&client, &contract) + .buy(100) + .bracket() + .entry_market() + .take_profit(55.0) + .stop_loss(45.0); + + let orders = bracket.build().unwrap(); + let parent_id = orders[0].order_id; + + assert_eq!(orders[1].parent_id, parent_id); + assert_eq!(orders[2].parent_id, parent_id); +} + #[test] fn test_market_on_close() { let client = MockClient;