Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ibapi"
version = "2.6.1"
version = "2.6.2"
edition = "2021"
authors = ["Wil Boayue <wil@wsbsolutions.com>"]
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."
Expand Down
58 changes: 41 additions & 17 deletions src/orders/builder/order_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<f64>,
entry_type: BracketEntryType,
take_profit_price: Option<f64>,
stop_loss_price: Option<f64>,
}
Expand All @@ -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<f64>) -> Self {
self.entry_price = Some(price.into());
self.entry_type = BracketEntryType::Limit(price.into());
self
}

Expand All @@ -966,26 +981,35 @@ impl<'a, C> BracketOrderBuilder<'a, C> {

/// Build bracket orders with full validation
pub fn build(mut self) -> Result<Vec<Order>, 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()?;
Expand Down
133 changes: 131 additions & 2 deletions src/orders/builder/order_builder/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -542,15 +542,15 @@ 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();

let bracket = OrderBuilder::new(&client, &contract).buy(100).bracket().take_profit(55.0).stop_loss(45.0);

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]
Expand Down Expand Up @@ -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;
Expand Down