From 63bbb4e01657831672e5b114fdd9270e7c9c3d7f Mon Sep 17 00:00:00 2001 From: userFRM Date: Tue, 28 Jul 2026 17:37:22 +0200 Subject: [PATCH 1/4] orders: route the adjustable stop through the shared encoder (ibx#240) `build_order_request` returned early into a standalone `SubmitAdjustableStop` request, which bypassed the extended-attributes path every other order type goes through. The dedicated encoder arm emitted neither tag 6107 nor 583 and hard-coded 59=0, so an adjustable stop used as a bracket child shipped unlinked from its parent, outside its OCA group and DAY. The bypass dropped the rest of `OrderAttrs` with them: outside-RTH, hidden, display size, trigger method, conditions and GTD expiry were all silently discarded on this path. The adjustable stop is now an `OrderKind::AdjustableStop` carried by `SubmitEx`, so it encodes through `send_order_ex` like every other kind and picks up the shared attribute block. The wire layout is unchanged: 40=3 and 99 sit with the other order-type tags, and the 6257/6261/6258/6259 group plus the conditional 6262 and 6260/6269 are appended after 204 and the attribute block, which is where the encoder being replaced put them. Tag order should not carry meaning, but this path had a shipped layout and there was no reason to move it as a side effect. `Context::submit_adjustable_stop` takes `tif` and `attrs` to match the other extended submitters. `Connection::for_test()` is new test-only plumbing: it hands back the peer socket so a test can assert on the bytes an encoder actually writes. Two regression tests use it: one pins 6107, 583 and 59 for a bracket child, the other pins the conditional 6262/6260/6269, the absence of 6107/583 when no parent or OCA is set, and the relative order of the whole group. The enum-level tests passed unchanged for the whole time the child was shipping naked, which is why these assert on the wire instead. Co-Authored-By: Claude Opus 5 (1M context) --- src/api/client/tests.rs | 47 ++++++- src/client_core.rs | 22 ++-- src/engine/context.rs | 29 +++- src/engine/hot_loop/order_builder.rs | 189 +++++++++++++++++++-------- src/protocol/connection.rs | 21 +++ src/types.rs | 59 ++++----- tests/ib_paper_compat/common.rs | 3 +- tests/ib_paper_compat/orders.rs | 9 +- 8 files changed, 264 insertions(+), 115 deletions(-) diff --git a/src/api/client/tests.rs b/src/api/client/tests.rs index 98f2fe81..48dda260 100644 --- a/src/api/client/tests.rs +++ b/src/api/client/tests.rs @@ -361,7 +361,7 @@ fn place_order_trailing_stop_without_trigger_is_unset() { #[test] fn place_order_adjustable_trail_carries_trailing_amount_and_unit() { // ibx#225 / ib-agent#167: a base STP that converts to a TRAIL must carry - // the trailing amount and unit through to the SubmitAdjustableStop request. + // the trailing amount and unit through to the AdjustableStop request. let (client, rx, shared) = test_client(); shared.market.set_instrument_count(1); let order = Order { @@ -378,9 +378,9 @@ fn place_order_adjustable_trail_carries_trailing_amount_and_unit() { let cmd = rx.try_recv().unwrap(); match cmd { - ControlCommand::Order(OrderRequest::SubmitAdjustableStop { + ControlCommand::Order(OrderRequest::SubmitEx { kind: crate::types::OrderKind::AdjustableStop { adjusted_order_type, stop_price, trigger_price, adjusted_stop_price, - adjusted_trailing_amount, adjustable_trailing_unit, .. }) => { + adjusted_trailing_amount, adjustable_trailing_unit, .. }, .. }) => { assert_eq!(adjusted_order_type, crate::types::AdjustedOrderType::Trail); assert_eq!(stop_price, (11.00 * PRICE_SCALE_F) as i64); assert_eq!(trigger_price, (11.00 * PRICE_SCALE_F) as i64); @@ -388,7 +388,40 @@ fn place_order_adjustable_trail_carries_trailing_amount_and_unit() { assert_eq!(adjusted_trailing_amount, (0.50 * PRICE_SCALE_F) as i64); assert_eq!(adjustable_trailing_unit, 0); } - _ => panic!("expected SubmitAdjustableStop, got {:?}", cmd), + _ => panic!("expected SubmitEx carrying AdjustableStop, got {:?}", cmd), + } +} + +#[test] +fn place_order_adjustable_stop_carries_bracket_attrs_and_tif() { + // ibx#240: an adjustable stop used as a bracket child must stay linked to + // its parent and its OCA group and keep the caller's tif. Routing it around + // the extended-attrs path shipped the child naked, unlinked and DAY. + let (client, rx, shared) = test_client(); + shared.market.set_instrument_count(1); + let order = Order { + action: "SELL".into(), total_quantity: 1.0, order_type: "STP".into(), + aux_price: 11.00, + adjusted_order_type: "STP".into(), + trigger_price: 12.00, + adjusted_stop_price: 11.50, + parent_id: 42, + oca_group: "bracket_1".into(), + oca_type: 1, + tif: "GTC".into(), + ..Default::default() + }; + client.place_order(7, &spy(), &order).unwrap(); + + match rx.try_recv().unwrap() { + ControlCommand::Order(OrderRequest::SubmitEx { kind, tif, attrs, .. }) => { + assert!(matches!(kind, crate::types::OrderKind::AdjustableStop { .. }), + "adjustable stop must route through the extended path; got {:?}", kind); + assert_eq!(tif, b'1', "tif must survive as GTC"); + assert_eq!(attrs.parent_id, 42, "bracket child must stay linked to its parent"); + assert_eq!(attrs.oca_group_str, "bracket_1", "OCA group must survive"); + } + cmd => panic!("expected SubmitEx carrying AdjustableStop, got {:?}", cmd), } } @@ -408,12 +441,12 @@ fn place_order_adjustable_trail_percent_unit_passes_through() { client.place_order(1, &spy(), &order).unwrap(); match rx.try_recv().unwrap() { - ControlCommand::Order(OrderRequest::SubmitAdjustableStop { - adjustable_trailing_unit, adjusted_trailing_amount, .. }) => { + ControlCommand::Order(OrderRequest::SubmitEx { kind: crate::types::OrderKind::AdjustableStop { + adjustable_trailing_unit, adjusted_trailing_amount, .. }, .. }) => { assert_eq!(adjustable_trailing_unit, 100); assert_eq!(adjusted_trailing_amount, (1.00 * PRICE_SCALE_F) as i64); } - cmd => panic!("expected SubmitAdjustableStop, got {:?}", cmd), + cmd => panic!("expected SubmitEx carrying AdjustableStop, got {:?}", cmd), } } diff --git a/src/client_core.rs b/src/client_core.rs index 3aaeb264..cb8f24a4 100644 --- a/src/client_core.rs +++ b/src/client_core.rs @@ -1468,15 +1468,21 @@ impl ClientCore { } else { order.adjusted_trailing_amount }; - return Ok(ControlCommand::Order(OrderRequest::SubmitAdjustableStop { + // Through SubmitEx like every other order type, so a bracket child + // keeps its parent link, its OCA group and its tif (ibx#240). + return Ok(ControlCommand::Order(OrderRequest::SubmitEx { order_id, instrument, side, qty, - stop_price: scale(order.aux_price), - trigger_price: scale(order.trigger_price), - adjusted_order_type: adjusted, - adjusted_stop_price: scale(order.adjusted_stop_price), - adjusted_stop_limit_price: scale(order.adjusted_stop_limit_price), - adjusted_trailing_amount: scale(adj_trail), - adjustable_trailing_unit: order.adjustable_trailing_unit, + kind: OrderKind::AdjustableStop { + stop_price: scale(order.aux_price), + trigger_price: scale(order.trigger_price), + adjusted_order_type: adjusted, + adjusted_stop_price: scale(order.adjusted_stop_price), + adjusted_stop_limit_price: scale(order.adjusted_stop_limit_price), + adjusted_trailing_amount: scale(adj_trail), + adjustable_trailing_unit: order.adjustable_trailing_unit, + }, + tif: order.tif_byte(), + attrs: order.attrs(), })); } diff --git a/src/engine/context.rs b/src/engine/context.rs index 0c39e6a1..78be9669 100644 --- a/src/engine/context.rs +++ b/src/engine/context.rs @@ -821,6 +821,10 @@ impl Context { } /// Submit an adjustable stop order. Adjusts to a different order type when trigger is hit. + /// Takes `tif` and `attrs` like the other extended submitters: an adjustable + /// stop is a normal bracket child, so it needs its parent link and OCA group + /// (ibx#240). `tif`: b'0' = DAY, b'1' = GTC, b'6' = GTD. + #[allow(clippy::too_many_arguments)] pub fn submit_adjustable_stop( &mut self, instrument: InstrumentId, @@ -833,13 +837,19 @@ impl Context { adjusted_stop_limit_price: Price, adjusted_trailing_amount: Price, adjustable_trailing_unit: i32, + tif: u8, + attrs: OrderAttrs, ) -> OrderId { let id = self.next_order_id; self.next_order_id += 1; - self.pending_orders.push(OrderRequest::SubmitAdjustableStop { - order_id: id, instrument, side, qty, stop_price, trigger_price, - adjusted_order_type, adjusted_stop_price, adjusted_stop_limit_price, - adjusted_trailing_amount, adjustable_trailing_unit, + self.pending_orders.push(OrderRequest::SubmitEx { + order_id: id, instrument, side, qty, + kind: OrderKind::AdjustableStop { + stop_price, trigger_price, adjusted_order_type, adjusted_stop_price, + adjusted_stop_limit_price, adjusted_trailing_amount, adjustable_trailing_unit, + }, + tif, + attrs, }); id } @@ -1683,12 +1693,15 @@ mod tests { 252_20 * (PRICE_SCALE / 100), // adjusted_limit 0, // adjusted_trailing_amount (StopLimit: unused) 0, // adjustable_trailing_unit + b'1', // GTC + OrderAttrs { parent_id: 9, ..Default::default() }, ); let orders: Vec<_> = ctx.drain_pending_orders().collect(); assert_eq!(orders.len(), 1); match &orders[0] { - OrderRequest::SubmitAdjustableStop { order_id, side, qty, stop_price, - trigger_price, adjusted_order_type, adjusted_stop_price, adjusted_stop_limit_price, .. } => { + OrderRequest::SubmitEx { order_id, side, qty, kind: OrderKind::AdjustableStop { + stop_price, trigger_price, adjusted_order_type, adjusted_stop_price, + adjusted_stop_limit_price, .. }, tif, attrs, .. } => { assert_eq!(*order_id, id); assert_eq!(*side, Side::Sell); assert_eq!(*qty, 1); @@ -1697,8 +1710,10 @@ mod tests { assert_eq!(*adjusted_order_type, AdjustedOrderType::StopLimit); assert_eq!(*adjusted_stop_price, 253_20 * (PRICE_SCALE / 100)); assert_eq!(*adjusted_stop_limit_price, 252_20 * (PRICE_SCALE / 100)); + assert_eq!(*tif, b'1'); + assert_eq!(attrs.parent_id, 9); } - _ => panic!("expected SubmitAdjustableStop"), + _ => panic!("expected SubmitEx carrying AdjustableStop"), } } } diff --git a/src/engine/hot_loop/order_builder.rs b/src/engine/hot_loop/order_builder.rs index bb7cb4b2..a6b95f78 100644 --- a/src/engine/hot_loop/order_builder.rs +++ b/src/engine/hot_loop/order_builder.rs @@ -1039,64 +1039,6 @@ pub(crate) fn drain_and_send_orders( (204, "0"), ]) } - OrderRequest::SubmitAdjustableStop { order_id, instrument, side, qty, - stop_price, trigger_price, adjusted_order_type, - adjusted_stop_price, adjusted_stop_limit_price, - adjusted_trailing_amount, adjustable_trailing_unit } => { - context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, 0, b'3', b'0', stop_price, - )); - let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); - let clord_str = format!("{}.{}", order_id, ver); - let side_str = fix_side(side); - let qty_str = format_uint(qty as u64); - let stop_str = format_price(stop_price); - let trigger_str = format_price(trigger_price); - let adj_stop_str = format_price(adjusted_stop_price); - let adj_limit_str = format_price(adjusted_stop_limit_price); - let adj_trail_str = format_price(adjusted_trailing_amount); - let adj_unit_str = adjustable_trailing_unit.to_string(); - let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, destination) = context.market.order_routing(instrument); - let now = chrono_free_timestamp(); - let mut fields: Vec<(u32, &str)> = vec![ - (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), - (fix::TAG_SENDING_TIME, &now), - (11, &clord_str), - (1, account_id), - (21, "2"), - (55, &symbol), - (54, side_str), - (38, &qty_str), - (40, "3"), // OrdType = Stop - (99, &stop_str), // StopPx - (59, "0"), - (60, &now), - (167, &sec_type_str), - (100, &destination), - (6210, &destination), - (15, "USD"), - (204, "0"), - (6257, "1"), // Has adjustable params flag - (6261, adjusted_order_type.fix_code()), // Adjusted order type - (6258, &trigger_str), // Trigger price - (6259, &adj_stop_str), // Adjusted stop price - ]; - if adjusted_stop_limit_price > 0 { - fields.push((6262, &adj_limit_str)); // Adjusted stop limit price - } - // When the stop converts to a trailing type, carry the trailing - // amount (6260) and its unit (6269: 0=amount, 100=percent). - // Captured in ib-agent#167 (ibx#225). - if matches!(adjusted_order_type, - crate::types::AdjustedOrderType::Trail - | crate::types::AdjustedOrderType::TrailLimit) - { - fields.push((6260, &adj_trail_str)); - fields.push((6269, &adj_unit_str)); - } - conn.send_fix(&fields) - } OrderRequest::SubmitMtl { order_id, instrument, side, qty } => { context.insert_order(crate::types::Order::new( order_id, instrument, side, qty, 0, b'K', b'0', 0, @@ -1632,6 +1574,7 @@ fn send_order_ex( K::PegMkt { offset } => (crate::types::ORD_PEG_MKT, 0, offset), K::PegMid { offset } => (crate::types::ORD_PEG_MID, 0, offset), K::Rel { offset } => (b'R', 0, offset), + K::AdjustableStop { stop_price, .. } => (b'3', 0, stop_price), }; context.insert_order(crate::types::Order::new( order_id, instrument, side, qty, track_price, ord_type_byte, tif, track_stop, @@ -1677,6 +1620,13 @@ fn send_order_ex( fields.push((44, format_price(price).to_string())); fields.push((99, format_price(stop_price).to_string())); } + K::AdjustableStop { stop_price, .. } => { + // Base order type only. The 6257+ adjustable tags are appended after + // the attribute block below, where the dedicated encoder this path + // replaced put them. + fields.push((40, "3".to_string())); // OrdType = Stop + fields.push((99, format_price(stop_price).to_string())); // StopPx + } K::TrailingStop { trail_amt, trail_stop_price } => { // Per ib-agent#136 capture: amount-based trailing stop carries // the trail amount in both 99 and 211 and requires 18=a. @@ -1865,6 +1815,32 @@ fn send_order_ex( } } + // Adjustable-stop tags last, keeping the position they held in the encoder + // this path replaced: after 204 and the attribute block, not in among the + // order-type tags. Values and conditions are unchanged; only the encoder + // they come from is new (ibx#240). + if let K::AdjustableStop { + trigger_price, adjusted_order_type, adjusted_stop_price, + adjusted_stop_limit_price, adjusted_trailing_amount, adjustable_trailing_unit, .. + } = kind { + fields.push((6257, "1".to_string())); // has adjustable params + fields.push((6261, adjusted_order_type.fix_code().to_string())); + fields.push((6258, format_price(trigger_price).to_string())); + fields.push((6259, format_price(adjusted_stop_price).to_string())); + if adjusted_stop_limit_price > 0 { + fields.push((6262, format_price(adjusted_stop_limit_price).to_string())); + } + // Trailing amount + unit for a Trail/TrailLimit conversion + // (ib-agent#167, ibx#225). + if matches!(adjusted_order_type, + crate::types::AdjustedOrderType::Trail + | crate::types::AdjustedOrderType::TrailLimit) + { + fields.push((6260, format_price(adjusted_trailing_amount).to_string())); + fields.push((6269, adjustable_trailing_unit.to_string())); + } + } + let refs: Vec<(u32, &str)> = fields.iter().map(|(t, s)| (*t, s.as_str())).collect(); conn.send_fix(&refs) } @@ -2056,4 +2032,101 @@ mod tests { assert_eq!(context.order(8).unwrap().status, OrderStatus::Filled); assert!(shared.orders.drain_order_updates().is_empty()); } + + /// ibx#240: the tags a bracket child cannot ship without. Asserted on the + /// bytes `send_order_ex` puts on the wire, not on the request enum — the + /// enum-level tests passed throughout the period the child shipped naked. + #[test] + fn adjustable_stop_wire_carries_parent_oca_and_tif() { + use std::io::Read; + let (mut conn, mut peer) = crate::protocol::connection::Connection::for_test(); + let mut context = Context::new(); + let attrs = crate::types::OrderAttrs { + parent_id: 42, + oca_group_str: "bracket_1".to_string(), + oca_type: 1, + ..Default::default() + }; + send_order_ex( + &mut conn, &mut context, "DU123456", 7, 0, Side::Sell, 1, + crate::types::OrderKind::AdjustableStop { + stop_price: 11 * crate::types::PRICE_SCALE, + trigger_price: 12 * crate::types::PRICE_SCALE, + adjusted_order_type: crate::types::AdjustedOrderType::Stop, + adjusted_stop_price: 11 * crate::types::PRICE_SCALE + crate::types::PRICE_SCALE / 2, + adjusted_stop_limit_price: 0, + adjusted_trailing_amount: 0, + adjustable_trailing_unit: 0, + }, + b'1', // GTC + &attrs, + ).unwrap(); + + let mut buf = [0u8; 4096]; + let n = peer.read(&mut buf).unwrap(); + let msg = String::from_utf8_lossy(&buf[..n]); + let tag = |t: &str| msg.split('\u{1}').find_map(|f| f.strip_prefix(t).map(str::to_string)); + + assert_eq!(tag("6107=").as_deref(), Some("42.0"), "parent link missing: {}", msg); + assert_eq!(tag("583=").as_deref(), Some("bracket_1"), "OCA group missing: {}", msg); + assert_eq!(tag("59=").as_deref(), Some("1"), "tif must be GTC, not DAY: {}", msg); + // The adjustable-specific tags keep both the values and the position the + // standalone arm gave them — after 204 and the attribute block — which + // the sibling test pins by asserting 204 precedes 6257. + assert_eq!(tag("40=").as_deref(), Some("3")); + assert_eq!(tag("99="), Some(format_price(11 * crate::types::PRICE_SCALE).to_string())); + assert_eq!(tag("6257=").as_deref(), Some("1")); + assert_eq!(tag("6261=").as_deref(), Some(crate::types::AdjustedOrderType::Stop.fix_code())); + assert_eq!(tag("6258="), Some(format_price(12 * crate::types::PRICE_SCALE).to_string())); + assert_eq!(tag("6259="), + Some(format_price(11 * crate::types::PRICE_SCALE + crate::types::PRICE_SCALE / 2).to_string())); + } + + /// The conditional adjustable tags: 6262 only with a stop-limit conversion, + /// 6260/6269 only with a trailing one. Same rules as the standalone arm. + #[test] + fn adjustable_stop_wire_carries_trail_and_limit_tags() { + use std::io::Read; + let (mut conn, mut peer) = crate::protocol::connection::Connection::for_test(); + let mut context = Context::new(); + send_order_ex( + &mut conn, &mut context, "DU123456", 8, 0, Side::Sell, 1, + crate::types::OrderKind::AdjustableStop { + stop_price: 11 * crate::types::PRICE_SCALE, + trigger_price: 12 * crate::types::PRICE_SCALE, + adjusted_order_type: crate::types::AdjustedOrderType::TrailLimit, + adjusted_stop_price: 11 * crate::types::PRICE_SCALE, + adjusted_stop_limit_price: 10 * crate::types::PRICE_SCALE, + adjusted_trailing_amount: crate::types::PRICE_SCALE / 2, + adjustable_trailing_unit: 0, + }, + b'0', + &crate::types::OrderAttrs::default(), + ).unwrap(); + + let mut buf = [0u8; 4096]; + let n = peer.read(&mut buf).unwrap(); + let msg = String::from_utf8_lossy(&buf[..n]); + let tag = |t: &str| msg.split('\u{1}').find_map(|f| f.strip_prefix(t).map(str::to_string)); + + assert_eq!(tag("6262="), Some(format_price(10 * crate::types::PRICE_SCALE).to_string())); + assert_eq!(tag("6260="), Some(format_price(crate::types::PRICE_SCALE / 2).to_string())); + assert_eq!(tag("6269=").as_deref(), Some("0")); + // No parent, no OCA set: those tags must be absent, not empty. + assert_eq!(tag("6107="), None); + assert_eq!(tag("583="), None); + + // Order, not just presence: the adjustable tags sit after 204 and the + // base type tags before 59, exactly where the dedicated encoder this + // path replaced put them. Tag order is not supposed to carry meaning, + // but this path had a shipped layout and there is no reason to change + // it as a side effect (ibx#240). + let pos = |t: &str| msg.split('\u{1}').position(|f| f.starts_with(t)); + assert!(pos("40=") < pos("59="), "base type tags precede tif: {}", msg); + assert!(pos("99=") < pos("59="), "stop price precedes tif: {}", msg); + assert!(pos("204=") < pos("6257="), "adjustable tags follow 204: {}", msg); + assert!(pos("6257=") < pos("6261="), "adjustable tags keep their order: {}", msg); + assert!(pos("6259=") < pos("6262="), "adjustable tags keep their order: {}", msg); + assert!(pos("6262=") < pos("6260="), "adjustable tags keep their order: {}", msg); + } } diff --git a/src/protocol/connection.rs b/src/protocol/connection.rs index 7b65de4e..3cb0e11e 100644 --- a/src/protocol/connection.rs +++ b/src/protocol/connection.rs @@ -286,6 +286,27 @@ impl Connection { (undistorted, valid) } + /// Test-only: a `Connection` whose writes land on the returned peer socket, + /// so a test can assert on the bytes an encoder actually puts on the wire. + #[cfg(test)] + pub(crate) fn for_test() -> (Connection, std::net::TcpStream) { + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); + let client = std::net::TcpStream::connect(addr).unwrap(); + let (peer, _) = listener.accept().unwrap(); + peer.set_read_timeout(Some(std::time::Duration::from_secs(5))).unwrap(); + let conn = Connection { + stream: Stream::Raw(client), + buf: Vec::new(), + seq: 0, + sign_key: Vec::new(), + sign_iv: Vec::new(), + read_key: Vec::new(), + read_iv: Vec::new(), + }; + (conn, peer) + } + /// Build a FIX message, sign it, and send it. Increments seq and chains sign IV. /// /// State (seq, sign_iv) is committed only after `write_all` returns Ok, diff --git a/src/types.rs b/src/types.rs index fe09f6b5..0eaf1b85 100644 --- a/src/types.rs +++ b/src/types.rs @@ -508,6 +508,24 @@ pub enum OrderKind { PegMkt { offset: Price }, PegMid { offset: Price }, Rel { offset: Price }, + /// Stop that converts to another order type once `trigger_price` is hit. + /// Tags: 6257=1, 6261=adjusted type, 6258=trigger, 6259=adjusted stop, + /// 6262=adjusted limit, 6260/6269=trailing amount + unit. + AdjustableStop { + stop_price: Price, + trigger_price: Price, + adjusted_order_type: AdjustedOrderType, + adjusted_stop_price: Price, + /// Only used when adjusted_order_type is StopLimit or TrailLimit. 0 = not set. + adjusted_stop_limit_price: Price, + /// Trailing amount for a Trail/TrailLimit conversion (tag 6260). When the + /// unit is amount it is a price offset (scaled); when percent it is the + /// percent value scaled (1.00% = PRICE_SCALE). 0 = not set. + adjusted_trailing_amount: Price, + /// Unit of `adjusted_trailing_amount` on the wire (tag 6269): 0 = amount, + /// 100 = percent. Other values are rejected by the gateway. + adjustable_trailing_unit: i32, + }, } /// Order request sent via control channel, processed by engine. @@ -833,27 +851,6 @@ pub enum OrderRequest { qty: Qty, // QTY_SCALE fixed-point price: Price, }, - /// Adjustable stop: a stop order that adjusts to a different order type when trigger_price is hit. - /// Tags: 6257=1, 6261=adjusted type, 6258=trigger, 6259=adjusted stop, 6262=adjusted limit. - SubmitAdjustableStop { - order_id: OrderId, - instrument: InstrumentId, - side: Side, - qty: u32, - stop_price: Price, - trigger_price: Price, - adjusted_order_type: AdjustedOrderType, - adjusted_stop_price: Price, - /// Only used when adjusted_order_type is StopLimit or TrailLimit. 0 = not set. - adjusted_stop_limit_price: Price, - /// Trailing amount for a Trail/TrailLimit conversion (tag 6260). When the - /// unit is amount it is a price offset (scaled); when percent it is the - /// percent value scaled (1.00% = PRICE_SCALE). 0 = not set. - adjusted_trailing_amount: Price, - /// Unit of `adjusted_trailing_amount` on the wire (tag 6269): 0 = amount, - /// 100 = percent. Other values are rejected by the gateway. - adjustable_trailing_unit: i32, - }, Cancel { order_id: OrderId, }, @@ -911,7 +908,6 @@ impl OrderRequest { | Self::SubmitMtlAuc { order_id, .. } | Self::SubmitWhatIf { order_id, .. } | Self::SubmitLimitFractional { order_id, .. } - | Self::SubmitAdjustableStop { order_id, .. } | Self::SubmitEx { order_id, .. } => *order_id, Self::SubmitBracket { parent_id, .. } => *parent_id, } @@ -960,7 +956,6 @@ impl OrderRequest { | Self::SubmitMtlAuc { instrument, .. } | Self::SubmitWhatIf { instrument, .. } | Self::SubmitLimitFractional { instrument, .. } - | Self::SubmitAdjustableStop { instrument, .. } | Self::SubmitEx { instrument, .. } | Self::SubmitBracket { instrument, .. } => Some(*instrument), } @@ -1017,19 +1012,19 @@ impl OrderRequest { Self::SubmitPegBench { price, pegged_change_amount, ref_change_amount, .. } => { s(price); s(pegged_change_amount); s(ref_change_amount); } - Self::SubmitAdjustableStop { - stop_price, trigger_price, adjusted_stop_price, adjusted_stop_limit_price, - adjusted_trailing_amount, adjustable_trailing_unit, .. - } => { - s(stop_price); s(trigger_price); s(adjusted_stop_price); s(adjusted_stop_limit_price); - // Snap the trailing amount only when it is an absolute price - // offset; a percent (unit 100) is not a price and must not snap. - if *adjustable_trailing_unit == 0 { s(adjusted_trailing_amount); } - } Self::SubmitEx { kind, .. } => match kind { OrderKind::Market | OrderKind::Moc | OrderKind::Mtl | OrderKind::MktPrt | OrderKind::SnapMkt | OrderKind::SnapMid | OrderKind::SnapPri => {} OrderKind::TrailPct { trail_stop_price, .. } => s(trail_stop_price), + OrderKind::AdjustableStop { + stop_price, trigger_price, adjusted_stop_price, adjusted_stop_limit_price, + adjusted_trailing_amount, adjustable_trailing_unit, .. + } => { + s(stop_price); s(trigger_price); s(adjusted_stop_price); s(adjusted_stop_limit_price); + // Snap the trailing amount only when it is an absolute price + // offset; a percent (unit 100) is not a price and must not snap. + if *adjustable_trailing_unit == 0 { s(adjusted_trailing_amount); } + } OrderKind::Limit { price } | OrderKind::Loc { price } => s(price), OrderKind::Stop { stop_price } | OrderKind::Mit { stop_price } diff --git a/tests/ib_paper_compat/common.rs b/tests/ib_paper_compat/common.rs index 0aee62ec..eadc2613 100644 --- a/tests/ib_paper_compat/common.rs +++ b/tests/ib_paper_compat/common.rs @@ -544,9 +544,8 @@ pub(super) fn run_submit_cancel_phase( OrderRequest::SubmitMtlAuc { order_id, .. } => *order_id, OrderRequest::SubmitWhatIf { order_id, .. } => *order_id, OrderRequest::SubmitLimitFractional { order_id, .. } => *order_id, - OrderRequest::SubmitAdjustableStop { order_id, .. } => *order_id, - OrderRequest::SubmitTrailingStopPctEx { order_id, .. } => *order_id, OrderRequest::SubmitEx { order_id, .. } => *order_id, + OrderRequest::SubmitTrailingStopPctEx { order_id, .. } => *order_id, OrderRequest::SubmitBracket { parent_id, .. } => *parent_id, OrderRequest::Cancel { order_id } => *order_id, OrderRequest::CancelAll { .. } => 0, diff --git a/tests/ib_paper_compat/orders.rs b/tests/ib_paper_compat/orders.rs index eba8d7c7..dbf8c89f 100644 --- a/tests/ib_paper_compat/orders.rs +++ b/tests/ib_paper_compat/orders.rs @@ -1338,7 +1338,14 @@ pub(super) fn phase_fractional_order(conns: Conns) -> Conns { pub(super) fn phase_adjustable_stop_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 75: Adjustable Stop Order (SPY)", - OrderRequest::SubmitAdjustableStop { order_id: oid, instrument: 0, side: Side::Sell, qty: 1, stop_price: 1_00_000_000, trigger_price: 500_00_000_000, adjusted_order_type: AdjustedOrderType::StopLimit, adjusted_stop_price: 1_50_000_000, adjusted_stop_limit_price: 1_00_000_000 }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Sell, qty: 1, + kind: ibx::types::OrderKind::AdjustableStop { + stop_price: 1_00_000_000, trigger_price: 500_00_000_000, + adjusted_order_type: AdjustedOrderType::StopLimit, + adjusted_stop_price: 1_50_000_000, adjusted_stop_limit_price: 1_00_000_000, + adjusted_trailing_amount: 0, adjustable_trailing_unit: 0, + }, + tif: b'0', attrs: Default::default() }, false) } From 2c6416fdcc9972702db4b7bcebef800cb57eca9c Mon Sep 17 00:00:00 2001 From: userFRM Date: Thu, 30 Jul 2026 17:22:12 +0200 Subject: [PATCH 2/4] orders: route adaptive, algo and what-if through the shared encoder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `build_order_request` computes the extended-attribute block partway down, and three order types returned before reaching it. Their request variants carried no attribute block at all, so `Order::attrs()` was never consulted on those paths and their encoders emitted none of it. Setting `outsideRth` on an adaptive, algo or what-if order was accepted by the API and silently ignored — the order went out regular-hours-only with nothing reporting the difference. The same early return bypassed the parent link and the OCA group, so an adaptive or algo order used as a bracket child was submitted unlinked and outside its group, which is what the attribute block exists to prevent. The tif was hard-coded to DAY on all three. Patching the missing tags into each dedicated encoder would leave the rest of the block — hidden, display size, min quantity, good-after, GTD expiry, discretionary amount, sweep-to-fill, all-or-none, trigger method, cash quantity, conditions — still dropped on those paths, and would put the emission in four places. These route instead, the way the adjustable stop does: `Adaptive`, `Algo` and `WhatIf` are now `OrderKind` variants carried by `SubmitEx`, and the three standalone encoder arms are gone. Their own tags keep their values and their position — after tag 204 and the attribute block, where the encoders this replaces put them: 18=e and the adaptive priority parameter, the algo strategy with 849 and its parameter pairs, and the what-if flag. A what-if is still tracked under its marker so the response is recognised as a preview. `OrderKind` is no longer `Copy`, because the algo parameters it now carries own their strings. `Context::submit_adaptive`, `submit_algo` and `submit_what_if` take a tif and an attribute block, as `submit_adjustable_stop` does. Closes #318. --- src/api/client/tests.rs | 6 +- src/client_core.rs | 75 +++++-- src/engine/context.rs | 38 ++-- src/engine/hot_loop/order_builder.rs | 299 +++++++++++++++------------ src/types.rs | 45 ++-- tests/ib_paper_compat/common.rs | 3 - tests/ib_paper_compat/orders.rs | 40 ++-- tests/scenarios.rs | 5 +- 8 files changed, 285 insertions(+), 226 deletions(-) diff --git a/src/api/client/tests.rs b/src/api/client/tests.rs index 48dda260..1ef36055 100644 --- a/src/api/client/tests.rs +++ b/src/api/client/tests.rs @@ -970,7 +970,8 @@ fn place_order_algo_vwap() { client.place_order(1, &spy(), &order).unwrap(); let cmd = rx.try_recv().unwrap(); - assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitAlgo { .. }))); + assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitEx { + kind: OrderKind::Algo { .. }, .. }))); } #[test] @@ -984,7 +985,8 @@ fn place_order_what_if() { client.place_order(1, &spy(), &order).unwrap(); let cmd = rx.try_recv().unwrap(); - assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitWhatIf { .. }))); + assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitEx { + kind: OrderKind::WhatIf { .. }, .. }))); } #[test] diff --git a/src/client_core.rs b/src/client_core.rs index cb8f24a4..227d0730 100644 --- a/src/client_core.rs +++ b/src/client_core.rs @@ -1414,6 +1414,20 @@ impl ClientCore { let qty = order.total_quantity as u32; let order_type = order.order_type.to_uppercase(); + // Every order type must carry extended attributes and a non-DAY tif + // when the caller sets them — dropping them silently produced + // unlinked, immediate-DAY bracket children (ibx#224). An empty tif + // is treated as DAY, matching the official API default. + let extended = order.has_extended_attrs() + || !matches!(order.tif.as_str(), "" | "DAY"); + + let ex = |kind: OrderKind| OrderRequest::SubmitEx { + order_id, instrument, side, qty, + kind, + tif: order.tif_byte(), + attrs: order.attrs(), + }; + // Adaptive orders (special-cased before generic algo) if order.algo_strategy.eq_ignore_ascii_case("Adaptive") { let price = (order.lmt_price * PRICE_SCALE_F) as i64; @@ -1426,26 +1440,20 @@ impl ClientCore { "Urgent" => AdaptivePriority::Urgent, _ => AdaptivePriority::Normal, }; - return Ok(ControlCommand::Order(OrderRequest::SubmitAdaptive { - order_id, instrument, side, qty, price, priority, - })); + return Ok(ControlCommand::Order(ex(OrderKind::Adaptive { price, priority }))); } // Algo orders if !order.algo_strategy.is_empty() { let algo = crate::api::client::parse_algo_params(&order.algo_strategy, &order.algo_params)?; let price = (order.lmt_price * PRICE_SCALE_F) as i64; - return Ok(ControlCommand::Order(OrderRequest::SubmitAlgo { - order_id, instrument, side, qty, price, algo, - })); + return Ok(ControlCommand::Order(ex(OrderKind::Algo { price, algo }))); } // What-if orders if order.what_if { let price = (order.lmt_price * PRICE_SCALE_F) as i64; - return Ok(ControlCommand::Order(OrderRequest::SubmitWhatIf { - order_id, instrument, side, qty, price, - })); + return Ok(ControlCommand::Order(ex(OrderKind::WhatIf { price }))); } // Adjustable stop: a base STP that converts to another order type when @@ -1486,19 +1494,6 @@ impl ClientCore { })); } - // Every order type must carry extended attributes and a non-DAY tif - // when the caller sets them — dropping them silently produced - // unlinked, immediate-DAY bracket children (ibx#224). An empty tif - // is treated as DAY, matching the official API default. - let extended = order.has_extended_attrs() - || !matches!(order.tif.as_str(), "" | "DAY"); - let ex = |kind: OrderKind| OrderRequest::SubmitEx { - order_id, instrument, side, qty, - kind, - tif: order.tif_byte(), - attrs: order.attrs(), - }; - let req = match order_type.as_str() { "MKT" => { if extended { ex(OrderKind::Market) } @@ -1950,4 +1945,40 @@ mod tests { core.subscribe_pnl_single(7, 1); assert_eq!(core.poll_pnl_single(&shared).len(), 1); } + /// ibx#318: adaptive, algo and what-if returned out of `build_order_request` + /// before the extended-attribute block was reached, so a caller could set + /// outside-RTH, a parent link, an OCA group or a non-DAY tif on any of them + /// and have it accepted and dropped. Asserted on the request the API layer + /// produces, which is the boundary where the drop happened. + #[test] + fn the_algo_order_types_carry_the_attributes_the_caller_set() { + let base = ApiOrder { + action: "BUY".into(), + total_quantity: 100.0, + order_type: "LMT".into(), + lmt_price: 150.0, + tif: "GTC".into(), + outside_rth: true, + parent_id: 42, + oca_group: "bracket_1".into(), + ..Default::default() + }; + let cases = [ + ("adaptive", ApiOrder { algo_strategy: "Adaptive".into(), ..base.clone() }), + ("algo", ApiOrder { algo_strategy: "Vwap".into(), ..base.clone() }), + ("what-if", ApiOrder { what_if: true, ..base.clone() }), + ]; + for (label, order) in cases { + let cmd = ClientCore::build_order_request(&order, 7, 0) + .unwrap_or_else(|e| panic!("{label}: {e}")); + let ControlCommand::Order(OrderRequest::SubmitEx { tif, attrs, .. }) = cmd else { + panic!("{label} must route through the shared extended submission"); + }; + assert!(attrs.outside_rth, "{label} dropped outside RTH"); + assert_eq!(attrs.parent_id, 42, "{label} dropped the parent link"); + assert_eq!(attrs.oca_group_str, "bracket_1", "{label} dropped the OCA group"); + assert_eq!(tif, b'1', "{label} was submitted DAY rather than GTC"); + } + } + } diff --git a/src/engine/context.rs b/src/engine/context.rs index 78be9669..fda42efe 100644 --- a/src/engine/context.rs +++ b/src/engine/context.rs @@ -562,16 +562,15 @@ impl Context { qty: u32, price: Price, priority: AdaptivePriority, + tif: u8, + attrs: OrderAttrs, ) -> OrderId { let id = self.next_order_id; self.next_order_id += 1; - self.pending_orders.push(OrderRequest::SubmitAdaptive { - order_id: id, - instrument, - side, - qty, - price, - priority, + self.pending_orders.push(OrderRequest::SubmitEx { + order_id: id, instrument, side, qty, + kind: OrderKind::Adaptive { price, priority }, + tif, attrs, }); id } @@ -714,11 +713,15 @@ impl Context { qty: u32, price: Price, algo: AlgoParams, + tif: u8, + attrs: OrderAttrs, ) -> OrderId { let id = self.next_order_id; self.next_order_id += 1; - self.pending_orders.push(OrderRequest::SubmitAlgo { - order_id: id, instrument, side, qty, price, algo, + self.pending_orders.push(OrderRequest::SubmitEx { + order_id: id, instrument, side, qty, + kind: OrderKind::Algo { price, algo }, + tif, attrs, }); id } @@ -794,11 +797,15 @@ impl Context { side: Side, qty: u32, price: Price, + tif: u8, + attrs: OrderAttrs, ) -> OrderId { let id = self.next_order_id; self.next_order_id += 1; - self.pending_orders.push(OrderRequest::SubmitWhatIf { - order_id: id, instrument, side, qty, price, + self.pending_orders.push(OrderRequest::SubmitEx { + order_id: id, instrument, side, qty, + kind: OrderKind::WhatIf { price }, + tif, attrs, }); id } @@ -1647,18 +1654,21 @@ mod tests { #[test] fn submit_what_if_drains_correctly() { let mut ctx = Context::new(); - let id = ctx.submit_what_if(0, Side::Buy, 100, 256_20 * (PRICE_SCALE / 100)); + let id = ctx.submit_what_if(0, Side::Buy, 100, 256_20 * (PRICE_SCALE / 100), + b'0', OrderAttrs::default()); let orders: Vec<_> = ctx.drain_pending_orders().collect(); assert_eq!(orders.len(), 1); match &orders[0] { - OrderRequest::SubmitWhatIf { order_id, instrument, side, qty, price } => { + OrderRequest::SubmitEx { + order_id, instrument, side, qty, kind: OrderKind::WhatIf { price }, .. + } => { assert_eq!(*order_id, id); assert_eq!(*instrument, 0); assert_eq!(*side, Side::Buy); assert_eq!(*qty, 100); assert_eq!(*price, 256_20 * (PRICE_SCALE / 100)); } - _ => panic!("expected SubmitWhatIf"), + _ => panic!("expected a what-if"), } } diff --git a/src/engine/hot_loop/order_builder.rs b/src/engine/hot_loop/order_builder.rs index a6b95f78..31da49b2 100644 --- a/src/engine/hot_loop/order_builder.rs +++ b/src/engine/hot_loop/order_builder.rs @@ -775,101 +775,6 @@ pub(crate) fn drain_and_send_orders( (204, "0"), ]) } - OrderRequest::SubmitAdaptive { order_id, instrument, side, qty, price, priority } => { - context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, price, b'2', b'0', 0, - )); - let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); - let clord_str = format!("{}.{}", order_id, ver); - let side_str = fix_side(side); - let qty_str = format_uint(qty as u64); - let price_str = format_price(price); - let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, destination) = context.market.order_routing(instrument); - let now = chrono_free_timestamp(); - let priority_str = priority.as_str(); - // Per ib-agent#136 capture: Adaptive needs 18=e (ExecInst = - // Adaptive algo wrapper). Without it, gateway rejects with - // "Invalid value in field # 18". - conn.send_fix(&[ - (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), - (fix::TAG_SENDING_TIME, &now), - (11, &clord_str), - (1, account_id), - (21, "2"), - (55, &symbol), - (54, side_str), - (38, &qty_str), - (40, "2"), // OrdType = Limit - (44, &price_str), - (18, "e"), // ExecInst = Adaptive algo - (59, "0"), // TIF = DAY - (60, &now), - (167, &sec_type_str), - (100, &destination), - (6210, &destination), - (15, "USD"), - (204, "0"), - (847, "Adaptive"), // AlgoStrategy - (5957, "1"), // AlgoParamCount - (5958, "adaptivePriority"), // AlgoParamTag - (5960, priority_str), // AlgoParamValue - ]) - } - OrderRequest::SubmitAlgo { order_id, instrument, side, qty, price, algo } => { - context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, price, b'2', b'0', 0, - )); - let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); - let clord_str = format!("{}.{}", order_id, ver); - let side_str = fix_side(side); - let qty_str = format_uint(qty as u64); - let price_str = format_price(price); - let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, destination) = context.market.order_routing(instrument); - let now = chrono_free_timestamp(); - let mut fields: Vec<(u32, &str)> = vec![ - (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), - (fix::TAG_SENDING_TIME, &now), - (11, &clord_str), - (1, account_id), - (21, "2"), - (55, &symbol), - (54, side_str), - (38, &qty_str), - (40, "2"), // OrdType = Limit - (44, &price_str), - (59, "0"), // TIF = DAY - (60, &now), - (167, &sec_type_str), - (100, &destination), - (6210, &destination), - (15, "USD"), - (204, "0"), - ]; - let (algo_name, param_strs) = build_algo_tags(&algo); - fields.push((847, algo_name)); - // Tag 849 (maxPctVol) for algos that use it - let pct_str = match &algo { - AlgoParams::Vwap { max_pct_vol, .. } - | AlgoParams::ArrivalPx { max_pct_vol, .. } - | AlgoParams::ClosePx { max_pct_vol, .. } => format!("{}", max_pct_vol), - _ => String::new(), - }; - if !pct_str.is_empty() { - fields.push((849, &pct_str)); - } - let count_str = (param_strs.len() / 2).to_string(); - fields.push((5957, &count_str)); - // Emit key/value pairs: 5958=key, 5960=value (repeated) - let mut i = 0; - while i < param_strs.len() { - fields.push((5958, ¶m_strs[i])); - fields.push((5960, ¶m_strs[i + 1])); - i += 2; - } - conn.send_fix(&fields) - } OrderRequest::SubmitPegBench { order_id, instrument, side, qty, price, ref_con_id, is_peg_decrease, pegged_change_amount, ref_change_amount } => { context.insert_order(crate::types::Order::new( @@ -973,40 +878,6 @@ pub(crate) fn drain_and_send_orders( (204, "0"), ]) } - OrderRequest::SubmitWhatIf { order_id, instrument, side, qty, price } => { - // What-if: insert with ORD_WHAT_IF marker so we can detect the response - context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, price, crate::types::ORD_WHAT_IF, b'0', 0, - )); - let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); - let clord_str = format!("{}.{}", order_id, ver); - let side_str = fix_side(side); - let qty_str = format_uint(qty as u64); - let price_str = format_price(price); - let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, destination) = context.market.order_routing(instrument); - let now = chrono_free_timestamp(); - conn.send_fix(&[ - (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), - (fix::TAG_SENDING_TIME, &now), - (11, &clord_str), - (1, account_id), - (21, "2"), - (55, &symbol), - (54, side_str), - (38, &qty_str), - (40, "2"), // OrdType = Limit - (44, &price_str), - (59, "0"), - (60, &now), - (167, &sec_type_str), - (100, &destination), - (6210, &destination), - (15, "USD"), - (204, "0"), - (6091, "1"), // What-If flag - ]) - } OrderRequest::SubmitLimitFractional { order_id, instrument, side, qty, price } => { context.insert_order(crate::types::Order::new( order_id, instrument, side, 0, price, b'2', b'0', 0, @@ -1575,6 +1446,10 @@ fn send_order_ex( K::PegMid { offset } => (crate::types::ORD_PEG_MID, 0, offset), K::Rel { offset } => (b'R', 0, offset), K::AdjustableStop { stop_price, .. } => (b'3', 0, stop_price), + K::Adaptive { price, .. } | K::Algo { price, .. } => (b'2', price, 0), + // Tracked under the what-if marker so the response is recognised as a + // preview; it never becomes a live order. + K::WhatIf { price } => (crate::types::ORD_WHAT_IF, price, 0), }; context.insert_order(crate::types::Order::new( order_id, instrument, side, qty, track_price, ord_type_byte, tif, track_stop, @@ -1714,6 +1589,24 @@ fn send_order_ex( fields.push((18, "R".to_string())); has_base_exec_inst = true; } + K::Adaptive { price, .. } => { + // Per ib-agent#136 capture: Adaptive needs 18=e (ExecInst = adaptive + // algo wrapper). Without it the gateway rejects with "Invalid value + // in field # 18". The strategy and its one parameter are appended + // after the attribute block, where the encoder this replaced put them. + fields.push((40, "2".to_string())); + fields.push((44, format_price(price).to_string())); + fields.push((18, "e".to_string())); + has_base_exec_inst = true; + } + K::Algo { price, .. } => { + fields.push((40, "2".to_string())); + fields.push((44, format_price(price).to_string())); + } + K::WhatIf { price } => { + fields.push((40, "2".to_string())); + fields.push((44, format_price(price).to_string())); + } } fields.push((59, tif_str.to_string())); @@ -1822,13 +1715,13 @@ fn send_order_ex( if let K::AdjustableStop { trigger_price, adjusted_order_type, adjusted_stop_price, adjusted_stop_limit_price, adjusted_trailing_amount, adjustable_trailing_unit, .. - } = kind { + } = &kind { fields.push((6257, "1".to_string())); // has adjustable params fields.push((6261, adjusted_order_type.fix_code().to_string())); - fields.push((6258, format_price(trigger_price).to_string())); - fields.push((6259, format_price(adjusted_stop_price).to_string())); - if adjusted_stop_limit_price > 0 { - fields.push((6262, format_price(adjusted_stop_limit_price).to_string())); + fields.push((6258, format_price(*trigger_price).to_string())); + fields.push((6259, format_price(*adjusted_stop_price).to_string())); + if *adjusted_stop_limit_price > 0 { + fields.push((6262, format_price(*adjusted_stop_limit_price).to_string())); } // Trailing amount + unit for a Trail/TrailLimit conversion // (ib-agent#167, ibx#225). @@ -1836,11 +1729,41 @@ fn send_order_ex( crate::types::AdjustedOrderType::Trail | crate::types::AdjustedOrderType::TrailLimit) { - fields.push((6260, format_price(adjusted_trailing_amount).to_string())); + fields.push((6260, format_price(*adjusted_trailing_amount).to_string())); fields.push((6269, adjustable_trailing_unit.to_string())); } } + // Strategy and preview tags last, in the position they held in the encoders + // this path replaced: after 204 and the attribute block (ibx#318). + match &kind { + K::Adaptive { priority, .. } => { + fields.push((847, "Adaptive".to_string())); + fields.push((5957, "1".to_string())); + fields.push((5958, "adaptivePriority".to_string())); + fields.push((5960, priority.as_str().to_string())); + } + K::Algo { algo, .. } => { + let (algo_name, param_strs) = build_algo_tags(algo); + fields.push((847, algo_name.to_string())); + // Tag 849 (maxPctVol) for the algos that use it. + if let AlgoParams::Vwap { max_pct_vol, .. } + | AlgoParams::ArrivalPx { max_pct_vol, .. } + | AlgoParams::ClosePx { max_pct_vol, .. } = algo + { + fields.push((849, format!("{}", max_pct_vol))); + } + fields.push((5957, (param_strs.len() / 2).to_string())); + // Key/value pairs: 5958=key, 5960=value, repeated. + for pair in param_strs.chunks_exact(2) { + fields.push((5958, pair[0].clone())); + fields.push((5960, pair[1].clone())); + } + } + K::WhatIf { .. } => fields.push((6091, "1".to_string())), + _ => {} + } + let refs: Vec<(u32, &str)> = fields.iter().map(|(t, s)| (*t, s.as_str())).collect(); conn.send_fix(&refs) } @@ -2033,6 +1956,112 @@ mod tests { assert!(shared.orders.drain_order_updates().is_empty()); } + /// ibx#318: adaptive, algo and what-if orders returned early into their own + /// encoders, which carried no attribute block at all — so outside-RTH, the + /// parent link and the OCA group were accepted by the API and silently + /// dropped, and the tif was hard-coded to DAY. Asserted on the bytes, + /// because the enum-level tests passed throughout. + #[test] + fn adaptive_wire_carries_the_attributes_and_keeps_its_algo_tags() { + let msg = send_kind_for_test( + crate::types::OrderKind::Adaptive { + price: 100 * crate::types::PRICE_SCALE, + priority: crate::types::AdaptivePriority::Urgent, + }, + b'1', + bracket_child_attrs(), + ); + let tag = |t: &str| msg.split('\u{1}').find_map(|f| f.strip_prefix(t).map(str::to_string)); + + assert_eq!(tag("6433=").as_deref(), Some("1"), "outside RTH missing: {}", msg); + assert_eq!(tag("6107=").as_deref(), Some("42.0"), "parent link missing: {}", msg); + assert_eq!(tag("583=").as_deref(), Some("bracket_1"), "OCA group missing: {}", msg); + assert_eq!(tag("59=").as_deref(), Some("1"), "tif must be GTC, not DAY: {}", msg); + + // And everything the standalone encoder emitted is unchanged. + assert_eq!(tag("40=").as_deref(), Some("2")); + assert_eq!(tag("18=").as_deref(), Some("e"), "adaptive wrapper missing: {}", msg); + assert_eq!(tag("847=").as_deref(), Some("Adaptive")); + assert_eq!(tag("5957=").as_deref(), Some("1")); + assert_eq!(tag("5958=").as_deref(), Some("adaptivePriority")); + assert_eq!(tag("5960=").as_deref(), Some("Urgent")); + assert!(msg.find("204=").unwrap() < msg.find("847=").unwrap(), + "the strategy tags keep their position after 204: {}", msg); + } + + #[test] + fn algo_wire_carries_the_attributes_and_keeps_its_algo_tags() { + let msg = send_kind_for_test( + crate::types::OrderKind::Algo { + price: 100 * crate::types::PRICE_SCALE, + algo: AlgoParams::Vwap { + max_pct_vol: 0.25, + no_take_liq: true, + allow_past_end_time: false, + start_time: String::new(), + end_time: String::new(), + }, + }, + b'1', + bracket_child_attrs(), + ); + let tag = |t: &str| msg.split('\u{1}').find_map(|f| f.strip_prefix(t).map(str::to_string)); + + assert_eq!(tag("6433=").as_deref(), Some("1"), "outside RTH missing: {}", msg); + assert_eq!(tag("6107=").as_deref(), Some("42.0"), "parent link missing: {}", msg); + assert_eq!(tag("583=").as_deref(), Some("bracket_1"), "OCA group missing: {}", msg); + assert_eq!(tag("59=").as_deref(), Some("1"), "tif must be GTC, not DAY: {}", msg); + + assert_eq!(tag("847=").as_deref(), Some("Vwap")); + assert_eq!(tag("849=").as_deref(), Some("0.25"), "maxPctVol missing: {}", msg); + assert_eq!(tag("5957=").as_deref(), Some("4"), "param count: {}", msg); + assert_eq!(tag("5958=").as_deref(), Some("noTakeLiq")); + assert_eq!(tag("5960=").as_deref(), Some("1")); + } + + #[test] + fn what_if_wire_carries_the_attributes_and_keeps_its_preview_flag() { + let msg = send_kind_for_test( + crate::types::OrderKind::WhatIf { price: 100 * crate::types::PRICE_SCALE }, + b'1', + bracket_child_attrs(), + ); + let tag = |t: &str| msg.split('\u{1}').find_map(|f| f.strip_prefix(t).map(str::to_string)); + + assert_eq!(tag("6433=").as_deref(), Some("1"), "outside RTH missing: {}", msg); + assert_eq!(tag("6107=").as_deref(), Some("42.0"), "parent link missing: {}", msg); + assert_eq!(tag("59=").as_deref(), Some("1"), "tif must be GTC, not DAY: {}", msg); + assert_eq!(tag("6091=").as_deref(), Some("1"), "what-if flag missing: {}", msg); + assert!(msg.find("204=").unwrap() < msg.find("6091=").unwrap(), + "the preview flag keeps its position after 204: {}", msg); + } + + fn bracket_child_attrs() -> crate::types::OrderAttrs { + crate::types::OrderAttrs { + parent_id: 42, + oca_group_str: "bracket_1".to_string(), + oca_type: 1, + outside_rth: true, + ..Default::default() + } + } + + /// Encode one kind and return the frame as text. + fn send_kind_for_test( + kind: crate::types::OrderKind, + tif: u8, + attrs: crate::types::OrderAttrs, + ) -> String { + use std::io::Read; + let (mut conn, mut peer) = crate::protocol::connection::Connection::for_test(); + let mut context = Context::new(); + send_order_ex(&mut conn, &mut context, "DU123456", 7, 0, Side::Buy, 1, kind, tif, &attrs) + .unwrap(); + let mut buf = [0u8; 4096]; + let n = peer.read(&mut buf).unwrap(); + String::from_utf8_lossy(&buf[..n]).to_string() + } + /// ibx#240: the tags a bracket child cannot ship without. Asserted on the /// bytes `send_order_ex` puts on the wire, not on the request enum — the /// enum-level tests passed throughout the period the child shipped naked. diff --git a/src/types.rs b/src/types.rs index 0eaf1b85..e5466866 100644 --- a/src/types.rs +++ b/src/types.rs @@ -479,7 +479,7 @@ pub enum AlgoParams { /// which pairs any of these with a TIF and an `OrderAttrs` block, so every /// order type can carry extended attributes without a per-type `*Ex` /// variant (ibx#224). -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub enum OrderKind { Market, Limit { price: Price }, @@ -526,6 +526,14 @@ pub enum OrderKind { /// 100 = percent. Other values are rejected by the gateway. adjustable_trailing_unit: i32, }, + /// Adaptive limit. Tags: 18=e (adaptive wrapper), 847=Adaptive, + /// 5957/5958/5960 = the single adaptivePriority algo parameter. + Adaptive { price: Price, priority: AdaptivePriority }, + /// Generic algo limit. Tags: 847=strategy, 5957 + 5958/5960 per parameter. + Algo { price: Price, algo: AlgoParams }, + /// Margin preview. Tag 6091=1; the order is tracked under `ORD_WHAT_IF` so + /// the response is recognised, and never becomes a live order. + WhatIf { price: Price }, } /// Order request sent via control channel, processed by engine. @@ -721,14 +729,6 @@ pub enum OrderRequest { price: Price, }, /// Adaptive algo limit order: LMT with IB Adaptive algorithm overlay. - SubmitAdaptive { - order_id: OrderId, - instrument: InstrumentId, - side: Side, - qty: u32, - price: Price, - priority: AdaptivePriority, - }, /// Market to Limit: fills at market, remainder converts to limit at fill price. OrdType K. SubmitMtl { order_id: OrderId, @@ -797,14 +797,6 @@ pub enum OrderRequest { offset: Price, // peg offset, 0 = no offset }, /// Algorithmic order: limit order with IB algo strategy overlay (VWAP, TWAP, etc.). - SubmitAlgo { - order_id: OrderId, - instrument: InstrumentId, - side: Side, - qty: u32, - price: Price, - algo: AlgoParams, - }, /// Pegged to Benchmark: pegs to a benchmark instrument's price. OrdType PB. /// Companion tags: 6941=refConId, 6938=isPegDecrease, 6939=pegChangeAmt, 6942=refChangeAmt. SubmitPegBench { @@ -835,13 +827,6 @@ pub enum OrderRequest { }, /// What-If order: sends a limit order with tag 6091=1 for margin/commission preview. /// The order is NOT placed — response comes back as 35=8 with margin fields. - SubmitWhatIf { - order_id: OrderId, - instrument: InstrumentId, - side: Side, - qty: u32, - price: Price, - }, /// Fractional shares limit order. Qty is fixed-point (QTY_SCALE = 10^4). /// E.g., 0.5 shares = 5000. Tag 38 sent as decimal string. SubmitLimitFractional { @@ -892,7 +877,6 @@ impl OrderRequest { | Self::SubmitLimitEx { order_id, .. } | Self::SubmitRel { order_id, .. } | Self::SubmitLimitOpg { order_id, .. } - | Self::SubmitAdaptive { order_id, .. } | Self::SubmitMtl { order_id, .. } | Self::SubmitMktPrt { order_id, .. } | Self::SubmitStpPrt { order_id, .. } @@ -902,11 +886,9 @@ impl OrderRequest { | Self::SubmitSnapPri { order_id, .. } | Self::SubmitPegMkt { order_id, .. } | Self::SubmitPegMid { order_id, .. } - | Self::SubmitAlgo { order_id, .. } | Self::SubmitPegBench { order_id, .. } | Self::SubmitLimitAuc { order_id, .. } | Self::SubmitMtlAuc { order_id, .. } - | Self::SubmitWhatIf { order_id, .. } | Self::SubmitLimitFractional { order_id, .. } | Self::SubmitEx { order_id, .. } => *order_id, Self::SubmitBracket { parent_id, .. } => *parent_id, @@ -940,7 +922,6 @@ impl OrderRequest { | Self::SubmitLimitEx { instrument, .. } | Self::SubmitRel { instrument, .. } | Self::SubmitLimitOpg { instrument, .. } - | Self::SubmitAdaptive { instrument, .. } | Self::SubmitMtl { instrument, .. } | Self::SubmitMktPrt { instrument, .. } | Self::SubmitStpPrt { instrument, .. } @@ -950,11 +931,9 @@ impl OrderRequest { | Self::SubmitSnapPri { instrument, .. } | Self::SubmitPegMkt { instrument, .. } | Self::SubmitPegMid { instrument, .. } - | Self::SubmitAlgo { instrument, .. } | Self::SubmitPegBench { instrument, .. } | Self::SubmitLimitAuc { instrument, .. } | Self::SubmitMtlAuc { instrument, .. } - | Self::SubmitWhatIf { instrument, .. } | Self::SubmitLimitFractional { instrument, .. } | Self::SubmitEx { instrument, .. } | Self::SubmitBracket { instrument, .. } => Some(*instrument), @@ -987,9 +966,6 @@ impl OrderRequest { | Self::SubmitLimitOpg { price, .. } | Self::SubmitLimitAuc { price, .. } | Self::SubmitLimitFractional { price, .. } - | Self::SubmitAdaptive { price, .. } - | Self::SubmitAlgo { price, .. } - | Self::SubmitWhatIf { price, .. } | Self::SubmitLoc { price, .. } => s(price), Self::SubmitStop { stop_price, .. } | Self::SubmitStopGtc { stop_price, .. } @@ -1016,6 +992,9 @@ impl OrderRequest { OrderKind::Market | OrderKind::Moc | OrderKind::Mtl | OrderKind::MktPrt | OrderKind::SnapMkt | OrderKind::SnapMid | OrderKind::SnapPri => {} OrderKind::TrailPct { trail_stop_price, .. } => s(trail_stop_price), + OrderKind::Adaptive { price, .. } + | OrderKind::Algo { price, .. } + | OrderKind::WhatIf { price } => s(price), OrderKind::AdjustableStop { stop_price, trigger_price, adjusted_stop_price, adjusted_stop_limit_price, adjusted_trailing_amount, adjustable_trailing_unit, .. diff --git a/tests/ib_paper_compat/common.rs b/tests/ib_paper_compat/common.rs index eadc2613..8c1ad2ab 100644 --- a/tests/ib_paper_compat/common.rs +++ b/tests/ib_paper_compat/common.rs @@ -528,7 +528,6 @@ pub(super) fn run_submit_cancel_phase( OrderRequest::SubmitLimitEx { order_id, .. } => *order_id, OrderRequest::SubmitRel { order_id, .. } => *order_id, OrderRequest::SubmitLimitOpg { order_id, .. } => *order_id, - OrderRequest::SubmitAdaptive { order_id, .. } => *order_id, OrderRequest::SubmitMtl { order_id, .. } => *order_id, OrderRequest::SubmitMktPrt { order_id, .. } => *order_id, OrderRequest::SubmitStpPrt { order_id, .. } => *order_id, @@ -538,11 +537,9 @@ pub(super) fn run_submit_cancel_phase( OrderRequest::SubmitSnapPri { order_id, .. } => *order_id, OrderRequest::SubmitPegMkt { order_id, .. } => *order_id, OrderRequest::SubmitPegMid { order_id, .. } => *order_id, - OrderRequest::SubmitAlgo { order_id, .. } => *order_id, OrderRequest::SubmitPegBench { order_id, .. } => *order_id, OrderRequest::SubmitLimitAuc { order_id, .. } => *order_id, OrderRequest::SubmitMtlAuc { order_id, .. } => *order_id, - OrderRequest::SubmitWhatIf { order_id, .. } => *order_id, OrderRequest::SubmitLimitFractional { order_id, .. } => *order_id, OrderRequest::SubmitEx { order_id, .. } => *order_id, OrderRequest::SubmitTrailingStopPctEx { order_id, .. } => *order_id, diff --git a/tests/ib_paper_compat/orders.rs b/tests/ib_paper_compat/orders.rs index dbf8c89f..e991b6d3 100644 --- a/tests/ib_paper_compat/orders.rs +++ b/tests/ib_paper_compat/orders.rs @@ -729,7 +729,9 @@ pub(super) fn phase_bracket_order(conns: Conns) -> Conns { pub(super) fn phase_adaptive_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 30: Adaptive Algo Limit Order (SPY)", - OrderRequest::SubmitAdaptive { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, price: 1_00_000_000, priority: AdaptivePriority::Normal }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, + kind: OrderKind::Adaptive { price: 1_00_000_000, priority: AdaptivePriority::Normal }, + tif: b'0', attrs: OrderAttrs::default() }, false) } @@ -1030,8 +1032,9 @@ pub(super) fn phase_multi_condition_order(conns: Conns) -> Conns { pub(super) fn phase_vwap_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 62: VWAP Algo Order (SPY)", - OrderRequest::SubmitAlgo { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, price: 1_00_000_000, - algo: AlgoParams::Vwap { max_pct_vol: 0.1, no_take_liq: false, allow_past_end_time: true, start_time: "20260311-13:30:00".into(), end_time: "20260311-20:00:00".into() } }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, + kind: OrderKind::Algo { price: 1_00_000_000, algo: AlgoParams::Vwap { max_pct_vol: 0.1, no_take_liq: false, allow_past_end_time: true, start_time: "20260311-13:30:00".into(), end_time: "20260311-20:00:00".into() } }, + tif: b'0', attrs: OrderAttrs::default() }, false) } @@ -1040,8 +1043,9 @@ pub(super) fn phase_vwap_order(conns: Conns) -> Conns { pub(super) fn phase_twap_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 63: TWAP Algo Order (SPY)", - OrderRequest::SubmitAlgo { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, price: 1_00_000_000, - algo: AlgoParams::Twap { allow_past_end_time: true, start_time: "20260311-13:30:00".into(), end_time: "20260311-20:00:00".into() } }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, + kind: OrderKind::Algo { price: 1_00_000_000, algo: AlgoParams::Twap { allow_past_end_time: true, start_time: "20260311-13:30:00".into(), end_time: "20260311-20:00:00".into() } }, + tif: b'0', attrs: OrderAttrs::default() }, false) } @@ -1050,8 +1054,9 @@ pub(super) fn phase_twap_order(conns: Conns) -> Conns { pub(super) fn phase_arrival_px_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 64: Arrival Price Algo Order (SPY)", - OrderRequest::SubmitAlgo { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, price: 1_00_000_000, - algo: AlgoParams::ArrivalPx { max_pct_vol: 0.1, risk_aversion: RiskAversion::Neutral, allow_past_end_time: true, force_completion: false, start_time: "20260311-13:30:00".into(), end_time: "20260311-20:00:00".into() } }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, + kind: OrderKind::Algo { price: 1_00_000_000, algo: AlgoParams::ArrivalPx { max_pct_vol: 0.1, risk_aversion: RiskAversion::Neutral, allow_past_end_time: true, force_completion: false, start_time: "20260311-13:30:00".into(), end_time: "20260311-20:00:00".into() } }, + tif: b'0', attrs: OrderAttrs::default() }, false) } @@ -1060,8 +1065,9 @@ pub(super) fn phase_arrival_px_order(conns: Conns) -> Conns { pub(super) fn phase_close_px_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 65: Close Price Algo Order (SPY)", - OrderRequest::SubmitAlgo { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, price: 1_00_000_000, - algo: AlgoParams::ClosePx { max_pct_vol: 0.1, risk_aversion: RiskAversion::Neutral, force_completion: false, start_time: "20260311-13:30:00".into() } }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, + kind: OrderKind::Algo { price: 1_00_000_000, algo: AlgoParams::ClosePx { max_pct_vol: 0.1, risk_aversion: RiskAversion::Neutral, force_completion: false, start_time: "20260311-13:30:00".into() } }, + tif: b'0', attrs: OrderAttrs::default() }, false) } @@ -1070,8 +1076,9 @@ pub(super) fn phase_close_px_order(conns: Conns) -> Conns { pub(super) fn phase_dark_ice_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 66: Dark Ice Algo Order (SPY)", - OrderRequest::SubmitAlgo { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, price: 1_00_000_000, - algo: AlgoParams::DarkIce { allow_past_end_time: true, display_size: 1, start_time: "20260311-13:30:00".into(), end_time: "20260311-20:00:00".into() } }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, + kind: OrderKind::Algo { price: 1_00_000_000, algo: AlgoParams::DarkIce { allow_past_end_time: true, display_size: 1, start_time: "20260311-13:30:00".into(), end_time: "20260311-20:00:00".into() } }, + tif: b'0', attrs: OrderAttrs::default() }, false) } @@ -1080,8 +1087,9 @@ pub(super) fn phase_dark_ice_order(conns: Conns) -> Conns { pub(super) fn phase_pct_vol_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 67: % of Volume Algo Order (SPY)", - OrderRequest::SubmitAlgo { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, price: 1_00_000_000, - algo: AlgoParams::PctVol { pct_vol: 0.1, no_take_liq: false, start_time: "20260311-13:30:00".into(), end_time: "20260311-20:00:00".into() } }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, + kind: OrderKind::Algo { price: 1_00_000_000, algo: AlgoParams::PctVol { pct_vol: 0.1, no_take_liq: false, start_time: "20260311-13:30:00".into(), end_time: "20260311-20:00:00".into() } }, + tif: b'0', attrs: OrderAttrs::default() }, false) } @@ -1137,8 +1145,10 @@ pub(super) fn phase_what_if_order(conns: Conns) -> Conns { hot_loop.context_mut().set_symbol(inst_id, "SPY".to_string()); let order_id = next_order_id(); - control_tx.send(ControlCommand::Order(OrderRequest::SubmitWhatIf { - order_id, instrument: inst_id, side: Side::Buy, qty: 100, price: 1_00_000_000, + control_tx.send(ControlCommand::Order(OrderRequest::SubmitEx { + order_id, instrument: inst_id, side: Side::Buy, qty: 100, + kind: OrderKind::WhatIf { price: 1_00_000_000 }, + tif: b'0', attrs: OrderAttrs::default(), })).unwrap(); control_tx.send(ControlCommand::Subscribe { con_id: 756733, symbol: "SPY".into(), exchange: String::new(), sec_type: String::new(), last_trade_date: String::new(), strike: 0.0, right: String::new(), multiplier: String::new(), mode_9887: 0, reply_tx: None }).unwrap(); let join = run_hot_loop(hot_loop); diff --git a/tests/scenarios.rs b/tests/scenarios.rs index 102d8e52..1206174f 100644 --- a/tests/scenarios.rs +++ b/tests/scenarios.rs @@ -199,10 +199,11 @@ fn order_lifecycle_what_if_preview() { }; client.place_order(90, &spy(), &order).unwrap(); - // Verify SubmitWhatIf was sent + // Verify the what-if submission was sent let mut found_what_if = false; while let Ok(cmd) = rx.try_recv() { - if matches!(cmd, ControlCommand::Order(OrderRequest::SubmitWhatIf { .. })) { + if matches!(cmd, ControlCommand::Order(OrderRequest::SubmitEx { + kind: OrderKind::WhatIf { .. }, .. })) { found_what_if = true; } } From 6861e438b9992475ea94424ebf96cb4c1ab03b31 Mon Sep 17 00:00:00 2001 From: userFRM Date: Thu, 30 Jul 2026 18:02:00 +0200 Subject: [PATCH 3/4] orders: encode every order type through one path `build_order_request` chose per order type between a request that carries the extended attributes and a plain one that does not, and the engine had a separate encoder for each. Twenty-one order types were encoded twice, in two places, from two field lists. That duplication is where the attribute bugs come from. An order type whose own encoder drifts from the shared one ships without something the caller set: bracket children submitted unlinked and DAY (#224), then the adjustable stop the same way (#240), then adaptive, algo and what-if (#318). Each was the same defect found again in a different arm. Every type now routes through the shared encoder. The plain-versus-extended choice is gone, so there is no arm for a type to drift into. A test encodes each type both ways and compares the frames, which is what makes that safe to do and what stops the two paths separating again while both exist. It found six that did not agree, all the same shape: an optional tag the per-type encoder appends after 204, emitted in among the order-type tags by the shared one. The mid-price cap on 44, the pegged offset on 211 with the two mid-offset tags beside it, and the initial trailing trigger on 6117 now sit where the per-type encoders put them, which is where the captures show them. The values and the conditions are unchanged; only the position moves, and only on the shared path. The remaining per-type request variants are now unreachable from the API surface and are removed separately. No wire change for an order with no extended attributes: the two encodings are byte-identical, tag for tag and value for value, for all twenty-one types. --- src/api/client/tests.rs | 104 +++++++++--------- src/client_core.rs | 90 +++++----------- src/engine/hot_loop/order_builder.rs | 153 ++++++++++++++++++++++----- 3 files changed, 207 insertions(+), 140 deletions(-) diff --git a/src/api/client/tests.rs b/src/api/client/tests.rs index 1ef36055..d1085d88 100644 --- a/src/api/client/tests.rs +++ b/src/api/client/tests.rs @@ -293,8 +293,8 @@ fn place_order_market() { let cmd = rx.try_recv().unwrap(); match cmd { - ControlCommand::Order(OrderRequest::SubmitMarket { qty, .. }) => assert_eq!(qty, 100), - _ => panic!("expected SubmitMarket, got {:?}", cmd), + ControlCommand::Order(OrderRequest::SubmitEx { qty, kind: OrderKind::Market, .. }) => assert_eq!(qty, 100), + _ => panic!("expected a Market order, got {:?}", cmd), } } @@ -310,11 +310,11 @@ fn place_order_limit() { let cmd = rx.try_recv().unwrap(); match cmd { - ControlCommand::Order(OrderRequest::SubmitLimit { qty, price, .. }) => { + ControlCommand::Order(OrderRequest::SubmitEx { qty, kind: OrderKind::Limit { price, .. }, .. }) => { assert_eq!(qty, 50); assert_eq!(price, (150.25 * PRICE_SCALE_F) as i64); } - _ => panic!("expected SubmitLimit, got {:?}", cmd), + _ => panic!("expected a Limit order, got {:?}", cmd), } } @@ -332,11 +332,11 @@ fn place_order_trailing_stop_carries_initial_trigger() { }; client.place_order(1, &spy(), &order).unwrap(); match rx.try_recv().unwrap() { - ControlCommand::Order(OrderRequest::SubmitTrailingStop { trail_amt, trail_stop_price, .. }) => { + ControlCommand::Order(OrderRequest::SubmitEx { kind: OrderKind::TrailingStop { trail_amt, trail_stop_price, .. }, .. }) => { assert_eq!(trail_amt, (0.50 * PRICE_SCALE_F) as i64); assert_eq!(trail_stop_price, (10.00 * PRICE_SCALE_F) as i64); } - cmd => panic!("expected SubmitTrailingStop, got {:?}", cmd), + cmd => panic!("expected a TrailingStop order, got {:?}", cmd), } } @@ -351,10 +351,10 @@ fn place_order_trailing_stop_without_trigger_is_unset() { }; client.place_order(1, &spy(), &order).unwrap(); match rx.try_recv().unwrap() { - ControlCommand::Order(OrderRequest::SubmitTrailingStop { trail_stop_price, .. }) => { + ControlCommand::Order(OrderRequest::SubmitEx { kind: OrderKind::TrailingStop { trail_stop_price, .. }, .. }) => { assert_eq!(trail_stop_price, 0); } - cmd => panic!("expected SubmitTrailingStop, got {:?}", cmd), + cmd => panic!("expected a TrailingStop order, got {:?}", cmd), } } @@ -451,7 +451,7 @@ fn place_order_adjustable_trail_percent_unit_passes_through() { } #[test] -fn place_order_limit_gtc_uses_limit_ex() { +fn place_order_limit_gtc_carries_the_tif() { let (client, rx, shared) = test_client(); shared.market.set_instrument_count(1); let order = Order { @@ -462,15 +462,15 @@ fn place_order_limit_gtc_uses_limit_ex() { let cmd = rx.try_recv().unwrap(); match cmd { - ControlCommand::Order(OrderRequest::SubmitLimitEx { tif, .. }) => { + ControlCommand::Order(OrderRequest::SubmitEx { tif, kind: OrderKind::Limit { .. }, .. }) => { assert_eq!(tif, b'1'); // GTC } - _ => panic!("expected SubmitLimitEx, got {:?}", cmd), + _ => panic!("expected a limit order, got {:?}", cmd), } } #[test] -fn place_order_limit_hidden_uses_limit_ex() { +fn place_order_limit_hidden_carries_the_attribute() { let (client, rx, shared) = test_client(); shared.market.set_instrument_count(1); let order = Order { @@ -481,10 +481,10 @@ fn place_order_limit_hidden_uses_limit_ex() { let cmd = rx.try_recv().unwrap(); match cmd { - ControlCommand::Order(OrderRequest::SubmitLimitEx { attrs, .. }) => { + ControlCommand::Order(OrderRequest::SubmitEx { attrs, kind: OrderKind::Limit { .. }, .. }) => { assert!(attrs.hidden); } - _ => panic!("expected SubmitLimitEx, got {:?}", cmd), + _ => panic!("expected a limit order, got {:?}", cmd), } } @@ -510,7 +510,7 @@ fn place_order_stop_with_parent_and_gtc_uses_submit_ex() { assert_eq!(attrs.parent_id, 42); assert_eq!(attrs.oca_group, 77); } - _ => panic!("expected SubmitEx, got {:?}", cmd), + _ => panic!("expected a Ex order, got {:?}", cmd), } } @@ -531,7 +531,7 @@ fn place_order_market_outside_rth_uses_submit_ex() { assert_eq!(tif, b'0'); // DAY assert!(attrs.outside_rth); } - _ => panic!("expected SubmitEx, got {:?}", cmd), + _ => panic!("expected a Ex order, got {:?}", cmd), } } @@ -555,13 +555,13 @@ fn place_order_trailing_amount_with_oca_uses_submit_ex() { assert_eq!(attrs.oca_group_str, "exit_9"); assert_eq!(attrs.oca_type, 2); // ibx#215 } - _ => panic!("expected SubmitEx, got {:?}", cmd), + _ => panic!("expected a Ex order, got {:?}", cmd), } } #[test] -fn place_order_empty_tif_stays_plain() { - // tif "" is DAY (the official API default) — no extended routing. +fn place_order_empty_tif_is_day() { + // An empty tif is DAY, matching the official API default. let (client, rx, shared) = test_client(); shared.market.set_instrument_count(1); let order = Order { @@ -569,8 +569,12 @@ fn place_order_empty_tif_stays_plain() { aux_price: 240.0, ..Default::default() }; client.place_order(1, &spy(), &order).unwrap(); - assert!(matches!(rx.try_recv().unwrap(), - ControlCommand::Order(OrderRequest::SubmitStop { .. }))); + match rx.try_recv().unwrap() { + ControlCommand::Order(OrderRequest::SubmitEx { tif, kind: OrderKind::Stop { .. }, .. }) => { + assert_eq!(tif, b'0', "an empty tif is DAY"); + } + other => panic!("expected a stop order, got {other:?}"), + } } // ── ibx#226: transmit=false must be rejected, not silently ignored ── @@ -638,11 +642,11 @@ fn place_order_stop() { let cmd = rx.try_recv().unwrap(); match cmd { - ControlCommand::Order(OrderRequest::SubmitStop { side, stop_price, .. }) => { + ControlCommand::Order(OrderRequest::SubmitEx { side, kind: OrderKind::Stop { stop_price, .. }, .. }) => { assert!(matches!(side, Side::Sell)); assert_eq!(stop_price, (145.0 * PRICE_SCALE_F) as i64); } - _ => panic!("expected SubmitStop, got {:?}", cmd), + _ => panic!("expected a Stop order, got {:?}", cmd), } } @@ -658,11 +662,11 @@ fn place_order_stop_limit() { let cmd = rx.try_recv().unwrap(); match cmd { - ControlCommand::Order(OrderRequest::SubmitStopLimit { price, stop_price, .. }) => { + ControlCommand::Order(OrderRequest::SubmitEx { kind: OrderKind::StopLimit { price, stop_price, .. }, .. }) => { assert_eq!(price, (144.0 * PRICE_SCALE_F) as i64); assert_eq!(stop_price, (145.0 * PRICE_SCALE_F) as i64); } - _ => panic!("expected SubmitStopLimit, got {:?}", cmd), + _ => panic!("expected a StopLimit order, got {:?}", cmd), } } @@ -678,10 +682,10 @@ fn place_order_trailing_stop_amount() { let cmd = rx.try_recv().unwrap(); match cmd { - ControlCommand::Order(OrderRequest::SubmitTrailingStop { trail_amt, .. }) => { + ControlCommand::Order(OrderRequest::SubmitEx { kind: OrderKind::TrailingStop { trail_amt, .. }, .. }) => { assert_eq!(trail_amt, (2.0 * PRICE_SCALE_F) as i64); } - _ => panic!("expected SubmitTrailingStop, got {:?}", cmd), + _ => panic!("expected a TrailingStop order, got {:?}", cmd), } } @@ -697,10 +701,10 @@ fn place_order_trailing_stop_percent() { let cmd = rx.try_recv().unwrap(); match cmd { - ControlCommand::Order(OrderRequest::SubmitTrailingStopPct { trail_pct, .. }) => { + ControlCommand::Order(OrderRequest::SubmitEx { kind: OrderKind::TrailPct { trail_pct, .. }, .. }) => { assert_eq!(trail_pct, 500); // 5.0 * 100 } - _ => panic!("expected SubmitTrailingStopPct, got {:?}", cmd), + _ => panic!("expected a TrailingStopPct order, got {:?}", cmd), } } @@ -715,7 +719,7 @@ fn place_order_trailing_stop_limit() { client.place_order(1, &spy(), &order).unwrap(); let cmd = rx.try_recv().unwrap(); - assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitTrailingStopLimit { .. }))); + assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitEx { kind: OrderKind::TrailingStopLimit { .. }, .. }))); } #[test] @@ -728,7 +732,7 @@ fn place_order_moc() { client.place_order(1, &spy(), &order).unwrap(); let cmd = rx.try_recv().unwrap(); - assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitMoc { .. }))); + assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitEx { kind: OrderKind::Moc, .. }))); } #[test] @@ -742,7 +746,7 @@ fn place_order_loc() { client.place_order(1, &spy(), &order).unwrap(); let cmd = rx.try_recv().unwrap(); - assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitLoc { .. }))); + assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitEx { kind: OrderKind::Loc { .. }, .. }))); } #[test] @@ -756,7 +760,7 @@ fn place_order_mit() { client.place_order(1, &spy(), &order).unwrap(); let cmd = rx.try_recv().unwrap(); - assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitMit { .. }))); + assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitEx { kind: OrderKind::Mit { .. }, .. }))); } #[test] @@ -770,7 +774,7 @@ fn place_order_lit() { client.place_order(1, &spy(), &order).unwrap(); let cmd = rx.try_recv().unwrap(); - assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitLit { .. }))); + assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitEx { kind: OrderKind::Lit { .. }, .. }))); } #[test] @@ -783,7 +787,7 @@ fn place_order_mtl() { client.place_order(1, &spy(), &order).unwrap(); let cmd = rx.try_recv().unwrap(); - assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitMtl { .. }))); + assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitEx { kind: OrderKind::Mtl, .. }))); } #[test] @@ -796,7 +800,7 @@ fn place_order_mkt_prt() { client.place_order(1, &spy(), &order).unwrap(); let cmd = rx.try_recv().unwrap(); - assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitMktPrt { .. }))); + assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitEx { kind: OrderKind::MktPrt, .. }))); } #[test] @@ -810,7 +814,7 @@ fn place_order_stp_prt() { client.place_order(1, &spy(), &order).unwrap(); let cmd = rx.try_recv().unwrap(); - assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitStpPrt { .. }))); + assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitEx { kind: OrderKind::StpPrt { .. }, .. }))); } #[test] @@ -824,7 +828,7 @@ fn place_order_rel() { client.place_order(1, &spy(), &order).unwrap(); let cmd = rx.try_recv().unwrap(); - assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitRel { .. }))); + assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitEx { kind: OrderKind::Rel { .. }, .. }))); } #[test] @@ -838,7 +842,7 @@ fn place_order_peg_mkt() { client.place_order(1, &spy(), &order).unwrap(); let cmd = rx.try_recv().unwrap(); - assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitPegMkt { .. }))); + assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitEx { kind: OrderKind::PegMkt { .. }, .. }))); } #[test] @@ -852,7 +856,7 @@ fn place_order_peg_mid() { client.place_order(1, &spy(), &order).unwrap(); let cmd = rx.try_recv().unwrap(); - assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitPegMid { .. }))); + assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitEx { kind: OrderKind::PegMid { .. }, .. }))); } #[test] @@ -866,7 +870,7 @@ fn place_order_midprice() { client.place_order(1, &spy(), &order).unwrap(); let cmd = rx.try_recv().unwrap(); - assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitMidPrice { .. }))); + assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitEx { kind: OrderKind::MidPrice { .. }, .. }))); } #[test] @@ -879,7 +883,7 @@ fn place_order_snap_mkt() { client.place_order(1, &spy(), &order).unwrap(); let cmd = rx.try_recv().unwrap(); - assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitSnapMkt { .. }))); + assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitEx { kind: OrderKind::SnapMkt, .. }))); } #[test] @@ -892,7 +896,7 @@ fn place_order_snap_mid() { client.place_order(1, &spy(), &order).unwrap(); let cmd = rx.try_recv().unwrap(); - assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitSnapMid { .. }))); + assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitEx { kind: OrderKind::SnapMid, .. }))); } #[test] @@ -905,7 +909,7 @@ fn place_order_snap_pri() { client.place_order(1, &spy(), &order).unwrap(); let cmd = rx.try_recv().unwrap(); - assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitSnapPri { .. }))); + assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitEx { kind: OrderKind::SnapPri, .. }))); } #[test] @@ -918,7 +922,7 @@ fn place_order_box_top() { client.place_order(1, &spy(), &order).unwrap(); let cmd = rx.try_recv().unwrap(); - assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitMtl { .. }))); + assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitEx { kind: OrderKind::Mtl, .. }))); } #[test] @@ -932,7 +936,7 @@ fn place_order_sell_side() { let cmd = rx.try_recv().unwrap(); match cmd { - ControlCommand::Order(OrderRequest::SubmitMarket { side, .. }) => { + ControlCommand::Order(OrderRequest::SubmitEx { side, kind: OrderKind::Market, .. }) => { assert!(matches!(side, Side::Sell)); } _ => panic!("expected SubmitMarket"), @@ -950,7 +954,7 @@ fn place_order_short_sell_side() { let cmd = rx.try_recv().unwrap(); match cmd { - ControlCommand::Order(OrderRequest::SubmitMarket { side, .. }) => { + ControlCommand::Order(OrderRequest::SubmitEx { side, kind: OrderKind::Market, .. }) => { assert!(matches!(side, Side::ShortSell)); } _ => panic!("expected SubmitMarket"), @@ -1052,7 +1056,7 @@ fn place_order_auto_assigns_id_when_zero() { let cmd = rx.try_recv().unwrap(); match cmd { - ControlCommand::Order(OrderRequest::SubmitMarket { order_id, .. }) => { + ControlCommand::Order(OrderRequest::SubmitEx { order_id, kind: OrderKind::Market, .. }) => { assert!(order_id > 0); } _ => panic!("expected SubmitMarket"), @@ -1121,7 +1125,7 @@ fn stp_order_with_valid_aux_price_succeeds() { }; client.place_order(1, &spy(), &order).unwrap(); let cmd = rx.try_recv().unwrap(); - assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitStop { .. }))); + assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitEx { kind: OrderKind::Stop { .. }, .. }))); } #[test] @@ -1160,7 +1164,7 @@ fn trail_order_with_trailing_percent_succeeds() { }; client.place_order(1, &spy(), &order).unwrap(); let cmd = rx.try_recv().unwrap(); - assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitTrailingStopPct { .. }))); + assert!(matches!(cmd, ControlCommand::Order(OrderRequest::SubmitEx { kind: OrderKind::TrailPct { .. }, .. }))); } #[test] diff --git a/src/client_core.rs b/src/client_core.rs index 227d0730..2f2c7087 100644 --- a/src/client_core.rs +++ b/src/client_core.rs @@ -1414,12 +1414,12 @@ impl ClientCore { let qty = order.total_quantity as u32; let order_type = order.order_type.to_uppercase(); - // Every order type must carry extended attributes and a non-DAY tif - // when the caller sets them — dropping them silently produced - // unlinked, immediate-DAY bracket children (ibx#224). An empty tif - // is treated as DAY, matching the official API default. - let extended = order.has_extended_attrs() - || !matches!(order.tif.as_str(), "" | "DAY"); + // Every order type carries its extended attributes and its time-in-force + // through one encoder. Choosing per type between an attribute-carrying + // request and a plain one is how an order type ends up shipping without + // something the caller set — unlinked, immediate-DAY bracket children + // (ibx#224), then the same defect again for the adjustable stop (#240) + // and for adaptive, algo and what-if (#318). let ex = |kind: OrderKind| OrderRequest::SubmitEx { order_id, instrument, side, qty, @@ -1496,51 +1496,30 @@ impl ClientCore { let req = match order_type.as_str() { "MKT" => { - if extended { ex(OrderKind::Market) } - else { OrderRequest::SubmitMarket { order_id, instrument, side, qty } } + ex(OrderKind::Market) } "LMT" => { let price = (order.lmt_price * PRICE_SCALE_F) as i64; - if extended { - OrderRequest::SubmitLimitEx { - order_id, instrument, side, qty, price, - tif: order.tif_byte(), - attrs: order.attrs(), - } - } else { - OrderRequest::SubmitLimit { order_id, instrument, side, qty, price } - } + ex(OrderKind::Limit { price }) } "STP" => { let stop = (order.aux_price * PRICE_SCALE_F) as i64; - if extended { ex(OrderKind::Stop { stop_price: stop }) } - else { OrderRequest::SubmitStop { order_id, instrument, side, qty, stop_price: stop } } + ex(OrderKind::Stop { stop_price: stop }) } "STP LMT" => { let price = (order.lmt_price * PRICE_SCALE_F) as i64; let stop = (order.aux_price * PRICE_SCALE_F) as i64; - if extended { ex(OrderKind::StopLimit { price, stop_price: stop }) } - else { OrderRequest::SubmitStopLimit { order_id, instrument, side, qty, price, stop_price: stop } } + ex(OrderKind::StopLimit { price, stop_price: stop }) } "TRAIL" => { // Optional initial stop trigger (tag 6117); default f64::MAX = unset. let trail_stop = if order.trail_stop_price == f64::MAX { 0 } else { (order.trail_stop_price * PRICE_SCALE_F) as i64 }; if order.trailing_percent > 0.0 { let pct = (order.trailing_percent * 100.0) as u32; - if extended { - OrderRequest::SubmitTrailingStopPctEx { - order_id, instrument, side, qty, trail_pct: pct, - tif: order.tif_byte(), - attrs: order.attrs(), - trail_stop_price: trail_stop, - } - } else { - OrderRequest::SubmitTrailingStopPct { order_id, instrument, side, qty, trail_pct: pct, trail_stop_price: trail_stop } - } + ex(OrderKind::TrailPct { trail_pct: pct, trail_stop_price: trail_stop }) } else { let trail = (order.aux_price * PRICE_SCALE_F) as i64; - if extended { ex(OrderKind::TrailingStop { trail_amt: trail, trail_stop_price: trail_stop }) } - else { OrderRequest::SubmitTrailingStop { order_id, instrument, side, qty, trail_amt: trail, trail_stop_price: trail_stop } } + ex(OrderKind::TrailingStop { trail_amt: trail, trail_stop_price: trail_stop }) } } "TRAIL LIMIT" => { @@ -1555,73 +1534,58 @@ impl ClientCore { let lmt_offset = (offset_f * PRICE_SCALE_F) as i64; let trail = (order.aux_price * PRICE_SCALE_F) as i64; let trail_stop = if order.trail_stop_price == f64::MAX { 0 } else { (order.trail_stop_price * PRICE_SCALE_F) as i64 }; - if extended { ex(OrderKind::TrailingStopLimit { lmt_offset, trail_amt: trail, trail_stop_price: trail_stop }) } - else { OrderRequest::SubmitTrailingStopLimit { order_id, instrument, side, qty, lmt_offset, trail_amt: trail, trail_stop_price: trail_stop } } + ex(OrderKind::TrailingStopLimit { lmt_offset, trail_amt: trail, trail_stop_price: trail_stop }) } "MOC" => { - if extended { ex(OrderKind::Moc) } - else { OrderRequest::SubmitMoc { order_id, instrument, side, qty } } + ex(OrderKind::Moc) } "LOC" => { let price = (order.lmt_price * PRICE_SCALE_F) as i64; - if extended { ex(OrderKind::Loc { price }) } - else { OrderRequest::SubmitLoc { order_id, instrument, side, qty, price } } + ex(OrderKind::Loc { price }) } "MIT" => { let stop = (order.aux_price * PRICE_SCALE_F) as i64; - if extended { ex(OrderKind::Mit { stop_price: stop }) } - else { OrderRequest::SubmitMit { order_id, instrument, side, qty, stop_price: stop } } + ex(OrderKind::Mit { stop_price: stop }) } "LIT" => { let price = (order.lmt_price * PRICE_SCALE_F) as i64; let stop = (order.aux_price * PRICE_SCALE_F) as i64; - if extended { ex(OrderKind::Lit { price, stop_price: stop }) } - else { OrderRequest::SubmitLit { order_id, instrument, side, qty, price, stop_price: stop } } + ex(OrderKind::Lit { price, stop_price: stop }) } "MTL" | "BOX TOP" => { - if extended { ex(OrderKind::Mtl) } - else { OrderRequest::SubmitMtl { order_id, instrument, side, qty } } + ex(OrderKind::Mtl) } "MKT PRT" => { - if extended { ex(OrderKind::MktPrt) } - else { OrderRequest::SubmitMktPrt { order_id, instrument, side, qty } } + ex(OrderKind::MktPrt) } "STP PRT" => { let stop = (order.aux_price * PRICE_SCALE_F) as i64; - if extended { ex(OrderKind::StpPrt { stop_price: stop }) } - else { OrderRequest::SubmitStpPrt { order_id, instrument, side, qty, stop_price: stop } } + ex(OrderKind::StpPrt { stop_price: stop }) } "REL" => { let offset = (order.aux_price * PRICE_SCALE_F) as i64; - if extended { ex(OrderKind::Rel { offset }) } - else { OrderRequest::SubmitRel { order_id, instrument, side, qty, offset } } + ex(OrderKind::Rel { offset }) } "PEG MKT" => { let offset = (order.aux_price * PRICE_SCALE_F) as i64; - if extended { ex(OrderKind::PegMkt { offset }) } - else { OrderRequest::SubmitPegMkt { order_id, instrument, side, qty, offset } } + ex(OrderKind::PegMkt { offset }) } "PEG MID" | "PEG MIDPT" => { let offset = (order.aux_price * PRICE_SCALE_F) as i64; - if extended { ex(OrderKind::PegMid { offset }) } - else { OrderRequest::SubmitPegMid { order_id, instrument, side, qty, offset } } + ex(OrderKind::PegMid { offset }) } "MIDPX" | "MIDPRICE" => { let cap = (order.lmt_price * PRICE_SCALE_F) as i64; - if extended { ex(OrderKind::MidPrice { price_cap: cap }) } - else { OrderRequest::SubmitMidPrice { order_id, instrument, side, qty, price_cap: cap } } + ex(OrderKind::MidPrice { price_cap: cap }) } "SNAP MKT" => { - if extended { ex(OrderKind::SnapMkt) } - else { OrderRequest::SubmitSnapMkt { order_id, instrument, side, qty } } + ex(OrderKind::SnapMkt) } "SNAP MID" | "SNAP MIDPT" => { - if extended { ex(OrderKind::SnapMid) } - else { OrderRequest::SubmitSnapMid { order_id, instrument, side, qty } } + ex(OrderKind::SnapMid) } "SNAP PRI" | "SNAP PRIM" => { - if extended { ex(OrderKind::SnapPri) } - else { OrderRequest::SubmitSnapPri { order_id, instrument, side, qty } } + ex(OrderKind::SnapPri) } _ => return Err(format!("Unsupported order type: '{}'", order.order_type)), }; diff --git a/src/engine/hot_loop/order_builder.rs b/src/engine/hot_loop/order_builder.rs index 31da49b2..5cb6dd12 100644 --- a/src/engine/hot_loop/order_builder.rs +++ b/src/engine/hot_loop/order_builder.rs @@ -1502,7 +1502,7 @@ fn send_order_ex( fields.push((40, "3".to_string())); // OrdType = Stop fields.push((99, format_price(stop_price).to_string())); // StopPx } - K::TrailingStop { trail_amt, trail_stop_price } => { + K::TrailingStop { trail_amt, .. } => { // Per ib-agent#136 capture: amount-based trailing stop carries // the trail amount in both 99 and 211 and requires 18=a. let t = format_price(trail_amt).to_string(); @@ -1510,11 +1510,9 @@ fn send_order_ex( fields.push((99, t.clone())); fields.push((211, t)); fields.push((18, "a".to_string())); - // Optional initial stop trigger (tag 6117), only when set (ib-agent#173). - if trail_stop_price > 0 { fields.push((6117, format_price(trail_stop_price).to_string())); } has_base_exec_inst = true; } - K::TrailingStopLimit { lmt_offset, trail_amt, trail_stop_price } => { + K::TrailingStopLimit { lmt_offset, trail_amt, .. } => { // Per ib-agent#136 capture: TRAIL LIMIT uses OrdType=TSL, no // tag 44, no tag 18; trail amount in both 99 and 211; 6370 is // the limit-vs-trail offset. @@ -1523,9 +1521,8 @@ fn send_order_ex( fields.push((99, t.clone())); fields.push((6370, format_price(lmt_offset).to_string())); fields.push((211, t)); - if trail_stop_price > 0 { fields.push((6117, format_price(trail_stop_price).to_string())); } } - K::TrailPct { trail_pct, trail_stop_price } => { + K::TrailPct { trail_pct, .. } => { // Per ib-agent#156 capture: percent-trail mirrors 99/211 as the // percent in decimal form (1.00 for 1%), alongside 6268 in // basis points and 18=a. @@ -1535,7 +1532,6 @@ fn send_order_ex( fields.push((211, pct_decimal)); fields.push((18, "a".to_string())); fields.push((6268, trail_pct.to_string())); - if trail_stop_price > 0 { fields.push((6117, format_price(trail_stop_price).to_string())); } has_base_exec_inst = true; } K::Moc => fields.push((40, "5".to_string())), @@ -1558,29 +1554,12 @@ fn send_order_ex( fields.push((40, "SP".to_string())); fields.push((99, format_price(stop_price).to_string())); } - K::MidPrice { price_cap } => { - fields.push((40, "MIDPX".to_string())); - if price_cap > 0 { - fields.push((44, format_price(price_cap).to_string())); - } - } + K::MidPrice { .. } => fields.push((40, "MIDPX".to_string())), K::SnapMkt => fields.push((40, "SMKT".to_string())), K::SnapMid => fields.push((40, "SMID".to_string())), K::SnapPri => fields.push((40, "SREL".to_string())), - K::PegMkt { offset } => { - fields.push((40, "E".to_string())); - if offset > 0 { - fields.push((211, format_price(offset).to_string())); - } - } - K::PegMid { offset } => { - fields.push((40, "E".to_string())); - fields.push((8403, "0.0".to_string())); // midOffsetAtWhole — differentiates PEGMID - fields.push((8404, "0.0".to_string())); // midOffsetAtHalf - if offset > 0 { - fields.push((211, format_price(offset).to_string())); - } - } + K::PegMkt { .. } => fields.push((40, "E".to_string())), + K::PegMid { .. } => fields.push((40, "E".to_string())), K::Rel { offset } => { // Per ib-agent#138 capture: Relative shares OrdType=P and is // disambiguated by 18=R; peg offset on 211, no tag 44. @@ -1734,6 +1713,32 @@ fn send_order_ex( } } + // The optional tags each type appends last, in the position the per-type + // encoders give them: after 204 and the attribute block, not in among the + // order-type tags. The values and the conditions are unchanged. + match &kind { + K::MidPrice { price_cap } if *price_cap > 0 => { + fields.push((44, format_price(*price_cap).to_string())); + } + K::PegMkt { offset } if *offset > 0 => { + fields.push((211, format_price(*offset).to_string())); + } + K::PegMid { offset } => { + fields.push((8403, "0.0".to_string())); // midOffsetAtWhole — differentiates PEGMID + fields.push((8404, "0.0".to_string())); // midOffsetAtHalf + if *offset > 0 { + fields.push((211, format_price(*offset).to_string())); + } + } + // Optional initial stop trigger (ib-agent#173). + K::TrailingStop { trail_stop_price, .. } + | K::TrailingStopLimit { trail_stop_price, .. } + | K::TrailPct { trail_stop_price, .. } if *trail_stop_price > 0 => { + fields.push((6117, format_price(*trail_stop_price).to_string())); + } + _ => {} + } + // Strategy and preview tags last, in the position they held in the encoders // this path replaced: after 204 and the attribute block (ibx#318). match &kind { @@ -1956,6 +1961,100 @@ mod tests { assert!(shared.orders.drain_order_updates().is_empty()); } + /// Encode one request and return the frame with the parts that cannot be + /// equal between two sends removed: sequence number, both timestamps, and + /// the body length and checksum that cover them. + fn encode_for_test(req: crate::types::OrderRequest) -> String { + use std::io::Read; + let (conn, mut peer) = crate::protocol::connection::Connection::for_test(); + let mut conn = Some(conn); + let mut context = Context::new(); + context.register_instrument(756733); + context.set_symbol(0, "SPY".to_string()); + context.pending_orders.push(req); + let mut hb = crate::engine::hot_loop::HeartbeatState::new(); + let shared = std::sync::Arc::new(SharedState::new()); + drain_and_send_orders(&mut conn, &mut context, "DU123456", &mut hb, false, &shared); + + let mut buf = [0u8; 8192]; + let n = peer.read(&mut buf).unwrap(); + String::from_utf8_lossy(&buf[..n]) + .split('\u{1}') + .filter(|f| !f.is_empty()) + .filter(|f| !["9=", "10=", "34=", "52=", "60="].iter().any(|t| f.starts_with(t))) + .collect::>() + .join("|") + } + + /// Every order type is encoded twice: once by its own request variant and + /// once through the shared encoder carrying the same order with no extended + /// attributes. The two must be the same message. + /// + /// That equality is what makes the per-type variants redundant. While both + /// exist, it is also what stops them drifting — a tag added to one encoder + /// and not the other is exactly how an order type ends up shipping without + /// something the caller set, which is #240 and #318. + #[test] + fn the_shared_encoder_restates_every_type_exactly_as_its_own_variant_does() { + use crate::types::{OrderKind as K, OrderRequest as R, PRICE_SCALE}; + let (id, inst, side, qty) = (7u64, 0u32, Side::Buy, 1u32); + let px = 100 * PRICE_SCALE; + let stop = 90 * PRICE_SCALE; + let off = PRICE_SCALE / 2; + + let cases: Vec<(&str, R, K)> = vec![ + ("MKT", R::SubmitMarket { order_id: id, instrument: inst, side, qty }, K::Market), + ("LMT", R::SubmitLimit { order_id: id, instrument: inst, side, qty, price: px }, + K::Limit { price: px }), + ("STP", R::SubmitStop { order_id: id, instrument: inst, side, qty, stop_price: stop }, + K::Stop { stop_price: stop }), + ("STP LMT", R::SubmitStopLimit { order_id: id, instrument: inst, side, qty, price: px, stop_price: stop }, + K::StopLimit { price: px, stop_price: stop }), + ("MOC", R::SubmitMoc { order_id: id, instrument: inst, side, qty }, K::Moc), + ("LOC", R::SubmitLoc { order_id: id, instrument: inst, side, qty, price: px }, + K::Loc { price: px }), + ("MIT", R::SubmitMit { order_id: id, instrument: inst, side, qty, stop_price: stop }, + K::Mit { stop_price: stop }), + ("LIT", R::SubmitLit { order_id: id, instrument: inst, side, qty, price: px, stop_price: stop }, + K::Lit { price: px, stop_price: stop }), + ("MTL", R::SubmitMtl { order_id: id, instrument: inst, side, qty }, K::Mtl), + ("MKT PRT", R::SubmitMktPrt { order_id: id, instrument: inst, side, qty }, K::MktPrt), + ("STP PRT", R::SubmitStpPrt { order_id: id, instrument: inst, side, qty, stop_price: stop }, + K::StpPrt { stop_price: stop }), + ("MIDPX", R::SubmitMidPrice { order_id: id, instrument: inst, side, qty, price_cap: px }, + K::MidPrice { price_cap: px }), + ("SNAP MKT", R::SubmitSnapMkt { order_id: id, instrument: inst, side, qty }, K::SnapMkt), + ("SNAP MID", R::SubmitSnapMid { order_id: id, instrument: inst, side, qty }, K::SnapMid), + ("SNAP PRI", R::SubmitSnapPri { order_id: id, instrument: inst, side, qty }, K::SnapPri), + ("PEG MKT", R::SubmitPegMkt { order_id: id, instrument: inst, side, qty, offset: off }, + K::PegMkt { offset: off }), + ("PEG MID", R::SubmitPegMid { order_id: id, instrument: inst, side, qty, offset: off }, + K::PegMid { offset: off }), + ("REL", R::SubmitRel { order_id: id, instrument: inst, side, qty, offset: off }, + K::Rel { offset: off }), + ("TRAIL", R::SubmitTrailingStop { order_id: id, instrument: inst, side, qty, trail_amt: off, trail_stop_price: stop }, + K::TrailingStop { trail_amt: off, trail_stop_price: stop }), + ("TRAIL LIMIT", R::SubmitTrailingStopLimit { order_id: id, instrument: inst, side, qty, lmt_offset: off, trail_amt: off, trail_stop_price: stop }, + K::TrailingStopLimit { lmt_offset: off, trail_amt: off, trail_stop_price: stop }), + ("TRAIL PCT", R::SubmitTrailingStopPct { order_id: id, instrument: inst, side, qty, trail_pct: 100, trail_stop_price: stop }, + K::TrailPct { trail_pct: 100, trail_stop_price: stop }), + ]; + + let mut differences = Vec::new(); + for (name, plain, kind) in cases { + let own = encode_for_test(plain); + let shared = encode_for_test(R::SubmitEx { + order_id: id, instrument: inst, side, qty, kind, + tif: b'0', attrs: crate::types::OrderAttrs::default(), + }); + if own != shared { + differences.push(format!("{name}\n own: {own}\n shrd: {shared}")); + } + } + assert!(differences.is_empty(), + "{} type(s) encoded differently:\n{}", differences.len(), differences.join("\n")); + } + /// ibx#318: adaptive, algo and what-if orders returned early into their own /// encoders, which carried no attribute block at all — so outside-RTH, the /// parent link and the OCA group were accepted by the API and silently From 50708834dadef82226fd541c25327c8f7df39b0b Mon Sep 17 00:00:00 2001 From: userFRM Date: Thu, 30 Jul 2026 18:10:26 +0200 Subject: [PATCH 4/4] orders: remove the per-type order request variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With every order type encoding through the shared path, the per-type request variants and their encoders are unreachable from the API surface and carry no behaviour of their own. Twenty-three variants and their encoder arms are removed. The `Context` helpers that constructed them build the equivalent `SubmitEx` instead, with the same defaults they already implied: `submit_market`, `submit_limit`, `submit_stop` and the rest keep their signatures, so a caller of the low-level engine API sees no change. `OrderRequest::order_id` already answers what the integration harness was answering with a forty-line match over every submit variant; it calls the accessor now. The equivalence test goes with them. Its subject was the second encoding of each type, and it existed to establish that the two agreed before the second one was removed. What it was guarding against — one encoder gaining a tag the other does not — is no longer expressible. OrderRequest variants 42 → 15 send_fix sites in one file 42 → 17 order_builder.rs 2059 → 1443 lines Three of the diagnostics `ib_paper_compat` already produced were call sites constructing a trailing-stop variant without its optional trigger; migrating them supplied it, so that target now reports nine rather than twelve. The nine are the ones it reported before, unchanged. No behaviour change: the removed encoders were byte-identical to the shared one for every type they covered. --- src/bin/bench_market_order.rs | 10 +- src/bin/benchmark.rs | 10 +- src/engine/context.rs | 212 ++--- src/engine/hot_loop/order_builder.rs | 1133 ++++------------------- src/types.rs | 348 +------ tests/error_edge_concurrency.rs | 8 +- tests/ib_paper_compat/account.rs | 22 +- tests/ib_paper_compat/common.rs | 41 +- tests/ib_paper_compat/error_handling.rs | 4 +- tests/ib_paper_compat/orders.rs | 149 ++- 10 files changed, 370 insertions(+), 1567 deletions(-) diff --git a/src/bin/bench_market_order.rs b/src/bin/bench_market_order.rs index 068651ce..b60d0a13 100644 --- a/src/bin/bench_market_order.rs +++ b/src/bin/bench_market_order.rs @@ -54,11 +54,14 @@ fn main() { for i in 0..iterations { // BUY let buy_time = Instant::now(); - session.send_order(OrderRequest::SubmitMarket { + session.send_order(OrderRequest::SubmitEx { order_id, instrument, side: Side::Buy, qty: 1, + kind: OrderKind::Market, + tif: b'0', + attrs: OrderAttrs::default(), }); let buy_ns = wait_for_fill(&session.event_rx, order_id, buy_time, &start, "BUY"); @@ -71,11 +74,14 @@ fn main() { // SELL let sell_time = Instant::now(); - session.send_order(OrderRequest::SubmitMarket { + session.send_order(OrderRequest::SubmitEx { order_id, instrument, side: Side::Sell, qty: 1, + kind: OrderKind::Market, + tif: b'0', + attrs: OrderAttrs::default(), }); let sell_ns = wait_for_fill(&session.event_rx, order_id, sell_time, &start, "SELL"); diff --git a/src/bin/benchmark.rs b/src/bin/benchmark.rs index 139f0d8e..f6a5552c 100644 --- a/src/bin/benchmark.rs +++ b/src/bin/benchmark.rs @@ -284,11 +284,14 @@ fn main() { ); buy_submit_time = Some(Instant::now()); let _ = control_tx2.send(ControlCommand::Order( - OrderRequest::SubmitMarket { + OrderRequest::SubmitEx { order_id: 1, instrument: target_instrument, side: Side::Buy, qty: 1, + kind: OrderKind::Market, + tif: b'0', + attrs: OrderAttrs::default(), }, )); order_phase = 1; @@ -317,11 +320,14 @@ fn main() { ); sell_submit_time = Some(Instant::now()); let _ = control_tx2.send(ControlCommand::Order( - OrderRequest::SubmitMarket { + OrderRequest::SubmitEx { order_id: 2, instrument: fill.instrument, side: Side::Sell, qty: 1, + kind: OrderKind::Market, + tif: b'0', + attrs: OrderAttrs::default(), }, )); order_phase = 2; diff --git a/src/engine/context.rs b/src/engine/context.rs index fda42efe..6291f8c2 100644 --- a/src/engine/context.rs +++ b/src/engine/context.rs @@ -155,12 +155,10 @@ impl Context { ) -> OrderId { let id = self.next_order_id; self.next_order_id += 1; - self.pending_orders.push(OrderRequest::SubmitLimit { - order_id: id, - instrument, - side, - qty, - price, + self.pending_orders.push(OrderRequest::SubmitEx { + order_id: id, instrument, side, qty, + kind: OrderKind::Limit { price }, + tif: b'0', attrs: OrderAttrs::default(), }); id } @@ -173,11 +171,10 @@ impl Context { ) -> OrderId { let id = self.next_order_id; self.next_order_id += 1; - self.pending_orders.push(OrderRequest::SubmitMarket { - order_id: id, - instrument, - side, - qty, + self.pending_orders.push(OrderRequest::SubmitEx { + order_id: id, instrument, side, qty, + kind: OrderKind::Market, + tif: b'0', attrs: OrderAttrs::default(), }); id } @@ -191,12 +188,10 @@ impl Context { ) -> OrderId { let id = self.next_order_id; self.next_order_id += 1; - self.pending_orders.push(OrderRequest::SubmitStop { - order_id: id, - instrument, - side, - qty, - stop_price, + self.pending_orders.push(OrderRequest::SubmitEx { + order_id: id, instrument, side, qty, + kind: OrderKind::Stop { stop_price }, + tif: b'0', attrs: OrderAttrs::default(), }); id } @@ -211,13 +206,10 @@ impl Context { ) -> OrderId { let id = self.next_order_id; self.next_order_id += 1; - self.pending_orders.push(OrderRequest::SubmitStopLimit { - order_id: id, - instrument, - side, - qty, - price, - stop_price, + self.pending_orders.push(OrderRequest::SubmitEx { + order_id: id, instrument, side, qty, + kind: OrderKind::StopLimit { price, stop_price }, + tif: b'0', attrs: OrderAttrs::default(), }); id } @@ -334,13 +326,10 @@ impl Context { ) -> OrderId { let id = self.next_order_id; self.next_order_id += 1; - self.pending_orders.push(OrderRequest::SubmitTrailingStop { - order_id: id, - instrument, - side, - qty, - trail_amt, - trail_stop_price: 0, + self.pending_orders.push(OrderRequest::SubmitEx { + order_id: id, instrument, side, qty, + kind: OrderKind::TrailingStop { trail_amt, trail_stop_price: 0 }, + tif: b'0', attrs: OrderAttrs::default(), }); id } @@ -355,14 +344,10 @@ impl Context { ) -> OrderId { let id = self.next_order_id; self.next_order_id += 1; - self.pending_orders.push(OrderRequest::SubmitTrailingStopLimit { - order_id: id, - instrument, - side, - qty, - lmt_offset, - trail_amt, - trail_stop_price: 0, + self.pending_orders.push(OrderRequest::SubmitEx { + order_id: id, instrument, side, qty, + kind: OrderKind::TrailingStopLimit { lmt_offset, trail_amt, trail_stop_price: 0 }, + tif: b'0', attrs: OrderAttrs::default(), }); id } @@ -376,13 +361,10 @@ impl Context { ) -> OrderId { let id = self.next_order_id; self.next_order_id += 1; - self.pending_orders.push(OrderRequest::SubmitTrailingStopPct { - order_id: id, - instrument, - side, - qty, - trail_pct, - trail_stop_price: 0, + self.pending_orders.push(OrderRequest::SubmitEx { + order_id: id, instrument, side, qty, + kind: OrderKind::TrailPct { trail_pct, trail_stop_price: 0 }, + tif: b'0', attrs: OrderAttrs::default(), }); id } @@ -395,11 +377,10 @@ impl Context { ) -> OrderId { let id = self.next_order_id; self.next_order_id += 1; - self.pending_orders.push(OrderRequest::SubmitMoc { - order_id: id, - instrument, - side, - qty, + self.pending_orders.push(OrderRequest::SubmitEx { + order_id: id, instrument, side, qty, + kind: OrderKind::Moc, + tif: b'0', attrs: OrderAttrs::default(), }); id } @@ -413,12 +394,10 @@ impl Context { ) -> OrderId { let id = self.next_order_id; self.next_order_id += 1; - self.pending_orders.push(OrderRequest::SubmitLoc { - order_id: id, - instrument, - side, - qty, - price, + self.pending_orders.push(OrderRequest::SubmitEx { + order_id: id, instrument, side, qty, + kind: OrderKind::Loc { price }, + tif: b'0', attrs: OrderAttrs::default(), }); id } @@ -432,12 +411,10 @@ impl Context { ) -> OrderId { let id = self.next_order_id; self.next_order_id += 1; - self.pending_orders.push(OrderRequest::SubmitMit { - order_id: id, - instrument, - side, - qty, - stop_price, + self.pending_orders.push(OrderRequest::SubmitEx { + order_id: id, instrument, side, qty, + kind: OrderKind::Mit { stop_price }, + tif: b'0', attrs: OrderAttrs::default(), }); id } @@ -452,13 +429,10 @@ impl Context { ) -> OrderId { let id = self.next_order_id; self.next_order_id += 1; - self.pending_orders.push(OrderRequest::SubmitLit { - order_id: id, - instrument, - side, - qty, - price, - stop_price, + self.pending_orders.push(OrderRequest::SubmitEx { + order_id: id, instrument, side, qty, + kind: OrderKind::Lit { price, stop_price }, + tif: b'0', attrs: OrderAttrs::default(), }); id } @@ -505,14 +479,10 @@ impl Context { ) -> OrderId { let id = self.next_order_id; self.next_order_id += 1; - self.pending_orders.push(OrderRequest::SubmitLimitEx { - order_id: id, - instrument, - side, - qty, - price, - tif, - attrs, + self.pending_orders.push(OrderRequest::SubmitEx { + order_id: id, instrument, side, qty, + kind: OrderKind::Limit { price }, + tif, attrs, }); id } @@ -526,12 +496,10 @@ impl Context { ) -> OrderId { let id = self.next_order_id; self.next_order_id += 1; - self.pending_orders.push(OrderRequest::SubmitRel { - order_id: id, - instrument, - side, - qty, - offset, + self.pending_orders.push(OrderRequest::SubmitEx { + order_id: id, instrument, side, qty, + kind: OrderKind::Rel { offset }, + tif: b'0', attrs: OrderAttrs::default(), }); id } @@ -583,8 +551,10 @@ impl Context { ) -> OrderId { let id = self.next_order_id; self.next_order_id += 1; - self.pending_orders.push(OrderRequest::SubmitMtl { + self.pending_orders.push(OrderRequest::SubmitEx { order_id: id, instrument, side, qty, + kind: OrderKind::Mtl, + tif: b'0', attrs: OrderAttrs::default(), }); id } @@ -597,8 +567,10 @@ impl Context { ) -> OrderId { let id = self.next_order_id; self.next_order_id += 1; - self.pending_orders.push(OrderRequest::SubmitMktPrt { + self.pending_orders.push(OrderRequest::SubmitEx { order_id: id, instrument, side, qty, + kind: OrderKind::MktPrt, + tif: b'0', attrs: OrderAttrs::default(), }); id } @@ -612,8 +584,10 @@ impl Context { ) -> OrderId { let id = self.next_order_id; self.next_order_id += 1; - self.pending_orders.push(OrderRequest::SubmitStpPrt { - order_id: id, instrument, side, qty, stop_price, + self.pending_orders.push(OrderRequest::SubmitEx { + order_id: id, instrument, side, qty, + kind: OrderKind::StpPrt { stop_price }, + tif: b'0', attrs: OrderAttrs::default(), }); id } @@ -627,8 +601,10 @@ impl Context { ) -> OrderId { let id = self.next_order_id; self.next_order_id += 1; - self.pending_orders.push(OrderRequest::SubmitMidPrice { - order_id: id, instrument, side, qty, price_cap, + self.pending_orders.push(OrderRequest::SubmitEx { + order_id: id, instrument, side, qty, + kind: OrderKind::MidPrice { price_cap }, + tif: b'0', attrs: OrderAttrs::default(), }); id } @@ -641,8 +617,10 @@ impl Context { ) -> OrderId { let id = self.next_order_id; self.next_order_id += 1; - self.pending_orders.push(OrderRequest::SubmitSnapMkt { + self.pending_orders.push(OrderRequest::SubmitEx { order_id: id, instrument, side, qty, + kind: OrderKind::SnapMkt, + tif: b'0', attrs: OrderAttrs::default(), }); id } @@ -655,8 +633,10 @@ impl Context { ) -> OrderId { let id = self.next_order_id; self.next_order_id += 1; - self.pending_orders.push(OrderRequest::SubmitSnapMid { + self.pending_orders.push(OrderRequest::SubmitEx { order_id: id, instrument, side, qty, + kind: OrderKind::SnapMid, + tif: b'0', attrs: OrderAttrs::default(), }); id } @@ -669,8 +649,10 @@ impl Context { ) -> OrderId { let id = self.next_order_id; self.next_order_id += 1; - self.pending_orders.push(OrderRequest::SubmitSnapPri { + self.pending_orders.push(OrderRequest::SubmitEx { order_id: id, instrument, side, qty, + kind: OrderKind::SnapPri, + tif: b'0', attrs: OrderAttrs::default(), }); id } @@ -684,8 +666,10 @@ impl Context { ) -> OrderId { let id = self.next_order_id; self.next_order_id += 1; - self.pending_orders.push(OrderRequest::SubmitPegMkt { - order_id: id, instrument, side, qty, offset, + self.pending_orders.push(OrderRequest::SubmitEx { + order_id: id, instrument, side, qty, + kind: OrderKind::PegMkt { offset }, + tif: b'0', attrs: OrderAttrs::default(), }); id } @@ -699,8 +683,10 @@ impl Context { ) -> OrderId { let id = self.next_order_id; self.next_order_id += 1; - self.pending_orders.push(OrderRequest::SubmitPegMid { - order_id: id, instrument, side, qty, offset, + self.pending_orders.push(OrderRequest::SubmitEx { + order_id: id, instrument, side, qty, + kind: OrderKind::PegMid { offset }, + tif: b'0', attrs: OrderAttrs::default(), }); id } @@ -1019,12 +1005,9 @@ mod tests { let orders: Vec<_> = ctx.drain_pending_orders().collect(); assert_eq!(orders.len(), 1); match orders[0] { - OrderRequest::SubmitLimit { - instrument, - side, - qty, - price, - .. + OrderRequest::SubmitEx { + order_id, instrument, side, qty, + kind: OrderKind::Limit { price }, .. } => { assert_eq!(instrument, 0); assert_eq!(side, Side::Buy); @@ -1043,11 +1026,9 @@ mod tests { let orders: Vec<_> = ctx.drain_pending_orders().collect(); assert_eq!(orders.len(), 1); match orders[0] { - OrderRequest::SubmitMarket { - instrument, - side, - qty, - .. + OrderRequest::SubmitEx { + order_id, instrument, side, qty, + kind: OrderKind::Market, .. } => { assert_eq!(instrument, 1); assert_eq!(side, Side::Sell); @@ -1366,7 +1347,10 @@ mod tests { let orders: Vec<_> = ctx.drain_pending_orders().collect(); assert_eq!(orders.len(), 1); match orders[0] { - OrderRequest::SubmitLimit { price, .. } => { + OrderRequest::SubmitEx { + order_id, instrument, side, qty, + kind: OrderKind::Limit { price }, .. + } => { assert_eq!(price, 150 * PRICE_SCALE); } _ => panic!("expected SubmitLimit"), @@ -1524,7 +1508,10 @@ mod tests { let orders: Vec<_> = ctx.drain_pending_orders().collect(); assert_eq!(orders.len(), 1); match orders[0] { - OrderRequest::SubmitStop { order_id, instrument, side, qty, stop_price } => { + OrderRequest::SubmitEx { + order_id, instrument, side, qty, + kind: OrderKind::Stop { stop_price }, .. + } => { assert_eq!(order_id, id); assert_eq!(instrument, 0); assert_eq!(side, Side::Sell); @@ -1641,7 +1628,10 @@ mod tests { let orders: Vec<_> = ctx.drain_pending_orders().collect(); assert_eq!(orders.len(), 1); match &orders[0] { - OrderRequest::SubmitMtl { order_id, instrument, side, qty } => { + OrderRequest::SubmitEx { + order_id, instrument, side, qty, + kind: OrderKind::Mtl, .. + } => { assert_eq!(*order_id, id); assert_eq!(*instrument, 0); assert_eq!(*side, Side::Buy); diff --git a/src/engine/hot_loop/order_builder.rs b/src/engine/hot_loop/order_builder.rs index 5cb6dd12..79b4efbe 100644 --- a/src/engine/hot_loop/order_builder.rs +++ b/src/engine/hot_loop/order_builder.rs @@ -38,747 +38,9 @@ pub(crate) fn drain_and_send_orders( order_req.snap_prices(context.market.min_tick_scaled(instrument)); } let result = match order_req { - OrderRequest::SubmitLimit { order_id, instrument, side, qty, price } => { - context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, price, b'2', b'0', 0, - )); - let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); - let clord_str = format!("{}.{}", order_id, ver); - let side_str = fix_side(side); - let qty_str = format_uint(qty as u64); - let price_str = format_price(price); - let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, destination) = context.market.order_routing(instrument); - let now = chrono_free_timestamp(); - conn.send_fix(&[ - (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), - (fix::TAG_SENDING_TIME, &now), - (11, &clord_str), // ClOrdID - (1, account_id), // Account - (21, "2"), // HandlInst = Automated - (55, &symbol), // Symbol - (54, side_str), // Side - (38, &qty_str), // OrderQty - (40, "2"), // OrdType = Limit - (44, &price_str), // Price - (59, "0"), // TIF = DAY - (60, &now), // TransactTime - (167, &sec_type_str), // SecurityType = CommonStock - (100, &destination), - (6210, &destination), // ExDestination - (15, "USD"), // Currency - (204, "0"), // CustomerOrFirm - ]) - } - OrderRequest::SubmitStopLimit { order_id, instrument, side, qty, price, stop_price } => { - context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, price, b'4', b'0', stop_price, - )); - let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); - let clord_str = format!("{}.{}", order_id, ver); - let side_str = fix_side(side); - let qty_str = format_uint(qty as u64); - let price_str = format_price(price); - let stop_str = format_price(stop_price); - let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, destination) = context.market.order_routing(instrument); - let now = chrono_free_timestamp(); - conn.send_fix(&[ - (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), - (fix::TAG_SENDING_TIME, &now), - (11, &clord_str), - (1, account_id), - (21, "2"), - (55, &symbol), - (54, side_str), - (38, &qty_str), - (40, "4"), // OrdType = Stop Limit - (44, &price_str), // Limit Price - (99, &stop_str), // StopPx - (59, "0"), // TIF = DAY - (60, &now), - (167, &sec_type_str), - (100, &destination), - (6210, &destination), - (15, "USD"), - (204, "0"), - ]) - } - OrderRequest::SubmitLimitGtc { order_id, instrument, side, qty, price, outside_rth } => { - context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, price, b'2', b'1', 0, - )); - let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); - let clord_str = format!("{}.{}", order_id, ver); - let side_str = fix_side(side); - let qty_str = format_uint(qty as u64); - let price_str = format_price(price); - let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, destination) = context.market.order_routing(instrument); - let now = chrono_free_timestamp(); - let mut fields: Vec<(u32, &str)> = vec![ - (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), - (fix::TAG_SENDING_TIME, &now), - (11, &clord_str), - (1, account_id), - (21, "2"), - (55, &symbol), - (54, side_str), - (38, &qty_str), - (40, "2"), // OrdType = Limit - (44, &price_str), - (59, "1"), // TIF = GTC - (60, &now), - (167, &sec_type_str), - (100, &destination), - (6210, &destination), - (15, "USD"), - (204, "0"), - ]; - if outside_rth { - fields.push((6433, "1")); // OutsideRTH - } - conn.send_fix(&fields) - } - OrderRequest::SubmitLimitEx { order_id, instrument, side, qty, price, tif, attrs } => { - send_order_ex(conn, context, account_id, order_id, instrument, side, qty, - crate::types::OrderKind::Limit { price }, tif, &attrs) - } - OrderRequest::SubmitEx { order_id, instrument, side, qty, kind, tif, attrs } => { - send_order_ex(conn, context, account_id, order_id, instrument, side, qty, - kind, tif, &attrs) - } - OrderRequest::SubmitMarket { order_id, instrument, side, qty } => { - context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, 0, b'1', b'0', 0, - )); - let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); - let clord_str = format!("{}.{}", order_id, ver); - let side_str = fix_side(side); - let qty_str = format_uint(qty as u64); - let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, destination) = context.market.order_routing(instrument); - let now = chrono_free_timestamp(); - log::info!("Sending MKT order: clord={} acct={} sym={} side={} qty={}", - clord_str, account_id, symbol, side_str, qty_str); - conn.send_fix(&[ - (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), - (fix::TAG_SENDING_TIME, &now), - (11, &clord_str), - (1, account_id), // Account - (21, "2"), // HandlInst = Automated - (55, &symbol), // Symbol - (54, side_str), - (38, &qty_str), - (40, "1"), // OrdType = Market - (59, "0"), // TIF = DAY - (60, &now), // TransactTime - (167, &sec_type_str), // SecurityType - (100, &destination), - (6210, &destination), // ExDestination - (15, "USD"), // Currency - (204, "0"), // CustomerOrFirm - ]) - } - OrderRequest::SubmitStop { order_id, instrument, side, qty, stop_price } => { - context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, stop_price, b'3', b'0', stop_price, - )); - let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); - let clord_str = format!("{}.{}", order_id, ver); - let side_str = fix_side(side); - let qty_str = format_uint(qty as u64); - let stop_str = format_price(stop_price); - let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, destination) = context.market.order_routing(instrument); - let now = chrono_free_timestamp(); - conn.send_fix(&[ - (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), - (fix::TAG_SENDING_TIME, &now), - (11, &clord_str), - (1, account_id), - (21, "2"), // HandlInst = Automated - (55, &symbol), - (54, side_str), - (38, &qty_str), - (40, "3"), // OrdType = Stop - (99, &stop_str), // StopPx - (59, "0"), // TIF = DAY - (60, &now), - (167, &sec_type_str), - (100, &destination), - (6210, &destination), - (15, "USD"), - (204, "0"), - ]) - } - OrderRequest::SubmitStopGtc { order_id, instrument, side, qty, stop_price, outside_rth } => { - context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, stop_price, b'3', b'1', stop_price, - )); - let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); - let clord_str = format!("{}.{}", order_id, ver); - let side_str = fix_side(side); - let qty_str = format_uint(qty as u64); - let stop_str = format_price(stop_price); - let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, destination) = context.market.order_routing(instrument); - let now = chrono_free_timestamp(); - let mut fields: Vec<(u32, &str)> = vec![ - (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), - (fix::TAG_SENDING_TIME, &now), - (11, &clord_str), - (1, account_id), - (21, "2"), - (55, &symbol), - (54, side_str), - (38, &qty_str), - (40, "3"), // OrdType = Stop - (99, &stop_str), - (59, "1"), // TIF = GTC - (60, &now), - (167, &sec_type_str), - (100, &destination), - (6210, &destination), - (15, "USD"), - (204, "0"), - ]; - if outside_rth { - fields.push((6433, "1")); - } - conn.send_fix(&fields) - } - OrderRequest::SubmitStopLimitGtc { order_id, instrument, side, qty, price, stop_price, outside_rth } => { - context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, price, b'4', b'1', stop_price, - )); - let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); - let clord_str = format!("{}.{}", order_id, ver); - let side_str = fix_side(side); - let qty_str = format_uint(qty as u64); - let price_str = format_price(price); - let stop_str = format_price(stop_price); - let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, destination) = context.market.order_routing(instrument); - let now = chrono_free_timestamp(); - let mut fields: Vec<(u32, &str)> = vec![ - (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), - (fix::TAG_SENDING_TIME, &now), - (11, &clord_str), - (1, account_id), - (21, "2"), - (55, &symbol), - (54, side_str), - (38, &qty_str), - (40, "4"), // OrdType = Stop Limit - (44, &price_str), - (99, &stop_str), - (59, "1"), // TIF = GTC - (60, &now), - (167, &sec_type_str), - (100, &destination), - (6210, &destination), - (15, "USD"), - (204, "0"), - ]; - if outside_rth { - fields.push((6433, "1")); - } - conn.send_fix(&fields) - } - OrderRequest::SubmitLimitIoc { order_id, instrument, side, qty, price } => { - context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, price, b'2', b'3', 0, - )); - let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); - let clord_str = format!("{}.{}", order_id, ver); - let side_str = fix_side(side); - let qty_str = format_uint(qty as u64); - let price_str = format_price(price); - let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, destination) = context.market.order_routing(instrument); - let now = chrono_free_timestamp(); - conn.send_fix(&[ - (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), - (fix::TAG_SENDING_TIME, &now), - (11, &clord_str), - (1, account_id), - (21, "2"), - (55, &symbol), - (54, side_str), - (38, &qty_str), - (40, "2"), // OrdType = Limit - (44, &price_str), - (59, "3"), // TIF = IOC - (60, &now), - (167, &sec_type_str), - (100, &destination), - (6210, &destination), - (15, "USD"), - (204, "0"), - ]) - } - OrderRequest::SubmitLimitFok { order_id, instrument, side, qty, price } => { - context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, price, b'2', b'4', 0, - )); - let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); - let clord_str = format!("{}.{}", order_id, ver); - let side_str = fix_side(side); - let qty_str = format_uint(qty as u64); - let price_str = format_price(price); - let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, destination) = context.market.order_routing(instrument); - let now = chrono_free_timestamp(); - conn.send_fix(&[ - (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), - (fix::TAG_SENDING_TIME, &now), - (11, &clord_str), - (1, account_id), - (21, "2"), - (55, &symbol), - (54, side_str), - (38, &qty_str), - (40, "2"), // OrdType = Limit - (44, &price_str), - (59, "4"), // TIF = FOK - (60, &now), - (167, &sec_type_str), - (100, &destination), - (6210, &destination), - (15, "USD"), - (204, "0"), - ]) - } - OrderRequest::SubmitTrailingStop { order_id, instrument, side, qty, trail_amt, trail_stop_price } => { - context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, 0, b'P', b'0', 0, - )); - let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); - let clord_str = format!("{}.{}", order_id, ver); - let side_str = fix_side(side); - let qty_str = format_uint(qty as u64); - let trail_str = format_price(trail_amt); - let trail_stop_str = format_price(trail_stop_price); - let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, destination) = context.market.order_routing(instrument); - let now = chrono_free_timestamp(); - // Per ib-agent#136 capture: amount-based trailing stop carries - // the trail amount in both 99 (StopPx) and 211 (PegOffset), - // and requires 18=a (ExecInst = TrailingStop). Without 18, - // the gateway rejects with "Invalid value in field # 18". - let mut fields: Vec<(u32, &str)> = vec![ - (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), - (fix::TAG_SENDING_TIME, &now), - (11, &clord_str), - (1, account_id), - (21, "2"), - (55, &symbol), - (54, side_str), - (38, &qty_str), - (40, "P"), // OrdType = Stop (used for trailing too) - (99, &trail_str), // StopPx = trail amount - (211, &trail_str), // PegOffset = trail amount - (18, "a"), // ExecInst = TrailingStop - (59, "0"), // TIF = DAY - (60, &now), - (167, &sec_type_str), - (100, &destination), - (6210, &destination), - (15, "USD"), - (204, "0"), - ]; - // Optional initial stop trigger (tag 6117), only when set - // (ib-agent#173). - if trail_stop_price > 0 { fields.push((6117, &trail_stop_str)); } - conn.send_fix(&fields) - } - OrderRequest::SubmitTrailingStopLimit { order_id, instrument, side, qty, lmt_offset, trail_amt, trail_stop_price } => { - context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, lmt_offset, b'P', b'0', 0, - )); - let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); - let clord_str = format!("{}.{}", order_id, ver); - let side_str = fix_side(side); - let qty_str = format_uint(qty as u64); - let offset_str = format_price(lmt_offset); - let trail_str = format_price(trail_amt); - let trail_stop_str = format_price(trail_stop_price); - let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, destination) = context.market.order_routing(instrument); - let now = chrono_free_timestamp(); - // Per ib-agent#136 capture: TRAIL LIMIT uses OrdType=TSL (not P), - // does NOT carry tag 44 (gateway derives the limit price from - // 6370 + the activation reference), does NOT carry tag 18, and - // carries the trail amount in both 99 and 211. The 6370 - // LimitPriceOffset is the limit-vs-trail offset. - let mut fields: Vec<(u32, &str)> = vec![ - (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), - (fix::TAG_SENDING_TIME, &now), - (11, &clord_str), - (1, account_id), - (21, "2"), - (55, &symbol), - (54, side_str), - (38, &qty_str), - (40, "TSL"), // OrdType = Trailing Stop Limit - (99, &trail_str), // StopPx = trail amount - (6370, &offset_str), // LimitPriceOffset - (211, &trail_str), // PegOffset = trail amount - (59, "0"), // TIF = DAY - (60, &now), - (167, &sec_type_str), - (100, &destination), - (6210, &destination), - (15, "USD"), - (204, "0"), - ]; - if trail_stop_price > 0 { fields.push((6117, &trail_stop_str)); } - conn.send_fix(&fields) - } - OrderRequest::SubmitTrailingStopPct { order_id, instrument, side, qty, trail_pct, trail_stop_price } => { - context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, 0, b'P', b'0', 0, - )); - let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); - let clord_str = format!("{}.{}", order_id, ver); - let side_str = fix_side(side); - let qty_str = format_uint(qty as u64); - let pct_str = trail_pct.to_string(); // basis points: 100 = 1% - // Per ib-agent#156 capture: percent-trail mirrors 99/211 as the - // percent in decimal form (1.00 for 1%), alongside 6268 in basis - // points and 18=a (ExecInst=TrailingStop). Without 99/211/18 the - // gateway rejects with "Invalid value in field # 18". - let pct_decimal = format!("{:.2}", trail_pct as f64 / 100.0); - let trail_stop_str = format_price(trail_stop_price); - let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, destination) = context.market.order_routing(instrument); - let now = chrono_free_timestamp(); - let mut fields: Vec<(u32, &str)> = vec![ - (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), - (fix::TAG_SENDING_TIME, &now), - (11, &clord_str), - (1, account_id), - (21, "2"), - (55, &symbol), - (54, side_str), - (38, &qty_str), - (40, "P"), // OrdType = Trailing Stop - (99, &pct_decimal), // StopPx = percent as decimal - (211, &pct_decimal), // PegOffset = percent as decimal (mirror of 99) - (18, "a"), // ExecInst = TrailingStop - (6268, &pct_str), // TrailingPercent (basis points) - (59, "0"), // TIF = DAY - (60, &now), - (167, &sec_type_str), - (100, &destination), - (6210, &destination), - (15, "USD"), - (204, "0"), - ]; - if trail_stop_price > 0 { fields.push((6117, &trail_stop_str)); } - conn.send_fix(&fields) - } - OrderRequest::SubmitTrailingStopPctEx { order_id, instrument, side, qty, trail_pct, tif, attrs, trail_stop_price } => { - send_order_ex(conn, context, account_id, order_id, instrument, side, qty, - crate::types::OrderKind::TrailPct { trail_pct, trail_stop_price }, tif, &attrs) - } - OrderRequest::SubmitMoc { order_id, instrument, side, qty } => { - context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, 0, b'5', b'0', 0, - )); - let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); - let clord_str = format!("{}.{}", order_id, ver); - let side_str = fix_side(side); - let qty_str = format_uint(qty as u64); - let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, destination) = context.market.order_routing(instrument); - let now = chrono_free_timestamp(); - conn.send_fix(&[ - (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), - (fix::TAG_SENDING_TIME, &now), - (11, &clord_str), - (1, account_id), - (21, "2"), - (55, &symbol), - (54, side_str), - (38, &qty_str), - (40, "5"), // OrdType = Market on Close - (59, "0"), // TIF = DAY - (60, &now), - (167, &sec_type_str), - (100, &destination), - (6210, &destination), - (15, "USD"), - (204, "0"), - ]) - } - OrderRequest::SubmitLoc { order_id, instrument, side, qty, price } => { - context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, price, b'B', b'0', 0, - )); - let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); - let clord_str = format!("{}.{}", order_id, ver); - let side_str = fix_side(side); - let qty_str = format_uint(qty as u64); - let price_str = format_price(price); - let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, destination) = context.market.order_routing(instrument); - let now = chrono_free_timestamp(); - conn.send_fix(&[ - (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), - (fix::TAG_SENDING_TIME, &now), - (11, &clord_str), - (1, account_id), - (21, "2"), - (55, &symbol), - (54, side_str), - (38, &qty_str), - (40, "B"), // OrdType = Limit on Close - (44, &price_str), // Limit price - (59, "0"), // TIF = DAY - (60, &now), - (167, &sec_type_str), - (100, &destination), - (6210, &destination), - (15, "USD"), - (204, "0"), - ]) - } - OrderRequest::SubmitMit { order_id, instrument, side, qty, stop_price } => { - context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, stop_price, b'J', b'0', stop_price, - )); - let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); - let clord_str = format!("{}.{}", order_id, ver); - let side_str = fix_side(side); - let qty_str = format_uint(qty as u64); - let stop_str = format_price(stop_price); - let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, destination) = context.market.order_routing(instrument); - let now = chrono_free_timestamp(); - conn.send_fix(&[ - (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), - (fix::TAG_SENDING_TIME, &now), - (11, &clord_str), - (1, account_id), - (21, "2"), - (55, &symbol), - (54, side_str), - (38, &qty_str), - (40, "J"), // OrdType = Market if Touched - (99, &stop_str), // StopPx = trigger price - (59, "0"), // TIF = DAY - (60, &now), - (167, &sec_type_str), - (100, &destination), - (6210, &destination), - (15, "USD"), - (204, "0"), - ]) - } - OrderRequest::SubmitLit { order_id, instrument, side, qty, price, stop_price } => { - context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, price, b'K', b'0', stop_price, - )); - let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); - let clord_str = format!("{}.{}", order_id, ver); - let side_str = fix_side(side); - let qty_str = format_uint(qty as u64); - let price_str = format_price(price); - let stop_str = format_price(stop_price); - let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, destination) = context.market.order_routing(instrument); - let now = chrono_free_timestamp(); - conn.send_fix(&[ - (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), - (fix::TAG_SENDING_TIME, &now), - (11, &clord_str), - (1, account_id), - (21, "2"), - (55, &symbol), - (54, side_str), - (38, &qty_str), - (40, "LT"), // OrdType = Limit If Touched (per ib-agent#138) - (44, &price_str), // Limit price - (99, &stop_str), // StopPx = trigger price - (59, "0"), // TIF = DAY - (60, &now), - (167, &sec_type_str), - (100, &destination), - (6210, &destination), - (15, "USD"), - (204, "0"), - ]) - } - OrderRequest::SubmitBracket { parent_id, tp_id, sl_id, instrument, side, qty, entry_price, take_profit, stop_loss } => { - let exit_side = match side { Side::Buy => Side::Sell, Side::Sell | Side::ShortSell => Side::Buy }; - let exit_side_str = fix_side(exit_side); - let side_str = fix_side(side); - let qty_str = format_uint(qty as u64); - let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, destination) = context.market.order_routing(instrument); - let parent_str = parent_id.to_string(); - let tp_str = tp_id.to_string(); - let sl_str = sl_id.to_string(); - let entry_str = format_price(entry_price); - let tp_price_str = format_price(take_profit); - let sl_price_str = format_price(stop_loss); - let oca_group = format!("OCA_{}", parent_id); - - // 1. Parent order: limit entry - context.insert_order(crate::types::Order::new( - parent_id, instrument, side, qty, entry_price, b'2', b'0', 0, - )); - let now = chrono_free_timestamp(); - let _ = conn.send_fix(&[ - (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), - (fix::TAG_SENDING_TIME, &now), - (11, &parent_str), - (1, account_id), - (21, "2"), - (55, &symbol), - (54, side_str), - (38, &qty_str), - (40, "2"), // Limit - (44, &entry_str), - (59, "0"), // DAY - (60, &now), - (167, &sec_type_str), - (100, &destination), - (6210, &destination), - (15, "USD"), - (204, "0"), - ]); - - // 2. Take-profit child: limit exit, linked to parent, in OCA group - context.insert_order(crate::types::Order::new( - tp_id, instrument, exit_side, qty, take_profit, b'2', b'1', 0, - )); - let now = chrono_free_timestamp(); - let _ = conn.send_fix(&[ - (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), - (fix::TAG_SENDING_TIME, &now), - (11, &tp_str), - (1, account_id), - (21, "2"), - (55, &symbol), - (54, exit_side_str), - (38, &qty_str), - (40, "2"), // Limit - (44, &tp_price_str), - (59, "1"), // GTC - (60, &now), - (167, &sec_type_str), - (100, &destination), - (6210, &destination), - (15, "USD"), - (204, "0"), - (6107, &parent_str), // ParentOrderID - (583, &oca_group), // OCAGroup - (6209, "ReduceOnFillNonBlock"), // OCA type: gateway default 3 (ibx#215) - ]); - - // 3. Stop-loss child: stop exit, linked to parent, in OCA group - context.insert_order(crate::types::Order::new( - sl_id, instrument, exit_side, qty, stop_loss, b'3', b'1', stop_loss, - )); - let now = chrono_free_timestamp(); - conn.send_fix(&[ - (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), - (fix::TAG_SENDING_TIME, &now), - (11, &sl_str), - (1, account_id), - (21, "2"), - (55, &symbol), - (54, exit_side_str), - (38, &qty_str), - (40, "3"), // Stop - (99, &sl_price_str), - (59, "1"), // GTC - (60, &now), - (167, &sec_type_str), - (100, &destination), - (6210, &destination), - (15, "USD"), - (204, "0"), - (6107, &parent_str), // ParentOrderID - (583, &oca_group), // OCAGroup - (6209, "ReduceOnFillNonBlock"), // OCA type: gateway default 3 (ibx#215) - ]) - } - OrderRequest::SubmitRel { order_id, instrument, side, qty, offset } => { - context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, 0, b'R', b'0', offset, - )); - let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); - let clord_str = format!("{}.{}", order_id, ver); - let side_str = fix_side(side); - let qty_str = format_uint(qty as u64); - let offset_str = format_price(offset); - let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, destination) = context.market.order_routing(instrument); - let now = chrono_free_timestamp(); - // Per ib-agent#138 capture: Relative shares OrdType=P with - // Trail and is disambiguated by ExecInst=R. Peg offset goes - // on tag 211 (not 99 outbound), and there is no tag 44. - conn.send_fix(&[ - (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), - (fix::TAG_SENDING_TIME, &now), - (11, &clord_str), - (1, account_id), - (21, "2"), - (55, &symbol), - (54, side_str), - (38, &qty_str), - (40, "P"), // OrdType = Pegged (used for Relative too) - (211, &offset_str), // PegOffset - (18, "R"), // ExecInst = Relative - (59, "0"), // TIF = DAY - (60, &now), - (167, &sec_type_str), - (100, &destination), - (6210, &destination), - (15, "USD"), - (204, "0"), - ]) - } - OrderRequest::SubmitLimitOpg { order_id, instrument, side, qty, price } => { - context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, price, b'2', b'2', 0, - )); - let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); - let clord_str = format!("{}.{}", order_id, ver); - let side_str = fix_side(side); - let qty_str = format_uint(qty as u64); - let price_str = format_price(price); - let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, destination) = context.market.order_routing(instrument); - let now = chrono_free_timestamp(); - conn.send_fix(&[ - (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), - (fix::TAG_SENDING_TIME, &now), - (11, &clord_str), - (1, account_id), - (21, "2"), - (55, &symbol), - (54, side_str), - (38, &qty_str), - (40, "2"), // OrdType = Limit - (44, &price_str), - (59, "2"), // TIF = OPG (At the Opening) - (60, &now), - (167, &sec_type_str), - (100, &destination), - (6210, &destination), - (15, "USD"), - (204, "0"), - ]) - } - OrderRequest::SubmitPegBench { order_id, instrument, side, qty, price, - ref_con_id, is_peg_decrease, pegged_change_amount, ref_change_amount } => { + OrderRequest::SubmitLimitGtc { order_id, instrument, side, qty, price, outside_rth } => { context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, price, crate::types::ORD_PEG_BENCH, b'0', 0, + order_id, instrument, side, qty, price, b'2', b'1', 0, )); let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); let clord_str = format!("{}.{}", order_id, ver); @@ -788,11 +50,7 @@ pub(crate) fn drain_and_send_orders( let symbol = context.market.symbol(instrument).to_string(); let (sec_type_str, destination) = context.market.order_routing(instrument); let now = chrono_free_timestamp(); - let ref_con_str = ref_con_id.to_string(); - let peg_decrease_str = if is_peg_decrease { "1" } else { "0" }; - let peg_change_str = format_price(pegged_change_amount); - let ref_change_str = format_price(ref_change_amount); - conn.send_fix(&[ + let mut fields: Vec<(u32, &str)> = vec![ (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), (fix::TAG_SENDING_TIME, &now), (11, &clord_str), @@ -801,34 +59,38 @@ pub(crate) fn drain_and_send_orders( (55, &symbol), (54, side_str), (38, &qty_str), - (40, "PB"), // OrdType = Pegged to Benchmark - (44, &price_str), // Limit price - (59, "0"), + (40, "2"), // OrdType = Limit + (44, &price_str), + (59, "1"), // TIF = GTC (60, &now), (167, &sec_type_str), (100, &destination), (6210, &destination), (15, "USD"), (204, "0"), - (6941, &ref_con_str), // referenceContractId - (6938, peg_decrease_str), // isPeggedChangeAmountDecrease - (6939, &peg_change_str), // peggedChangeAmount - (6942, &ref_change_str), // referenceChangeAmount - ]) + ]; + if outside_rth { + fields.push((6433, "1")); // OutsideRTH + } + conn.send_fix(&fields) } - OrderRequest::SubmitLimitAuc { order_id, instrument, side, qty, price } => { + OrderRequest::SubmitEx { order_id, instrument, side, qty, kind, tif, attrs } => { + send_order_ex(conn, context, account_id, order_id, instrument, side, qty, + kind, tif, &attrs) + } + OrderRequest::SubmitStopGtc { order_id, instrument, side, qty, stop_price, outside_rth } => { context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, price, b'2', b'8', 0, + order_id, instrument, side, qty, stop_price, b'3', b'1', stop_price, )); let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); let clord_str = format!("{}.{}", order_id, ver); let side_str = fix_side(side); let qty_str = format_uint(qty as u64); - let price_str = format_price(price); + let stop_str = format_price(stop_price); let symbol = context.market.symbol(instrument).to_string(); let (sec_type_str, destination) = context.market.order_routing(instrument); let now = chrono_free_timestamp(); - conn.send_fix(&[ + let mut fields: Vec<(u32, &str)> = vec![ (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), (fix::TAG_SENDING_TIME, &now), (11, &clord_str), @@ -837,29 +99,35 @@ pub(crate) fn drain_and_send_orders( (55, &symbol), (54, side_str), (38, &qty_str), - (40, "2"), // OrdType = Limit - (44, &price_str), // Limit price - (59, "8"), // TIF = Auction + (40, "3"), // OrdType = Stop + (99, &stop_str), + (59, "1"), // TIF = GTC (60, &now), (167, &sec_type_str), (100, &destination), (6210, &destination), (15, "USD"), (204, "0"), - ]) + ]; + if outside_rth { + fields.push((6433, "1")); + } + conn.send_fix(&fields) } - OrderRequest::SubmitMtlAuc { order_id, instrument, side, qty } => { + OrderRequest::SubmitStopLimitGtc { order_id, instrument, side, qty, price, stop_price, outside_rth } => { context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, 0, b'K', b'8', 0, + order_id, instrument, side, qty, price, b'4', b'1', stop_price, )); let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); let clord_str = format!("{}.{}", order_id, ver); let side_str = fix_side(side); let qty_str = format_uint(qty as u64); + let price_str = format_price(price); + let stop_str = format_price(stop_price); let symbol = context.market.symbol(instrument).to_string(); let (sec_type_str, destination) = context.market.order_routing(instrument); let now = chrono_free_timestamp(); - conn.send_fix(&[ + let mut fields: Vec<(u32, &str)> = vec![ (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), (fix::TAG_SENDING_TIME, &now), (11, &clord_str), @@ -868,24 +136,30 @@ pub(crate) fn drain_and_send_orders( (55, &symbol), (54, side_str), (38, &qty_str), - (40, "K"), // OrdType = Market to Limit - (59, "8"), // TIF = Auction + (40, "4"), // OrdType = Stop Limit + (44, &price_str), + (99, &stop_str), + (59, "1"), // TIF = GTC (60, &now), (167, &sec_type_str), (100, &destination), (6210, &destination), (15, "USD"), (204, "0"), - ]) + ]; + if outside_rth { + fields.push((6433, "1")); + } + conn.send_fix(&fields) } - OrderRequest::SubmitLimitFractional { order_id, instrument, side, qty, price } => { + OrderRequest::SubmitLimitIoc { order_id, instrument, side, qty, price } => { context.insert_order(crate::types::Order::new( - order_id, instrument, side, 0, price, b'2', b'0', 0, + order_id, instrument, side, qty, price, b'2', b'3', 0, )); let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); let clord_str = format!("{}.{}", order_id, ver); let side_str = fix_side(side); - let qty_str = format_qty(qty); + let qty_str = format_uint(qty as u64); let price_str = format_price(price); let symbol = context.market.symbol(instrument).to_string(); let (sec_type_str, destination) = context.market.order_routing(instrument); @@ -898,10 +172,10 @@ pub(crate) fn drain_and_send_orders( (21, "2"), (55, &symbol), (54, side_str), - (38, &qty_str), // Decimal qty (e.g., "0.5") - (40, "2"), // OrdType = Limit + (38, &qty_str), + (40, "2"), // OrdType = Limit (44, &price_str), - (59, "0"), + (59, "3"), // TIF = IOC (60, &now), (167, &sec_type_str), (100, &destination), @@ -910,14 +184,15 @@ pub(crate) fn drain_and_send_orders( (204, "0"), ]) } - OrderRequest::SubmitMtl { order_id, instrument, side, qty } => { + OrderRequest::SubmitLimitFok { order_id, instrument, side, qty, price } => { context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, 0, b'K', b'0', 0, + order_id, instrument, side, qty, price, b'2', b'4', 0, )); let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); let clord_str = format!("{}.{}", order_id, ver); let side_str = fix_side(side); let qty_str = format_uint(qty as u64); + let price_str = format_price(price); let symbol = context.market.symbol(instrument).to_string(); let (sec_type_str, destination) = context.market.order_routing(instrument); let now = chrono_free_timestamp(); @@ -930,8 +205,9 @@ pub(crate) fn drain_and_send_orders( (55, &symbol), (54, side_str), (38, &qty_str), - (40, "K"), // OrdType = Market to Limit - (59, "0"), + (40, "2"), // OrdType = Limit + (44, &price_str), + (59, "4"), // TIF = FOK (60, &now), (167, &sec_type_str), (100, &destination), @@ -940,114 +216,113 @@ pub(crate) fn drain_and_send_orders( (204, "0"), ]) } - OrderRequest::SubmitMktPrt { order_id, instrument, side, qty } => { - context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, 0, b'U', b'0', 0, - )); - let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); - let clord_str = format!("{}.{}", order_id, ver); + OrderRequest::SubmitBracket { parent_id, tp_id, sl_id, instrument, side, qty, entry_price, take_profit, stop_loss } => { + let exit_side = match side { Side::Buy => Side::Sell, Side::Sell | Side::ShortSell => Side::Buy }; + let exit_side_str = fix_side(exit_side); let side_str = fix_side(side); let qty_str = format_uint(qty as u64); let symbol = context.market.symbol(instrument).to_string(); let (sec_type_str, destination) = context.market.order_routing(instrument); + let parent_str = parent_id.to_string(); + let tp_str = tp_id.to_string(); + let sl_str = sl_id.to_string(); + let entry_str = format_price(entry_price); + let tp_price_str = format_price(take_profit); + let sl_price_str = format_price(stop_loss); + let oca_group = format!("OCA_{}", parent_id); + + // 1. Parent order: limit entry + context.insert_order(crate::types::Order::new( + parent_id, instrument, side, qty, entry_price, b'2', b'0', 0, + )); let now = chrono_free_timestamp(); - conn.send_fix(&[ + let _ = conn.send_fix(&[ (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), (fix::TAG_SENDING_TIME, &now), - (11, &clord_str), + (11, &parent_str), (1, account_id), (21, "2"), (55, &symbol), (54, side_str), (38, &qty_str), - (40, "U"), // OrdType = Market with Protection - (59, "0"), + (40, "2"), // Limit + (44, &entry_str), + (59, "0"), // DAY (60, &now), (167, &sec_type_str), (100, &destination), (6210, &destination), (15, "USD"), (204, "0"), - ]) - } - OrderRequest::SubmitStpPrt { order_id, instrument, side, qty, stop_price } => { + ]); + + // 2. Take-profit child: limit exit, linked to parent, in OCA group context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, 0, crate::types::ORD_STP_PRT, b'0', stop_price, + tp_id, instrument, exit_side, qty, take_profit, b'2', b'1', 0, )); - let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); - let clord_str = format!("{}.{}", order_id, ver); - let side_str = fix_side(side); - let qty_str = format_uint(qty as u64); - let stop_str = format_price(stop_price); - let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, destination) = context.market.order_routing(instrument); let now = chrono_free_timestamp(); - conn.send_fix(&[ + let _ = conn.send_fix(&[ (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), (fix::TAG_SENDING_TIME, &now), - (11, &clord_str), + (11, &tp_str), (1, account_id), (21, "2"), (55, &symbol), - (54, side_str), + (54, exit_side_str), (38, &qty_str), - (40, "SP"), // OrdType = Stop with Protection - (99, &stop_str), // StopPx - (59, "0"), + (40, "2"), // Limit + (44, &tp_price_str), + (59, "1"), // GTC (60, &now), (167, &sec_type_str), (100, &destination), (6210, &destination), (15, "USD"), (204, "0"), - ]) - } - OrderRequest::SubmitMidPrice { order_id, instrument, side, qty, price_cap } => { + (6107, &parent_str), // ParentOrderID + (583, &oca_group), // OCAGroup + (6209, "ReduceOnFillNonBlock"), // OCA type: gateway default 3 (ibx#215) + ]); + + // 3. Stop-loss child: stop exit, linked to parent, in OCA group context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, price_cap, crate::types::ORD_MIDPX, b'0', 0, + sl_id, instrument, exit_side, qty, stop_loss, b'3', b'1', stop_loss, )); - let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); - let clord_str = format!("{}.{}", order_id, ver); - let side_str = fix_side(side); - let qty_str = format_uint(qty as u64); - let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, _destination) = context.market.order_routing(instrument); let now = chrono_free_timestamp(); - let mut fields: Vec<(u32, &str)> = vec![ + conn.send_fix(&[ (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), (fix::TAG_SENDING_TIME, &now), - (11, &clord_str), + (11, &sl_str), (1, account_id), (21, "2"), (55, &symbol), - (54, side_str), + (54, exit_side_str), (38, &qty_str), - (40, "MIDPX"), // OrdType = Mid-Price - (59, "0"), + (40, "3"), // Stop + (99, &sl_price_str), + (59, "1"), // GTC (60, &now), (167, &sec_type_str), - (100, "ISLAND"), // Requires directed exchange - (6210, "ISLAND"), + (100, &destination), + (6210, &destination), (15, "USD"), (204, "0"), - ]; - let cap_str; - if price_cap > 0 { - cap_str = format_price(price_cap); - fields.push((44, &cap_str)); // Price cap - } - conn.send_fix(&fields) + (6107, &parent_str), // ParentOrderID + (583, &oca_group), // OCAGroup + (6209, "ReduceOnFillNonBlock"), // OCA type: gateway default 3 (ibx#215) + ]) } - OrderRequest::SubmitSnapMkt { order_id, instrument, side, qty } => { + OrderRequest::SubmitLimitOpg { order_id, instrument, side, qty, price } => { context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, 0, crate::types::ORD_SNAP_MKT, b'0', 0, + order_id, instrument, side, qty, price, b'2', b'2', 0, )); let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); let clord_str = format!("{}.{}", order_id, ver); let side_str = fix_side(side); let qty_str = format_uint(qty as u64); + let price_str = format_price(price); let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, _destination) = context.market.order_routing(instrument); + let (sec_type_str, destination) = context.market.order_routing(instrument); let now = chrono_free_timestamp(); conn.send_fix(&[ (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), @@ -1058,27 +333,34 @@ pub(crate) fn drain_and_send_orders( (55, &symbol), (54, side_str), (38, &qty_str), - (40, "SMKT"), // OrdType = Snap to Market - (59, "0"), + (40, "2"), // OrdType = Limit + (44, &price_str), + (59, "2"), // TIF = OPG (At the Opening) (60, &now), (167, &sec_type_str), - (100, "ISLAND"), // Requires directed exchange - (6210, "ISLAND"), + (100, &destination), + (6210, &destination), (15, "USD"), (204, "0"), ]) } - OrderRequest::SubmitSnapMid { order_id, instrument, side, qty } => { + OrderRequest::SubmitPegBench { order_id, instrument, side, qty, price, + ref_con_id, is_peg_decrease, pegged_change_amount, ref_change_amount } => { context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, 0, crate::types::ORD_SNAP_MID, b'0', 0, + order_id, instrument, side, qty, price, crate::types::ORD_PEG_BENCH, b'0', 0, )); let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); let clord_str = format!("{}.{}", order_id, ver); let side_str = fix_side(side); let qty_str = format_uint(qty as u64); + let price_str = format_price(price); let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, _destination) = context.market.order_routing(instrument); + let (sec_type_str, destination) = context.market.order_routing(instrument); let now = chrono_free_timestamp(); + let ref_con_str = ref_con_id.to_string(); + let peg_decrease_str = if is_peg_decrease { "1" } else { "0" }; + let peg_change_str = format_price(pegged_change_amount); + let ref_change_str = format_price(ref_change_amount); conn.send_fix(&[ (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), (fix::TAG_SENDING_TIME, &now), @@ -1088,26 +370,32 @@ pub(crate) fn drain_and_send_orders( (55, &symbol), (54, side_str), (38, &qty_str), - (40, "SMID"), // OrdType = Snap to Midpoint + (40, "PB"), // OrdType = Pegged to Benchmark + (44, &price_str), // Limit price (59, "0"), (60, &now), (167, &sec_type_str), - (100, "ISLAND"), // Requires directed exchange - (6210, "ISLAND"), + (100, &destination), + (6210, &destination), (15, "USD"), (204, "0"), + (6941, &ref_con_str), // referenceContractId + (6938, peg_decrease_str), // isPeggedChangeAmountDecrease + (6939, &peg_change_str), // peggedChangeAmount + (6942, &ref_change_str), // referenceChangeAmount ]) } - OrderRequest::SubmitSnapPri { order_id, instrument, side, qty } => { + OrderRequest::SubmitLimitAuc { order_id, instrument, side, qty, price } => { context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, 0, crate::types::ORD_SNAP_PRI, b'0', 0, + order_id, instrument, side, qty, price, b'2', b'8', 0, )); let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); let clord_str = format!("{}.{}", order_id, ver); let side_str = fix_side(side); let qty_str = format_uint(qty as u64); + let price_str = format_price(price); let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, _destination) = context.market.order_routing(instrument); + let (sec_type_str, destination) = context.market.order_routing(instrument); let now = chrono_free_timestamp(); conn.send_fix(&[ (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), @@ -1118,28 +406,29 @@ pub(crate) fn drain_and_send_orders( (55, &symbol), (54, side_str), (38, &qty_str), - (40, "SREL"), // OrdType = Snap to Primary - (59, "0"), + (40, "2"), // OrdType = Limit + (44, &price_str), // Limit price + (59, "8"), // TIF = Auction (60, &now), (167, &sec_type_str), - (100, "ISLAND"), // Requires directed exchange - (6210, "ISLAND"), + (100, &destination), + (6210, &destination), (15, "USD"), (204, "0"), ]) } - OrderRequest::SubmitPegMkt { order_id, instrument, side, qty, offset } => { + OrderRequest::SubmitMtlAuc { order_id, instrument, side, qty } => { context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, 0, crate::types::ORD_PEG_MKT, b'0', offset, + order_id, instrument, side, qty, 0, b'K', b'8', 0, )); let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); let clord_str = format!("{}.{}", order_id, ver); let side_str = fix_side(side); let qty_str = format_uint(qty as u64); let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, _destination) = context.market.order_routing(instrument); + let (sec_type_str, destination) = context.market.order_routing(instrument); let now = chrono_free_timestamp(); - let mut fields: Vec<(u32, &str)> = vec![ + conn.send_fix(&[ (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), (fix::TAG_SENDING_TIME, &now), (11, &clord_str), @@ -1148,34 +437,29 @@ pub(crate) fn drain_and_send_orders( (55, &symbol), (54, side_str), (38, &qty_str), - (40, "E"), // OrdType = Pegged (no mid-offset tags = PEGMKT) - (59, "0"), + (40, "K"), // OrdType = Market to Limit + (59, "8"), // TIF = Auction (60, &now), (167, &sec_type_str), - (100, "ISLAND"), // Requires directed exchange - (6210, "ISLAND"), + (100, &destination), + (6210, &destination), (15, "USD"), (204, "0"), - ]; - let offset_str; - if offset > 0 { - offset_str = format_price(offset); - fields.push((211, &offset_str)); // PegOffsetValue - } - conn.send_fix(&fields) + ]) } - OrderRequest::SubmitPegMid { order_id, instrument, side, qty, offset } => { + OrderRequest::SubmitLimitFractional { order_id, instrument, side, qty, price } => { context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, 0, crate::types::ORD_PEG_MID, b'0', offset, + order_id, instrument, side, 0, price, b'2', b'0', 0, )); let ver = *context.modify_versions.get(&order_id).unwrap_or(&0); let clord_str = format!("{}.{}", order_id, ver); let side_str = fix_side(side); - let qty_str = format_uint(qty as u64); + let qty_str = format_qty(qty); + let price_str = format_price(price); let symbol = context.market.symbol(instrument).to_string(); - let (sec_type_str, _destination) = context.market.order_routing(instrument); + let (sec_type_str, destination) = context.market.order_routing(instrument); let now = chrono_free_timestamp(); - let mut fields: Vec<(u32, &str)> = vec![ + conn.send_fix(&[ (fix::TAG_MSG_TYPE, fix::MSG_NEW_ORDER), (fix::TAG_SENDING_TIME, &now), (11, &clord_str), @@ -1183,24 +467,17 @@ pub(crate) fn drain_and_send_orders( (21, "2"), (55, &symbol), (54, side_str), - (38, &qty_str), - (40, "E"), // OrdType = Pegged (tags 8403/8404 = PEGMID) + (38, &qty_str), // Decimal qty (e.g., "0.5") + (40, "2"), // OrdType = Limit + (44, &price_str), (59, "0"), (60, &now), (167, &sec_type_str), - (100, "ISLAND"), // Requires directed exchange - (6210, "ISLAND"), + (100, &destination), + (6210, &destination), (15, "USD"), (204, "0"), - (8403, "0.0"), // midOffsetAtWhole — differentiates PEGMID from PEGMKT - (8404, "0.0"), // midOffsetAtHalf - ]; - let offset_str; - if offset > 0 { - offset_str = format_price(offset); - fields.push((211, &offset_str)); // PegOffsetValue - } - conn.send_fix(&fields) + ]) } OrderRequest::Cancel { order_id } => { // OrigClOrdID must match exactly what the server has on record. @@ -1961,100 +1238,6 @@ mod tests { assert!(shared.orders.drain_order_updates().is_empty()); } - /// Encode one request and return the frame with the parts that cannot be - /// equal between two sends removed: sequence number, both timestamps, and - /// the body length and checksum that cover them. - fn encode_for_test(req: crate::types::OrderRequest) -> String { - use std::io::Read; - let (conn, mut peer) = crate::protocol::connection::Connection::for_test(); - let mut conn = Some(conn); - let mut context = Context::new(); - context.register_instrument(756733); - context.set_symbol(0, "SPY".to_string()); - context.pending_orders.push(req); - let mut hb = crate::engine::hot_loop::HeartbeatState::new(); - let shared = std::sync::Arc::new(SharedState::new()); - drain_and_send_orders(&mut conn, &mut context, "DU123456", &mut hb, false, &shared); - - let mut buf = [0u8; 8192]; - let n = peer.read(&mut buf).unwrap(); - String::from_utf8_lossy(&buf[..n]) - .split('\u{1}') - .filter(|f| !f.is_empty()) - .filter(|f| !["9=", "10=", "34=", "52=", "60="].iter().any(|t| f.starts_with(t))) - .collect::>() - .join("|") - } - - /// Every order type is encoded twice: once by its own request variant and - /// once through the shared encoder carrying the same order with no extended - /// attributes. The two must be the same message. - /// - /// That equality is what makes the per-type variants redundant. While both - /// exist, it is also what stops them drifting — a tag added to one encoder - /// and not the other is exactly how an order type ends up shipping without - /// something the caller set, which is #240 and #318. - #[test] - fn the_shared_encoder_restates_every_type_exactly_as_its_own_variant_does() { - use crate::types::{OrderKind as K, OrderRequest as R, PRICE_SCALE}; - let (id, inst, side, qty) = (7u64, 0u32, Side::Buy, 1u32); - let px = 100 * PRICE_SCALE; - let stop = 90 * PRICE_SCALE; - let off = PRICE_SCALE / 2; - - let cases: Vec<(&str, R, K)> = vec![ - ("MKT", R::SubmitMarket { order_id: id, instrument: inst, side, qty }, K::Market), - ("LMT", R::SubmitLimit { order_id: id, instrument: inst, side, qty, price: px }, - K::Limit { price: px }), - ("STP", R::SubmitStop { order_id: id, instrument: inst, side, qty, stop_price: stop }, - K::Stop { stop_price: stop }), - ("STP LMT", R::SubmitStopLimit { order_id: id, instrument: inst, side, qty, price: px, stop_price: stop }, - K::StopLimit { price: px, stop_price: stop }), - ("MOC", R::SubmitMoc { order_id: id, instrument: inst, side, qty }, K::Moc), - ("LOC", R::SubmitLoc { order_id: id, instrument: inst, side, qty, price: px }, - K::Loc { price: px }), - ("MIT", R::SubmitMit { order_id: id, instrument: inst, side, qty, stop_price: stop }, - K::Mit { stop_price: stop }), - ("LIT", R::SubmitLit { order_id: id, instrument: inst, side, qty, price: px, stop_price: stop }, - K::Lit { price: px, stop_price: stop }), - ("MTL", R::SubmitMtl { order_id: id, instrument: inst, side, qty }, K::Mtl), - ("MKT PRT", R::SubmitMktPrt { order_id: id, instrument: inst, side, qty }, K::MktPrt), - ("STP PRT", R::SubmitStpPrt { order_id: id, instrument: inst, side, qty, stop_price: stop }, - K::StpPrt { stop_price: stop }), - ("MIDPX", R::SubmitMidPrice { order_id: id, instrument: inst, side, qty, price_cap: px }, - K::MidPrice { price_cap: px }), - ("SNAP MKT", R::SubmitSnapMkt { order_id: id, instrument: inst, side, qty }, K::SnapMkt), - ("SNAP MID", R::SubmitSnapMid { order_id: id, instrument: inst, side, qty }, K::SnapMid), - ("SNAP PRI", R::SubmitSnapPri { order_id: id, instrument: inst, side, qty }, K::SnapPri), - ("PEG MKT", R::SubmitPegMkt { order_id: id, instrument: inst, side, qty, offset: off }, - K::PegMkt { offset: off }), - ("PEG MID", R::SubmitPegMid { order_id: id, instrument: inst, side, qty, offset: off }, - K::PegMid { offset: off }), - ("REL", R::SubmitRel { order_id: id, instrument: inst, side, qty, offset: off }, - K::Rel { offset: off }), - ("TRAIL", R::SubmitTrailingStop { order_id: id, instrument: inst, side, qty, trail_amt: off, trail_stop_price: stop }, - K::TrailingStop { trail_amt: off, trail_stop_price: stop }), - ("TRAIL LIMIT", R::SubmitTrailingStopLimit { order_id: id, instrument: inst, side, qty, lmt_offset: off, trail_amt: off, trail_stop_price: stop }, - K::TrailingStopLimit { lmt_offset: off, trail_amt: off, trail_stop_price: stop }), - ("TRAIL PCT", R::SubmitTrailingStopPct { order_id: id, instrument: inst, side, qty, trail_pct: 100, trail_stop_price: stop }, - K::TrailPct { trail_pct: 100, trail_stop_price: stop }), - ]; - - let mut differences = Vec::new(); - for (name, plain, kind) in cases { - let own = encode_for_test(plain); - let shared = encode_for_test(R::SubmitEx { - order_id: id, instrument: inst, side, qty, kind, - tif: b'0', attrs: crate::types::OrderAttrs::default(), - }); - if own != shared { - differences.push(format!("{name}\n own: {own}\n shrd: {shared}")); - } - } - assert!(differences.is_empty(), - "{} type(s) encoded differently:\n{}", differences.len(), differences.join("\n")); - } - /// ibx#318: adaptive, algo and what-if orders returned early into their own /// encoders, which carried no attribute block at all — so outside-RTH, the /// parent link and the OCA group were accepted by the API and silently diff --git a/src/types.rs b/src/types.rs index e5466866..f976a9c6 100644 --- a/src/types.rs +++ b/src/types.rs @@ -539,34 +539,6 @@ pub enum OrderKind { /// Order request sent via control channel, processed by engine. #[derive(Debug, Clone)] pub enum OrderRequest { - SubmitLimit { - order_id: OrderId, - instrument: InstrumentId, - side: Side, - qty: u32, - price: Price, - }, - SubmitMarket { - order_id: OrderId, - instrument: InstrumentId, - side: Side, - qty: u32, - }, - SubmitStop { - order_id: OrderId, - instrument: InstrumentId, - side: Side, - qty: u32, - stop_price: Price, - }, - SubmitStopLimit { - order_id: OrderId, - instrument: InstrumentId, - side: Side, - qty: u32, - price: Price, - stop_price: Price, - }, SubmitLimitGtc { order_id: OrderId, instrument: InstrumentId, @@ -606,76 +578,6 @@ pub enum OrderRequest { qty: u32, price: Price, }, - SubmitTrailingStop { - order_id: OrderId, - instrument: InstrumentId, - side: Side, - qty: u32, - trail_amt: Price, - /// Optional initial stop trigger (tag 6117); 0 = not set. - trail_stop_price: Price, - }, - SubmitTrailingStopLimit { - order_id: OrderId, - instrument: InstrumentId, - side: Side, - qty: u32, - /// Limit offset from the trail-stop price (wire tag 6370 LimitPriceOffset). - /// The gateway derives the absolute limit price; do not pass an absolute price here. - lmt_offset: Price, - trail_amt: Price, - /// Optional initial stop trigger (tag 6117); 0 = not set. - trail_stop_price: Price, - }, - /// Trailing stop by percentage (tag 6268). Trail percent is in basis points (1% = 100). - SubmitTrailingStopPct { - order_id: OrderId, - instrument: InstrumentId, - side: Side, - qty: u32, - trail_pct: u32, // basis points: 100 = 1%, 250 = 2.5% - /// Optional initial stop trigger (tag 6117); 0 = not set. - trail_stop_price: Price, - }, - SubmitTrailingStopPctEx { - order_id: OrderId, - instrument: InstrumentId, - side: Side, - qty: u32, - trail_pct: u32, - tif: u8, - attrs: OrderAttrs, - /// Optional initial stop trigger (tag 6117); 0 = not set. - trail_stop_price: Price, - }, - SubmitMoc { - order_id: OrderId, - instrument: InstrumentId, - side: Side, - qty: u32, - }, - SubmitLoc { - order_id: OrderId, - instrument: InstrumentId, - side: Side, - qty: u32, - price: Price, - }, - SubmitMit { - order_id: OrderId, - instrument: InstrumentId, - side: Side, - qty: u32, - stop_price: Price, - }, - SubmitLit { - order_id: OrderId, - instrument: InstrumentId, - side: Side, - qty: u32, - price: Price, - stop_price: Price, - }, /// Bracket order: parent entry + take-profit + stop-loss, linked via OCA. /// Generates 3 FIX messages: parent (35=D), TP child (35=D with 6107+583), SL child (35=D with 6107+583). SubmitBracket { @@ -689,16 +591,6 @@ pub enum OrderRequest { take_profit: Price, stop_loss: Price, }, - /// Extended limit order with optional attributes (display size, hidden, GAT, GTD). - SubmitLimitEx { - order_id: OrderId, - instrument: InstrumentId, - side: Side, - qty: u32, - price: Price, - tif: u8, - attrs: OrderAttrs, - }, /// Extended submission for any order type: `kind` selects the order type /// and its prices, paired with a TIF and the full `OrderAttrs` block. /// This is how non-LMT types carry parent_id/oca_group/outside_rth/tif @@ -712,14 +604,6 @@ pub enum OrderRequest { tif: u8, attrs: OrderAttrs, }, - /// Relative / Pegged-to-Primary order: pegs to NBBO with optional offset. - SubmitRel { - order_id: OrderId, - instrument: InstrumentId, - side: Side, - qty: u32, - offset: Price, // peg offset in tag 99 - }, /// Limit order for opening auction (TIF=OPG). SubmitLimitOpg { order_id: OrderId, @@ -728,74 +612,6 @@ pub enum OrderRequest { qty: u32, price: Price, }, - /// Adaptive algo limit order: LMT with IB Adaptive algorithm overlay. - /// Market to Limit: fills at market, remainder converts to limit at fill price. OrdType K. - SubmitMtl { - order_id: OrderId, - instrument: InstrumentId, - side: Side, - qty: u32, - }, - /// Market with Protection: market order with price protection for futures. OrdType U. - SubmitMktPrt { - order_id: OrderId, - instrument: InstrumentId, - side: Side, - qty: u32, - }, - /// Stop with Protection: stop order with price protection. OrdType SP. - SubmitStpPrt { - order_id: OrderId, - instrument: InstrumentId, - side: Side, - qty: u32, - stop_price: Price, - }, - /// Mid-Price: pegs to midpoint with optional price cap. OrdType MIDPX. - SubmitMidPrice { - order_id: OrderId, - instrument: InstrumentId, - side: Side, - qty: u32, - price_cap: Price, // 0 = no cap - }, - /// Snap to Market: snaps to market price. OrdType SMKT. - SubmitSnapMkt { - order_id: OrderId, - instrument: InstrumentId, - side: Side, - qty: u32, - }, - /// Snap to Midpoint: snaps to midpoint. OrdType SMID. - SubmitSnapMid { - order_id: OrderId, - instrument: InstrumentId, - side: Side, - qty: u32, - }, - /// Snap to Primary: snaps to primary (NBBO). OrdType SREL. - SubmitSnapPri { - order_id: OrderId, - instrument: InstrumentId, - side: Side, - qty: u32, - }, - /// Pegged to Market: pegs to market with optional offset. OrdType E + ExecInst P. - SubmitPegMkt { - order_id: OrderId, - instrument: InstrumentId, - side: Side, - qty: u32, - offset: Price, // peg offset, 0 = no offset - }, - /// Pegged to Midpoint: pegs to midpoint with optional offset. OrdType E + ExecInst M. - SubmitPegMid { - order_id: OrderId, - instrument: InstrumentId, - side: Side, - qty: u32, - offset: Price, // peg offset, 0 = no offset - }, /// Algorithmic order: limit order with IB algo strategy overlay (VWAP, TWAP, etc.). /// Pegged to Benchmark: pegs to a benchmark instrument's price. OrdType PB. /// Companion tags: 6941=refConId, 6938=isPegDecrease, 6939=pegChangeAmt, 6942=refChangeAmt. @@ -857,35 +673,12 @@ impl OrderRequest { Self::Cancel { order_id } => *order_id, Self::CancelAll { .. } => 0, Self::Modify { order_id, .. } => *order_id, - Self::SubmitLimit { order_id, .. } - | Self::SubmitMarket { order_id, .. } - | Self::SubmitStop { order_id, .. } - | Self::SubmitStopLimit { order_id, .. } | Self::SubmitLimitGtc { order_id, .. } | Self::SubmitStopGtc { order_id, .. } | Self::SubmitStopLimitGtc { order_id, .. } | Self::SubmitLimitIoc { order_id, .. } | Self::SubmitLimitFok { order_id, .. } - | Self::SubmitTrailingStop { order_id, .. } - | Self::SubmitTrailingStopLimit { order_id, .. } - | Self::SubmitTrailingStopPct { order_id, .. } - | Self::SubmitTrailingStopPctEx { order_id, .. } - | Self::SubmitMoc { order_id, .. } - | Self::SubmitLoc { order_id, .. } - | Self::SubmitMit { order_id, .. } - | Self::SubmitLit { order_id, .. } - | Self::SubmitLimitEx { order_id, .. } - | Self::SubmitRel { order_id, .. } | Self::SubmitLimitOpg { order_id, .. } - | Self::SubmitMtl { order_id, .. } - | Self::SubmitMktPrt { order_id, .. } - | Self::SubmitStpPrt { order_id, .. } - | Self::SubmitMidPrice { order_id, .. } - | Self::SubmitSnapMkt { order_id, .. } - | Self::SubmitSnapMid { order_id, .. } - | Self::SubmitSnapPri { order_id, .. } - | Self::SubmitPegMkt { order_id, .. } - | Self::SubmitPegMid { order_id, .. } | Self::SubmitPegBench { order_id, .. } | Self::SubmitLimitAuc { order_id, .. } | Self::SubmitMtlAuc { order_id, .. } @@ -902,35 +695,12 @@ impl OrderRequest { match self { Self::Cancel { .. } | Self::Modify { .. } => None, Self::CancelAll { instrument } - | Self::SubmitLimit { instrument, .. } - | Self::SubmitMarket { instrument, .. } - | Self::SubmitStop { instrument, .. } - | Self::SubmitStopLimit { instrument, .. } | Self::SubmitLimitGtc { instrument, .. } | Self::SubmitStopGtc { instrument, .. } | Self::SubmitStopLimitGtc { instrument, .. } | Self::SubmitLimitIoc { instrument, .. } | Self::SubmitLimitFok { instrument, .. } - | Self::SubmitTrailingStop { instrument, .. } - | Self::SubmitTrailingStopLimit { instrument, .. } - | Self::SubmitTrailingStopPct { instrument, .. } - | Self::SubmitTrailingStopPctEx { instrument, .. } - | Self::SubmitMoc { instrument, .. } - | Self::SubmitLoc { instrument, .. } - | Self::SubmitMit { instrument, .. } - | Self::SubmitLit { instrument, .. } - | Self::SubmitLimitEx { instrument, .. } - | Self::SubmitRel { instrument, .. } | Self::SubmitLimitOpg { instrument, .. } - | Self::SubmitMtl { instrument, .. } - | Self::SubmitMktPrt { instrument, .. } - | Self::SubmitStpPrt { instrument, .. } - | Self::SubmitMidPrice { instrument, .. } - | Self::SubmitSnapMkt { instrument, .. } - | Self::SubmitSnapMid { instrument, .. } - | Self::SubmitSnapPri { instrument, .. } - | Self::SubmitPegMkt { instrument, .. } - | Self::SubmitPegMid { instrument, .. } | Self::SubmitPegBench { instrument, .. } | Self::SubmitLimitAuc { instrument, .. } | Self::SubmitMtlAuc { instrument, .. } @@ -952,36 +722,16 @@ impl OrderRequest { } let s = |p: &mut Price| *p = snap_to_tick(*p, tick); match self { - Self::Cancel { .. } | Self::CancelAll { .. } - | Self::SubmitMarket { .. } | Self::SubmitMoc { .. } - | Self::SubmitMtl { .. } | Self::SubmitMktPrt { .. } - | Self::SubmitSnapMkt { .. } | Self::SubmitSnapMid { .. } - | Self::SubmitSnapPri { .. } | Self::SubmitMtlAuc { .. } => {} + Self::Cancel { .. } | Self::CancelAll { .. } | Self::SubmitMtlAuc { .. } => {} Self::Modify { price, .. } => s(price), - Self::SubmitLimit { price, .. } - | Self::SubmitLimitGtc { price, .. } + Self::SubmitLimitGtc { price, .. } | Self::SubmitLimitIoc { price, .. } | Self::SubmitLimitFok { price, .. } - | Self::SubmitLimitEx { price, .. } | Self::SubmitLimitOpg { price, .. } | Self::SubmitLimitAuc { price, .. } - | Self::SubmitLimitFractional { price, .. } - | Self::SubmitLoc { price, .. } => s(price), - Self::SubmitStop { stop_price, .. } - | Self::SubmitStopGtc { stop_price, .. } - | Self::SubmitMit { stop_price, .. } - | Self::SubmitStpPrt { stop_price, .. } => s(stop_price), - Self::SubmitStopLimit { price, stop_price, .. } - | Self::SubmitStopLimitGtc { price, stop_price, .. } - | Self::SubmitLit { price, stop_price, .. } => { s(price); s(stop_price); } - Self::SubmitTrailingStop { trail_amt, trail_stop_price, .. } => { s(trail_amt); s(trail_stop_price); } - Self::SubmitTrailingStopLimit { lmt_offset, trail_amt, trail_stop_price, .. } => { s(lmt_offset); s(trail_amt); s(trail_stop_price); } - Self::SubmitTrailingStopPct { trail_stop_price, .. } - | Self::SubmitTrailingStopPctEx { trail_stop_price, .. } => s(trail_stop_price), - Self::SubmitMidPrice { price_cap, .. } => s(price_cap), - Self::SubmitRel { offset, .. } - | Self::SubmitPegMkt { offset, .. } - | Self::SubmitPegMid { offset, .. } => s(offset), + | Self::SubmitLimitFractional { price, .. } => s(price), + Self::SubmitStopGtc { stop_price, .. } => s(stop_price), + Self::SubmitStopLimitGtc { price, stop_price, .. } => { s(price); s(stop_price); } Self::SubmitBracket { entry_price, take_profit, stop_loss, .. } => { s(entry_price); s(take_profit); s(stop_loss); } @@ -1542,12 +1292,10 @@ mod tests { #[test] fn order_buffer_push_and_drain() { let mut buf = OrderBuffer::new(); - buf.push(OrderRequest::SubmitLimit { - order_id: 1, - instrument: 0, - side: Side::Buy, - qty: 100, - price: 150 * PRICE_SCALE, + buf.push(OrderRequest::SubmitEx { + order_id: 1, instrument: 0, side: Side::Buy, qty: 100, + kind: OrderKind::Limit { price: 150 * PRICE_SCALE }, + tif: b'0', attrs: OrderAttrs::default(), }); buf.push(OrderRequest::Cancel { order_id: 42 }); assert!(!buf.is_empty()); @@ -1571,11 +1319,10 @@ mod tests { #[test] fn order_buffer_drain_reusable() { let mut buf = OrderBuffer::new(); - buf.push(OrderRequest::SubmitMarket { - order_id: 1, - instrument: 0, - side: Side::Sell, - qty: 50, + buf.push(OrderRequest::SubmitEx { + order_id: 1, instrument: 0, side: Side::Sell, qty: 50, + kind: OrderKind::Market, + tif: b'0', attrs: OrderAttrs::default(), }); let _: Vec<_> = buf.drain().collect(); assert!(buf.is_empty()); @@ -1731,26 +1478,6 @@ mod tests { // --- All OrderRequest variants --- - #[test] - fn order_request_submit_limit_fields() { - let req = OrderRequest::SubmitLimit { - order_id: 1, - instrument: 42, - side: Side::Buy, - qty: 100, - price: 150 * PRICE_SCALE, - }; - match req { - OrderRequest::SubmitLimit { instrument, side, qty, price, .. } => { - assert_eq!(instrument, 42); - assert_eq!(side, Side::Buy); - assert_eq!(qty, 100); - assert_eq!(price, 150 * PRICE_SCALE); - } - _ => panic!("wrong variant"), - } - } - // ── ibx#216: snap-to-tick ── const TICK_CENT: i64 = PRICE_SCALE / 100; // 0.01 @@ -1780,13 +1507,14 @@ mod tests { #[test] fn snap_prices_limit_and_stop_fields() { - let mut req = OrderRequest::SubmitStopLimit { + let mut req = OrderRequest::SubmitEx { order_id: 1, instrument: 0, side: Side::Buy, qty: 1, - price: 15_012_345_678, stop_price: 15_099_999_999, + kind: OrderKind::StopLimit { price: 15_012_345_678, stop_price: 15_099_999_999 }, + tif: b'0', attrs: OrderAttrs::default(), }; req.snap_prices(TICK_CENT); match req { - OrderRequest::SubmitStopLimit { price, stop_price, .. } => { + OrderRequest::SubmitEx { kind: OrderKind::StopLimit { price, stop_price }, .. } => { assert_eq!(price, 15_012_000_000); assert_eq!(stop_price, 15_100_000_000); } @@ -1813,32 +1541,40 @@ mod tests { #[test] fn snap_prices_leaves_percent_trail_alone() { // trail_pct is basis points, not a price — must never be snapped. - let mut req = OrderRequest::SubmitTrailingStopPct { - order_id: 1, instrument: 0, side: Side::Sell, qty: 1, trail_pct: 137, - trail_stop_price: 0, + let mut req = OrderRequest::SubmitEx { + order_id: 1, instrument: 0, side: Side::Sell, qty: 1, + kind: OrderKind::TrailPct { trail_pct: 137, trail_stop_price: 0 }, + tif: b'0', attrs: OrderAttrs::default(), }; req.snap_prices(TICK_CENT); match req { - OrderRequest::SubmitTrailingStopPct { trail_pct, .. } => assert_eq!(trail_pct, 137), + OrderRequest::SubmitEx { kind: OrderKind::TrailPct { trail_pct, .. }, .. } => + assert_eq!(trail_pct, 137), _ => unreachable!(), } } #[test] fn snap_prices_unknown_tick_is_noop() { - let mut req = OrderRequest::SubmitLimit { - order_id: 1, instrument: 0, side: Side::Buy, qty: 1, price: 15_012_345_678, + let mut req = OrderRequest::SubmitEx { + order_id: 1, instrument: 0, side: Side::Buy, qty: 1, + kind: OrderKind::Limit { price: 15_012_345_678 }, + tif: b'0', attrs: OrderAttrs::default(), }; req.snap_prices(0); match req { - OrderRequest::SubmitLimit { price, .. } => assert_eq!(price, 15_012_345_678), + OrderRequest::SubmitEx { kind: OrderKind::Limit { price }, .. } => + assert_eq!(price, 15_012_345_678), _ => unreachable!(), } } #[test] fn instrument_accessor_covers_submits() { - let req = OrderRequest::SubmitMarket { order_id: 1, instrument: 7, side: Side::Buy, qty: 1 }; + let req = OrderRequest::SubmitEx { + order_id: 1, instrument: 7, side: Side::Buy, qty: 1, + kind: OrderKind::Market, tif: b'0', attrs: OrderAttrs::default(), + }; assert_eq!(req.instrument(), Some(7)); assert_eq!(OrderRequest::Cancel { order_id: 1 }.instrument(), None); assert_eq!( @@ -1847,24 +1583,6 @@ mod tests { ); } - #[test] - fn order_request_submit_market_fields() { - let req = OrderRequest::SubmitMarket { - order_id: 1, - instrument: 0, - side: Side::Sell, - qty: 50, - }; - match req { - OrderRequest::SubmitMarket { instrument, side, qty, .. } => { - assert_eq!(instrument, 0); - assert_eq!(side, Side::Sell); - assert_eq!(qty, 50); - } - _ => panic!("wrong variant"), - } - } - #[test] fn order_request_modify_fields() { let req = OrderRequest::Modify { new_order_id: 100, order_id: 99, price: 200 * PRICE_SCALE, qty: 10 }; diff --git a/tests/error_edge_concurrency.rs b/tests/error_edge_concurrency.rs index c96908fb..e1222c95 100644 --- a/tests/error_edge_concurrency.rs +++ b/tests/error_edge_concurrency.rs @@ -652,9 +652,7 @@ fn concurrent_account_read_write() { fn order_buffer_push_drain_cycle() { let mut buf = OrderBuffer::new(); for i in 0..64 { - buf.push(OrderRequest::SubmitMarket { - order_id: i, instrument: 0, side: Side::Buy, qty: 1, - }); + buf.push(OrderRequest::SubmitEx { order_id: i, instrument: 0, side: Side::Buy, qty: 1, kind: OrderKind::Market, tif: b'0', attrs: OrderAttrs::default() }); } let drained: Vec<_> = buf.drain().collect(); assert_eq!(drained.len(), 64); @@ -667,9 +665,7 @@ fn order_buffer_multiple_drain_cycles() { let mut buf = OrderBuffer::new(); for cycle in 0..5 { for i in 0..10 { - buf.push(OrderRequest::SubmitMarket { - order_id: cycle * 10 + i, instrument: 0, side: Side::Buy, qty: 1, - }); + buf.push(OrderRequest::SubmitEx { order_id: cycle * 10 + i, instrument: 0, side: Side::Buy, qty: 1, kind: OrderKind::Market, tif: b'0', attrs: OrderAttrs::default() }); } let drained: Vec<_> = buf.drain().collect(); assert_eq!(drained.len(), 10); diff --git a/tests/ib_paper_compat/account.rs b/tests/ib_paper_compat/account.rs index ffc70fb2..73cf7dbd 100644 --- a/tests/ib_paper_compat/account.rs +++ b/tests/ib_paper_compat/account.rs @@ -155,18 +155,14 @@ pub(super) fn phase_position_tracking(conns: Conns) -> Conns { tick_count += 1; if phase == 0 && tick_count >= 5 { let buy_oid = next_order_id(); - control_tx.send(ControlCommand::Order(OrderRequest::SubmitMarket { - order_id: buy_oid, instrument, side: Side::Buy, qty: 1, - })).unwrap(); + control_tx.send(ControlCommand::Order(OrderRequest::SubmitEx { order_id: buy_oid, instrument, side: Side::Buy, qty: 1, kind: OrderKind::Market, tif: b'0', attrs: OrderAttrs::default() })).unwrap(); phase = 1; } } Ok(Event::Fill(fill)) => { if phase == 1 && fill.side == Side::Buy { let sell_order_id = next_order_id() + 1; - control_tx.send(ControlCommand::Order(OrderRequest::SubmitMarket { - order_id: sell_order_id, instrument: fill.instrument, side: Side::Sell, qty: 1, - })).unwrap(); + control_tx.send(ControlCommand::Order(OrderRequest::SubmitEx { order_id: sell_order_id, instrument: fill.instrument, side: Side::Sell, qty: 1, kind: OrderKind::Market, tif: b'0', attrs: OrderAttrs::default() })).unwrap(); phase = 2; } else if phase == 2 && fill.side == Side::Sell { // Wait a bit more for position update @@ -776,12 +772,7 @@ pub(super) fn phase_enriched_exec_details(conns: Conns) -> Conns { }).unwrap(); let order_id = next_order_id(); - control_tx.send(ControlCommand::Order(OrderRequest::SubmitMarket { - order_id, - instrument: inst_id, - side: Side::Buy, - qty: 1, - })).unwrap(); + control_tx.send(ControlCommand::Order(OrderRequest::SubmitEx { order_id, instrument: inst_id, side: Side::Buy, qty: 1, kind: OrderKind::Market, tif: b'0', attrs: OrderAttrs::default() })).unwrap(); let join = run_hot_loop(hot_loop); @@ -835,12 +826,7 @@ pub(super) fn phase_enriched_exec_details(conns: Conns) -> Conns { // Sell back to flatten let sell_id = next_order_id(); - control_tx.send(ControlCommand::Order(OrderRequest::SubmitMarket { - order_id: sell_id, - instrument: inst_id, - side: Side::Sell, - qty: 1, - })).unwrap(); + control_tx.send(ControlCommand::Order(OrderRequest::SubmitEx { order_id: sell_id, instrument: inst_id, side: Side::Sell, qty: 1, kind: OrderKind::Market, tif: b'0', attrs: OrderAttrs::default() })).unwrap(); let deadline = Instant::now() + Duration::from_secs(15); while Instant::now() < deadline { match event_rx.recv_timeout(Duration::from_millis(100)) { diff --git a/tests/ib_paper_compat/common.rs b/tests/ib_paper_compat/common.rs index 8c1ad2ab..42739b0c 100644 --- a/tests/ib_paper_compat/common.rs +++ b/tests/ib_paper_compat/common.rs @@ -508,46 +508,7 @@ pub(super) fn run_submit_cancel_phase( let inst_id = hot_loop.context_mut().register_instrument(756733); hot_loop.context_mut().set_symbol(inst_id, "SPY".to_string()); - let order_id = match &order_req { - OrderRequest::SubmitLimit { order_id, .. } => *order_id, - OrderRequest::SubmitMarket { order_id, .. } => *order_id, - OrderRequest::SubmitStop { order_id, .. } => *order_id, - OrderRequest::SubmitStopLimit { order_id, .. } => *order_id, - OrderRequest::SubmitLimitGtc { order_id, .. } => *order_id, - OrderRequest::SubmitStopGtc { order_id, .. } => *order_id, - OrderRequest::SubmitStopLimitGtc { order_id, .. } => *order_id, - OrderRequest::SubmitLimitIoc { order_id, .. } => *order_id, - OrderRequest::SubmitLimitFok { order_id, .. } => *order_id, - OrderRequest::SubmitTrailingStop { order_id, .. } => *order_id, - OrderRequest::SubmitTrailingStopLimit { order_id, .. } => *order_id, - OrderRequest::SubmitTrailingStopPct { order_id, .. } => *order_id, - OrderRequest::SubmitMoc { order_id, .. } => *order_id, - OrderRequest::SubmitLoc { order_id, .. } => *order_id, - OrderRequest::SubmitMit { order_id, .. } => *order_id, - OrderRequest::SubmitLit { order_id, .. } => *order_id, - OrderRequest::SubmitLimitEx { order_id, .. } => *order_id, - OrderRequest::SubmitRel { order_id, .. } => *order_id, - OrderRequest::SubmitLimitOpg { order_id, .. } => *order_id, - OrderRequest::SubmitMtl { order_id, .. } => *order_id, - OrderRequest::SubmitMktPrt { order_id, .. } => *order_id, - OrderRequest::SubmitStpPrt { order_id, .. } => *order_id, - OrderRequest::SubmitMidPrice { order_id, .. } => *order_id, - OrderRequest::SubmitSnapMkt { order_id, .. } => *order_id, - OrderRequest::SubmitSnapMid { order_id, .. } => *order_id, - OrderRequest::SubmitSnapPri { order_id, .. } => *order_id, - OrderRequest::SubmitPegMkt { order_id, .. } => *order_id, - OrderRequest::SubmitPegMid { order_id, .. } => *order_id, - OrderRequest::SubmitPegBench { order_id, .. } => *order_id, - OrderRequest::SubmitLimitAuc { order_id, .. } => *order_id, - OrderRequest::SubmitMtlAuc { order_id, .. } => *order_id, - OrderRequest::SubmitLimitFractional { order_id, .. } => *order_id, - OrderRequest::SubmitEx { order_id, .. } => *order_id, - OrderRequest::SubmitTrailingStopPctEx { order_id, .. } => *order_id, - OrderRequest::SubmitBracket { parent_id, .. } => *parent_id, - OrderRequest::Cancel { order_id } => *order_id, - OrderRequest::CancelAll { .. } => 0, - OrderRequest::Modify { new_order_id, .. } => *new_order_id, - }; + let order_id = order_req.order_id(); control_tx.send(ControlCommand::Order(order_req)).unwrap(); control_tx.send(ControlCommand::Subscribe { con_id: 756733, symbol: "SPY".into(), exchange: String::new(), sec_type: String::new(), last_trade_date: String::new(), strike: 0.0, right: String::new(), multiplier: String::new(), mode_9887: 0, reply_tx: None }).unwrap(); diff --git a/tests/ib_paper_compat/error_handling.rs b/tests/ib_paper_compat/error_handling.rs index 77170d79..df056f7e 100644 --- a/tests/ib_paper_compat/error_handling.rs +++ b/tests/ib_paper_compat/error_handling.rs @@ -22,9 +22,7 @@ pub(super) fn phase_ib_error_handling(conns: Conns) -> Conns { let bogus_inst = hot_loop.context_mut().register_instrument(999999999); hot_loop.context_mut().set_symbol(bogus_inst, "BOGUS".to_string()); - control_tx.send(ControlCommand::Order(OrderRequest::SubmitMarket { - order_id: oid, instrument: bogus_inst, side: Side::Buy, qty: 1, - })).unwrap(); + control_tx.send(ControlCommand::Order(OrderRequest::SubmitEx { order_id: oid, instrument: bogus_inst, side: Side::Buy, qty: 1, kind: OrderKind::Market, tif: b'0', attrs: OrderAttrs::default() })).unwrap(); control_tx.send(ControlCommand::Subscribe { con_id: 756733, symbol: "SPY".into(), exchange: String::new(), sec_type: String::new(), last_trade_date: String::new(), strike: 0.0, right: String::new(), multiplier: String::new(), mode_9887: 0, reply_tx: None }).unwrap(); let join = run_hot_loop(hot_loop); diff --git a/tests/ib_paper_compat/orders.rs b/tests/ib_paper_compat/orders.rs index e991b6d3..4ad9825c 100644 --- a/tests/ib_paper_compat/orders.rs +++ b/tests/ib_paper_compat/orders.rs @@ -36,12 +36,7 @@ pub(super) fn phase_market_order(conns: Conns) -> Conns { tick_count += 1; if phase == 0 && tick_count >= 5 { buy_order_id = next_order_id(); - control_tx.send(ControlCommand::Order(OrderRequest::SubmitMarket { - order_id: buy_order_id, - instrument, - side: Side::Buy, - qty: 1, - })).unwrap(); + control_tx.send(ControlCommand::Order(OrderRequest::SubmitEx { order_id: buy_order_id, instrument, side: Side::Buy, qty: 1, kind: OrderKind::Market, tif: b'0', attrs: OrderAttrs::default() })).unwrap(); buy_sent_at = Some(Instant::now()); phase = 1; } @@ -51,12 +46,7 @@ pub(super) fn phase_market_order(conns: Conns) -> Conns { buy_price = fill.price; buy_rtt_us = buy_sent_at.map(|t| t.elapsed().as_micros() as u64).unwrap_or(0); sell_order_id = next_order_id() + 1; - control_tx.send(ControlCommand::Order(OrderRequest::SubmitMarket { - order_id: sell_order_id, - instrument: fill.instrument, - side: Side::Sell, - qty: 1, - })).unwrap(); + control_tx.send(ControlCommand::Order(OrderRequest::SubmitEx { order_id: sell_order_id, instrument: fill.instrument, side: Side::Sell, qty: 1, kind: OrderKind::Market, tif: b'0', attrs: OrderAttrs::default() })).unwrap(); sell_sent_at = Some(Instant::now()); phase = 2; } else if phase == 2 && fill.side == Side::Sell { @@ -111,13 +101,7 @@ pub(super) fn phase_limit_order(conns: Conns) -> Conns { hot_loop.context_mut().set_symbol(inst_id, "SPY".to_string()); let order_id = next_order_id(); - control_tx.send(ControlCommand::Order(OrderRequest::SubmitLimit { - order_id, - instrument: inst_id, - side: Side::Buy, - qty: 1, - price: 1_00_000_000, - })).unwrap(); + control_tx.send(ControlCommand::Order(OrderRequest::SubmitEx { order_id, instrument: inst_id, side: Side::Buy, qty: 1, kind: OrderKind::Limit { price: 1_00_000_000 }, tif: b'0', attrs: OrderAttrs::default() })).unwrap(); control_tx.send(ControlCommand::Subscribe { con_id: 756733, symbol: "SPY".into(), exchange: String::new(), sec_type: String::new(), last_trade_date: String::new(), strike: 0.0, right: String::new(), multiplier: String::new(), mode_9887: 0, reply_tx: None }).unwrap(); let join = run_hot_loop(hot_loop); @@ -187,7 +171,7 @@ pub(super) fn phase_limit_order(conns: Conns) -> Conns { pub(super) fn phase_stop_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 8: Stop Order Submit + Cancel (SPY)", - OrderRequest::SubmitStop { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, stop_price: 1_00_000_000 }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, kind: OrderKind::Stop { stop_price: 1_00_000_000 }, tif: b'0', attrs: OrderAttrs::default() }, false) } @@ -206,9 +190,7 @@ pub(super) fn phase_modify_order(conns: Conns) -> Conns { hot_loop.context_mut().set_symbol(inst_id, "SPY".to_string()); let order_id = next_order_id(); - control_tx.send(ControlCommand::Order(OrderRequest::SubmitLimit { - order_id, instrument: inst_id, side: Side::Buy, qty: 1, price: 1_00_000_000, - })).unwrap(); + control_tx.send(ControlCommand::Order(OrderRequest::SubmitEx { order_id, instrument: inst_id, side: Side::Buy, qty: 1, kind: OrderKind::Limit { price: 1_00_000_000 }, tif: b'0', attrs: OrderAttrs::default() })).unwrap(); control_tx.send(ControlCommand::Subscribe { con_id: 756733, symbol: "SPY".into(), exchange: String::new(), sec_type: String::new(), last_trade_date: String::new(), strike: 0.0, right: String::new(), multiplier: String::new(), mode_9887: 0, reply_tx: None }).unwrap(); let join = run_hot_loop(hot_loop); @@ -274,7 +256,7 @@ pub(super) fn phase_outside_rth(conns: Conns) -> Conns { pub(super) fn phase_stop_limit_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 15: Stop Limit Order Submit + Cancel (SPY)", - OrderRequest::SubmitStopLimit { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, price: 998_00_000_000, stop_price: 999_00_000_000 }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, kind: OrderKind::StopLimit { price: 998_00_000_000, stop_price: 999_00_000_000 }, tif: b'0', attrs: OrderAttrs::default() }, false) } @@ -293,9 +275,7 @@ pub(super) fn phase_commission(conns: Conns) -> Conns { hot_loop.context_mut().set_symbol(inst_id, "SPY".to_string()); let buy_id = next_order_id(); - control_tx.send(ControlCommand::Order(OrderRequest::SubmitMarket { - order_id: buy_id, instrument: inst_id, side: Side::Buy, qty: 1, - })).unwrap(); + control_tx.send(ControlCommand::Order(OrderRequest::SubmitEx { order_id: buy_id, instrument: inst_id, side: Side::Buy, qty: 1, kind: OrderKind::Market, tif: b'0', attrs: OrderAttrs::default() })).unwrap(); control_tx.send(ControlCommand::Subscribe { con_id: 756733, symbol: "SPY".into(), exchange: String::new(), sec_type: String::new(), last_trade_date: String::new(), strike: 0.0, right: String::new(), multiplier: String::new(), mode_9887: 0, reply_tx: None }).unwrap(); let join = run_hot_loop(hot_loop); @@ -314,9 +294,7 @@ pub(super) fn phase_commission(conns: Conns) -> Conns { buy_price = fill.price; buy_comm = fill.commission; let sid = next_order_id() + 1; - control_tx.send(ControlCommand::Order(OrderRequest::SubmitMarket { - order_id: sid, instrument: fill.instrument, side: Side::Sell, qty: 1, - })).unwrap(); + control_tx.send(ControlCommand::Order(OrderRequest::SubmitEx { order_id: sid, instrument: fill.instrument, side: Side::Sell, qty: 1, kind: OrderKind::Market, tif: b'0', attrs: OrderAttrs::default() })).unwrap(); phase = 2; } else if phase == 2 && fill.side == Side::Sell { sell_price = fill.price; @@ -435,9 +413,7 @@ pub(super) fn phase_modify_qty(conns: Conns) -> Conns { let order_id = next_order_id(); let new_order_id = order_id + 1; - control_tx.send(ControlCommand::Order(OrderRequest::SubmitLimit { - order_id, instrument: inst_id, side: Side::Buy, qty: 1, price: 1_00_000_000, - })).unwrap(); + control_tx.send(ControlCommand::Order(OrderRequest::SubmitEx { order_id, instrument: inst_id, side: Side::Buy, qty: 1, kind: OrderKind::Limit { price: 1_00_000_000 }, tif: b'0', attrs: OrderAttrs::default() })).unwrap(); control_tx.send(ControlCommand::Subscribe { con_id: 756733, symbol: "SPY".into(), exchange: String::new(), sec_type: String::new(), last_trade_date: String::new(), strike: 0.0, right: String::new(), multiplier: String::new(), mode_9887: 0, reply_tx: None }).unwrap(); let join = run_hot_loop(hot_loop); @@ -493,7 +469,7 @@ pub(super) fn phase_modify_qty(conns: Conns) -> Conns { pub(super) fn phase_trailing_stop(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 19: Trailing Stop Order (SPY)", - OrderRequest::SubmitTrailingStop { order_id: oid, instrument: 0, side: Side::Sell, qty: 1, trail_amt: 5_00_000_000 }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Sell, qty: 1, kind: OrderKind::TrailingStop { trail_stop_price: 0, trail_amt: 5_00_000_000 }, tif: b'0', attrs: OrderAttrs::default() }, false) } @@ -502,7 +478,7 @@ pub(super) fn phase_trailing_stop(conns: Conns) -> Conns { pub(super) fn phase_trailing_stop_limit(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 20: Trailing Stop Limit Order (SPY)", - OrderRequest::SubmitTrailingStopLimit { order_id: oid, instrument: 0, side: Side::Sell, qty: 1, lmt_offset: 1_00_000_000, trail_amt: 5_00_000_000 }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Sell, qty: 1, kind: OrderKind::TrailingStopLimit { trail_stop_price: 0, lmt_offset: 1_00_000_000, trail_amt: 5_00_000_000 }, tif: b'0', attrs: OrderAttrs::default() }, false) } @@ -627,7 +603,7 @@ pub(super) fn phase_stop_limit_gtc(conns: Conns) -> Conns { pub(super) fn phase_mit_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 25: Market if Touched Order (SPY)", - OrderRequest::SubmitMit { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, stop_price: 1_00_000_000 }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, kind: OrderKind::Mit { stop_price: 1_00_000_000 }, tif: b'0', attrs: OrderAttrs::default() }, false) } @@ -636,7 +612,7 @@ pub(super) fn phase_mit_order(conns: Conns) -> Conns { pub(super) fn phase_lit_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 26: Limit if Touched Order (SPY)", - OrderRequest::SubmitLit { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, price: 2_00_000_000, stop_price: 1_00_000_000 }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, kind: OrderKind::Lit { price: 2_00_000_000, stop_price: 1_00_000_000 }, tif: b'0', attrs: OrderAttrs::default() }, false) } @@ -645,7 +621,7 @@ pub(super) fn phase_lit_order(conns: Conns) -> Conns { pub(super) fn phase_moc_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 27: MOC Order (SPY)", - OrderRequest::SubmitMoc { order_id: oid, instrument: 0, side: Side::Buy, qty: 1 }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, kind: OrderKind::Moc, tif: b'0', attrs: OrderAttrs::default() }, false) } @@ -654,7 +630,7 @@ pub(super) fn phase_moc_order(conns: Conns) -> Conns { pub(super) fn phase_loc_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 28: LOC Order (SPY)", - OrderRequest::SubmitLoc { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, price: 1_00_000_000 }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, kind: OrderKind::Loc { price: 1_00_000_000 }, tif: b'0', attrs: OrderAttrs::default() }, false) } @@ -740,7 +716,7 @@ pub(super) fn phase_adaptive_order(conns: Conns) -> Conns { pub(super) fn phase_rel_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 31: Relative Order (SPY)", - OrderRequest::SubmitRel { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, offset: 1_000_000 }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, kind: OrderKind::Rel { offset: 1_000_000 }, tif: b'0', attrs: OrderAttrs::default() }, false) } @@ -758,7 +734,7 @@ pub(super) fn phase_limit_opg(conns: Conns) -> Conns { pub(super) fn phase_iceberg_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 33: Iceberg Order (SPY)", - OrderRequest::SubmitLimitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 10, price: 1_00_000_000, tif: b'1', attrs: OrderAttrs { display_size: 1, outside_rth: true, ..OrderAttrs::default() } }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 10, kind: OrderKind::Limit { price: 1_00_000_000 }, tif: b'1', attrs: OrderAttrs { display_size: 1, outside_rth: true, ..OrderAttrs::default() } }, false) } @@ -767,7 +743,7 @@ pub(super) fn phase_iceberg_order(conns: Conns) -> Conns { pub(super) fn phase_hidden_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 34: Hidden Order (SPY)", - OrderRequest::SubmitLimitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, price: 1_00_000_000, tif: b'1', attrs: OrderAttrs { hidden: true, outside_rth: true, ..OrderAttrs::default() } }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, kind: OrderKind::Limit { price: 1_00_000_000 }, tif: b'1', attrs: OrderAttrs { hidden: true, outside_rth: true, ..OrderAttrs::default() } }, false) } @@ -776,7 +752,7 @@ pub(super) fn phase_hidden_order(conns: Conns) -> Conns { pub(super) fn phase_short_sell(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 35: Short Sell Limit Order (SPY)", - OrderRequest::SubmitLimitEx { order_id: oid, instrument: 0, side: Side::ShortSell, qty: 1, price: 1_00_000_000, tif: b'0', attrs: OrderAttrs::default() }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::ShortSell, qty: 1, kind: OrderKind::Limit { price: 1_00_000_000 }, tif: b'0', attrs: OrderAttrs::default() }, false) } @@ -785,7 +761,7 @@ pub(super) fn phase_short_sell(conns: Conns) -> Conns { pub(super) fn phase_trailing_stop_pct(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 36: Trailing Stop Percent Order (SPY)", - OrderRequest::SubmitTrailingStopPct { order_id: oid, instrument: 0, side: Side::Sell, qty: 1, trail_pct: 100 }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Sell, qty: 1, kind: OrderKind::TrailPct { trail_stop_price: 0, trail_pct: 100 }, tif: b'0', attrs: OrderAttrs::default() }, false) } @@ -806,12 +782,12 @@ pub(super) fn phase_oca_group(conns: Conns) -> Conns { let oca = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos() as u64; let id1 = next_order_id(); let id2 = id1 + 1; - control_tx.send(ControlCommand::Order(OrderRequest::SubmitLimitEx { - order_id: id1, instrument: inst_id, side: Side::Buy, qty: 1, price: 1_00_000_000, tif: b'1', + control_tx.send(ControlCommand::Order(OrderRequest::SubmitEx { + order_id: id1, instrument: inst_id, side: Side::Buy, qty: 1, kind: OrderKind::Limit { price: 1_00_000_000 }, tif: b'1', attrs: OrderAttrs { oca_group: oca, outside_rth: true, ..OrderAttrs::default() }, })).unwrap(); - control_tx.send(ControlCommand::Order(OrderRequest::SubmitLimitEx { - order_id: id2, instrument: inst_id, side: Side::Buy, qty: 1, price: 2_00_000_000, tif: b'1', + control_tx.send(ControlCommand::Order(OrderRequest::SubmitEx { + order_id: id2, instrument: inst_id, side: Side::Buy, qty: 1, kind: OrderKind::Limit { price: 2_00_000_000 }, tif: b'1', attrs: OrderAttrs { oca_group: oca, outside_rth: true, ..OrderAttrs::default() }, })).unwrap(); control_tx.send(ControlCommand::Subscribe { con_id: 756733, symbol: "SPY".into(), exchange: String::new(), sec_type: String::new(), last_trade_date: String::new(), strike: 0.0, right: String::new(), multiplier: String::new(), mode_9887: 0, reply_tx: None }).unwrap(); @@ -867,7 +843,7 @@ pub(super) fn phase_oca_group(conns: Conns) -> Conns { pub(super) fn phase_mtl_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 38: Market to Limit Order (SPY)", - OrderRequest::SubmitMtl { order_id: oid, instrument: 0, side: Side::Buy, qty: 1 }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, kind: OrderKind::Mtl, tif: b'0', attrs: OrderAttrs::default() }, true) } @@ -876,7 +852,7 @@ pub(super) fn phase_mtl_order(conns: Conns) -> Conns { pub(super) fn phase_mkt_prt_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 39: Market with Protection Order (SPY)", - OrderRequest::SubmitMktPrt { order_id: oid, instrument: 0, side: Side::Buy, qty: 1 }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, kind: OrderKind::MktPrt, tif: b'0', attrs: OrderAttrs::default() }, true) } @@ -885,7 +861,7 @@ pub(super) fn phase_mkt_prt_order(conns: Conns) -> Conns { pub(super) fn phase_stp_prt_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 40: Stop with Protection Order (SPY)", - OrderRequest::SubmitStpPrt { order_id: oid, instrument: 0, side: Side::Sell, qty: 1, stop_price: 1_00_000_000 }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Sell, qty: 1, kind: OrderKind::StpPrt { stop_price: 1_00_000_000 }, tif: b'0', attrs: OrderAttrs::default() }, false) } @@ -894,7 +870,7 @@ pub(super) fn phase_stp_prt_order(conns: Conns) -> Conns { pub(super) fn phase_mid_price_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 41: Mid-Price Order (SPY)", - OrderRequest::SubmitMidPrice { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, price_cap: 1_00_000_000 }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, kind: OrderKind::MidPrice { price_cap: 1_00_000_000 }, tif: b'0', attrs: OrderAttrs::default() }, false) } @@ -903,7 +879,7 @@ pub(super) fn phase_mid_price_order(conns: Conns) -> Conns { pub(super) fn phase_snap_mkt_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 42: Snap to Market Order (SPY)", - OrderRequest::SubmitSnapMkt { order_id: oid, instrument: 0, side: Side::Buy, qty: 1 }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, kind: OrderKind::SnapMkt, tif: b'0', attrs: OrderAttrs::default() }, true) } @@ -912,7 +888,7 @@ pub(super) fn phase_snap_mkt_order(conns: Conns) -> Conns { pub(super) fn phase_snap_mid_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 43: Snap to Midpoint Order (SPY)", - OrderRequest::SubmitSnapMid { order_id: oid, instrument: 0, side: Side::Buy, qty: 1 }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, kind: OrderKind::SnapMid, tif: b'0', attrs: OrderAttrs::default() }, true) } @@ -921,7 +897,7 @@ pub(super) fn phase_snap_mid_order(conns: Conns) -> Conns { pub(super) fn phase_snap_pri_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 44: Snap to Primary Order (SPY)", - OrderRequest::SubmitSnapPri { order_id: oid, instrument: 0, side: Side::Buy, qty: 1 }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, kind: OrderKind::SnapPri, tif: b'0', attrs: OrderAttrs::default() }, true) } @@ -930,7 +906,7 @@ pub(super) fn phase_snap_pri_order(conns: Conns) -> Conns { pub(super) fn phase_peg_mkt_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 45: Pegged to Market Order (SPY)", - OrderRequest::SubmitPegMkt { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, offset: 0 }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, kind: OrderKind::PegMkt { offset: 0 }, tif: b'0', attrs: OrderAttrs::default() }, true) } @@ -939,7 +915,7 @@ pub(super) fn phase_peg_mkt_order(conns: Conns) -> Conns { pub(super) fn phase_peg_mid_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 46: Pegged to Midpoint Order (SPY)", - OrderRequest::SubmitPegMid { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, offset: 0 }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, kind: OrderKind::PegMid { offset: 0 }, tif: b'0', attrs: OrderAttrs::default() }, true) } @@ -948,7 +924,7 @@ pub(super) fn phase_peg_mid_order(conns: Conns) -> Conns { pub(super) fn phase_discretionary_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 47: Discretionary Amount Order (SPY)", - OrderRequest::SubmitLimitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, price: 1_00_000_000, tif: b'1', attrs: OrderAttrs { discretionary_amt: 50_000_000, outside_rth: true, ..OrderAttrs::default() } }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, kind: OrderKind::Limit { price: 1_00_000_000 }, tif: b'1', attrs: OrderAttrs { discretionary_amt: 50_000_000, outside_rth: true, ..OrderAttrs::default() } }, false) } @@ -957,7 +933,7 @@ pub(super) fn phase_discretionary_order(conns: Conns) -> Conns { pub(super) fn phase_sweep_to_fill_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 48: Sweep to Fill Order (SPY)", - OrderRequest::SubmitLimitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, price: 1_00_000_000, tif: b'1', attrs: OrderAttrs { sweep_to_fill: true, outside_rth: true, ..OrderAttrs::default() } }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, kind: OrderKind::Limit { price: 1_00_000_000 }, tif: b'1', attrs: OrderAttrs { sweep_to_fill: true, outside_rth: true, ..OrderAttrs::default() } }, false) } @@ -966,7 +942,7 @@ pub(super) fn phase_sweep_to_fill_order(conns: Conns) -> Conns { pub(super) fn phase_all_or_none_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 49: All or None Order (SPY)", - OrderRequest::SubmitLimitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, price: 1_00_000_000, tif: b'1', attrs: OrderAttrs { all_or_none: true, outside_rth: true, ..OrderAttrs::default() } }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, kind: OrderKind::Limit { price: 1_00_000_000 }, tif: b'1', attrs: OrderAttrs { all_or_none: true, outside_rth: true, ..OrderAttrs::default() } }, false) } @@ -975,7 +951,7 @@ pub(super) fn phase_all_or_none_order(conns: Conns) -> Conns { pub(super) fn phase_trigger_method_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 50: Trigger Method Order (SPY)", - OrderRequest::SubmitLimitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, price: 1_00_000_000, tif: b'1', attrs: OrderAttrs { trigger_method: 2, outside_rth: true, ..OrderAttrs::default() } }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, kind: OrderKind::Limit { price: 1_00_000_000 }, tif: b'1', attrs: OrderAttrs { trigger_method: 2, outside_rth: true, ..OrderAttrs::default() } }, false) } @@ -984,7 +960,7 @@ pub(super) fn phase_trigger_method_order(conns: Conns) -> Conns { pub(super) fn phase_price_condition_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 57: Price Condition Order (SPY)", - OrderRequest::SubmitLimitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, price: 1_00_000_000, tif: b'1', + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, kind: OrderKind::Limit { price: 1_00_000_000 }, tif: b'1', attrs: OrderAttrs { outside_rth: true, conditions: vec![OrderCondition::Price { con_id: 756733, exchange: "BEST".into(), price: 1_00_000_000, is_more: false, trigger_method: 0 }], ..OrderAttrs::default() } }, false) } @@ -994,7 +970,7 @@ pub(super) fn phase_price_condition_order(conns: Conns) -> Conns { pub(super) fn phase_time_condition_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 58: Time Condition Order (SPY)", - OrderRequest::SubmitLimitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, price: 1_00_000_000, tif: b'1', + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, kind: OrderKind::Limit { price: 1_00_000_000 }, tif: b'1', attrs: OrderAttrs { outside_rth: true, conditions: vec![OrderCondition::Time { time: "20991231-23:59:59".into(), is_more: false }], ..OrderAttrs::default() } }, false) } @@ -1004,7 +980,7 @@ pub(super) fn phase_time_condition_order(conns: Conns) -> Conns { pub(super) fn phase_volume_condition_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 59: Volume Condition Order (SPY)", - OrderRequest::SubmitLimitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, price: 1_00_000_000, tif: b'1', + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, kind: OrderKind::Limit { price: 1_00_000_000 }, tif: b'1', attrs: OrderAttrs { outside_rth: true, conditions: vec![OrderCondition::Volume { con_id: 756733, exchange: "BEST".into(), volume: 999_999_999, is_more: true }], ..OrderAttrs::default() } }, false) } @@ -1014,7 +990,7 @@ pub(super) fn phase_volume_condition_order(conns: Conns) -> Conns { pub(super) fn phase_multi_condition_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 60: Multi-Condition Order (SPY)", - OrderRequest::SubmitLimitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, price: 1_00_000_000, tif: b'1', + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, kind: OrderKind::Limit { price: 1_00_000_000 }, tif: b'1', attrs: OrderAttrs { outside_rth: true, conditions: vec![ @@ -1125,7 +1101,7 @@ pub(super) fn phase_mtl_auc_order(conns: Conns) -> Conns { pub(super) fn phase_box_top_order(conns: Conns) -> Conns { let oid = next_order_id(); run_submit_cancel_phase(conns, "Phase 71: Box Top Order (SPY)", - OrderRequest::SubmitMtl { order_id: oid, instrument: 0, side: Side::Buy, qty: 1 }, + OrderRequest::SubmitEx { order_id: oid, instrument: 0, side: Side::Buy, qty: 1, kind: OrderKind::Mtl, tif: b'0', attrs: OrderAttrs::default() }, true) } @@ -1237,8 +1213,9 @@ pub(super) fn phase_cash_qty_order(conns: Conns) -> Conns { hot_loop.context_mut().set_symbol(inst_id, "SPY".to_string()); let order_id = next_order_id(); - control_tx.send(ControlCommand::Order(OrderRequest::SubmitLimitEx { - order_id, instrument: inst_id, side: Side::Buy, qty: 100, price: 1_00_000_000, tif: b'0', + control_tx.send(ControlCommand::Order(OrderRequest::SubmitEx { + order_id, instrument: inst_id, side: Side::Buy, qty: 100, + kind: OrderKind::Limit { price: 1_00_000_000 }, tif: b'0', attrs: OrderAttrs { cash_qty: 1000 * PRICE_SCALE, ..OrderAttrs::default() }, })).unwrap(); control_tx.send(ControlCommand::Subscribe { con_id: 756733, symbol: "SPY".into(), exchange: String::new(), sec_type: String::new(), last_trade_date: String::new(), strike: 0.0, right: String::new(), multiplier: String::new(), mode_9887: 0, reply_tx: None }).unwrap(); @@ -1430,9 +1407,7 @@ pub(super) fn phase_bracket_fill_cascade(conns: Conns) -> Conns { cancelled_count += 1; if cancelled_count >= 2 { let sid = next_order_id() + 10; - control_tx.send(ControlCommand::Order(OrderRequest::SubmitMarket { - order_id: sid, instrument: inst_id, side: Side::Sell, qty: 1, - })).unwrap(); + control_tx.send(ControlCommand::Order(OrderRequest::SubmitEx { order_id: sid, instrument: inst_id, side: Side::Sell, qty: 1, kind: OrderKind::Market, tif: b'0', attrs: OrderAttrs::default() })).unwrap(); } } OrderStatus::Rejected => { any_rejected = true; break; } @@ -1493,9 +1468,7 @@ pub(super) fn phase_pnl_after_round_trip(conns: Conns) -> Conns { tick_count += 1; if phase == 0 && tick_count >= 5 { let oid = next_order_id(); - control_tx.send(ControlCommand::Order(OrderRequest::SubmitMarket { - order_id: oid, instrument: inst_id, side: Side::Buy, qty: 1, - })).unwrap(); + control_tx.send(ControlCommand::Order(OrderRequest::SubmitEx { order_id: oid, instrument: inst_id, side: Side::Buy, qty: 1, kind: OrderKind::Market, tif: b'0', attrs: OrderAttrs::default() })).unwrap(); phase = 1; } if phase == 3 { @@ -1511,9 +1484,7 @@ pub(super) fn phase_pnl_after_round_trip(conns: Conns) -> Conns { if phase == 1 && fill.side == Side::Buy { buy_filled = true; let sid = next_order_id() + 1; - control_tx.send(ControlCommand::Order(OrderRequest::SubmitMarket { - order_id: sid, instrument: fill.instrument, side: Side::Sell, qty: 1, - })).unwrap(); + control_tx.send(ControlCommand::Order(OrderRequest::SubmitEx { order_id: sid, instrument: fill.instrument, side: Side::Sell, qty: 1, kind: OrderKind::Market, tif: b'0', attrs: OrderAttrs::default() })).unwrap(); phase = 2; } else if phase == 2 && fill.side == Side::Sell { sell_filled = true; @@ -1641,9 +1612,7 @@ pub(super) fn phase_rapid_order_dedup(conns: Conns) -> Conns { let order_ids: Vec = (0..5).map(|i| base_oid + i * 1000).collect(); for (i, &oid) in order_ids.iter().enumerate() { let price = (1 + i as i64) * 1_00_000_000; // $1, $2, $3, $4, $5 - control_tx.send(ControlCommand::Order(OrderRequest::SubmitLimit { - order_id: oid, instrument: inst_id, side: Side::Buy, qty: 1, price, - })).unwrap(); + control_tx.send(ControlCommand::Order(OrderRequest::SubmitEx { order_id: oid, instrument: inst_id, side: Side::Buy, qty: 1, kind: OrderKind::Limit { price }, tif: b'0', attrs: OrderAttrs::default() })).unwrap(); } control_tx.send(ControlCommand::Subscribe { con_id: 756733, symbol: "SPY".into(), exchange: String::new(), sec_type: String::new(), last_trade_date: String::new(), strike: 0.0, right: String::new(), multiplier: String::new(), mode_9887: 0, reply_tx: None }).unwrap(); let join = run_hot_loop(hot_loop); @@ -1721,9 +1690,7 @@ pub(super) fn phase_modify_price_and_qty(conns: Conns) -> Conns { let order_id = next_order_id(); let new_order_id = order_id + 1; // Submit limit buy at $1, qty=1 - control_tx.send(ControlCommand::Order(OrderRequest::SubmitLimit { - order_id, instrument: inst_id, side: Side::Buy, qty: 1, price: 1_00_000_000, - })).unwrap(); + control_tx.send(ControlCommand::Order(OrderRequest::SubmitEx { order_id, instrument: inst_id, side: Side::Buy, qty: 1, kind: OrderKind::Limit { price: 1_00_000_000 }, tif: b'0', attrs: OrderAttrs::default() })).unwrap(); control_tx.send(ControlCommand::Subscribe { con_id: 756733, symbol: "SPY".into(), exchange: String::new(), sec_type: String::new(), last_trade_date: String::new(), strike: 0.0, right: String::new(), multiplier: String::new(), mode_9887: 0, reply_tx: None }).unwrap(); let join = run_hot_loop(hot_loop); @@ -1794,9 +1761,7 @@ pub(super) fn phase_double_modify(conns: Conns) -> Conns { let modify_id_2 = order_id + 2; // Submit limit buy at $1 - control_tx.send(ControlCommand::Order(OrderRequest::SubmitLimit { - order_id, instrument: inst_id, side: Side::Buy, qty: 1, price: 1_00_000_000, - })).unwrap(); + control_tx.send(ControlCommand::Order(OrderRequest::SubmitEx { order_id, instrument: inst_id, side: Side::Buy, qty: 1, kind: OrderKind::Limit { price: 1_00_000_000 }, tif: b'0', attrs: OrderAttrs::default() })).unwrap(); control_tx.send(ControlCommand::Subscribe { con_id: 756733, symbol: "SPY".into(), exchange: String::new(), sec_type: String::new(), last_trade_date: String::new(), strike: 0.0, right: String::new(), multiplier: String::new(), mode_9887: 0, reply_tx: None }).unwrap(); let join = run_hot_loop(hot_loop); @@ -1873,9 +1838,7 @@ pub(super) fn phase_cancel_during_modify(conns: Conns) -> Conns { let new_order_id = order_id + 1; // Submit limit buy at $1 - control_tx.send(ControlCommand::Order(OrderRequest::SubmitLimit { - order_id, instrument: inst_id, side: Side::Buy, qty: 1, price: 1_00_000_000, - })).unwrap(); + control_tx.send(ControlCommand::Order(OrderRequest::SubmitEx { order_id, instrument: inst_id, side: Side::Buy, qty: 1, kind: OrderKind::Limit { price: 1_00_000_000 }, tif: b'0', attrs: OrderAttrs::default() })).unwrap(); control_tx.send(ControlCommand::Subscribe { con_id: 756733, symbol: "SPY".into(), exchange: String::new(), sec_type: String::new(), last_trade_date: String::new(), strike: 0.0, right: String::new(), multiplier: String::new(), mode_9887: 0, reply_tx: None }).unwrap(); let join = run_hot_loop(hot_loop); @@ -2030,9 +1993,7 @@ pub(super) fn phase_cancel_filled_order(conns: Conns) -> Conns { if phase == 0 && tick_count >= 5 { buy_order_id = next_order_id(); instrument_id = instrument; - control_tx.send(ControlCommand::Order(OrderRequest::SubmitMarket { - order_id: buy_order_id, instrument, side: Side::Buy, qty: 1, - })).unwrap(); + control_tx.send(ControlCommand::Order(OrderRequest::SubmitEx { order_id: buy_order_id, instrument, side: Side::Buy, qty: 1, kind: OrderKind::Market, tif: b'0', attrs: OrderAttrs::default() })).unwrap(); phase = 1; } } @@ -2076,9 +2037,7 @@ pub(super) fn phase_cancel_filled_order(conns: Conns) -> Conns { } // Sell to flatten position let sell_oid = next_order_id() + 1; - control_tx.send(ControlCommand::Order(OrderRequest::SubmitMarket { - order_id: sell_oid, instrument: instrument_id, side: Side::Sell, qty: 1, - })).unwrap(); + control_tx.send(ControlCommand::Order(OrderRequest::SubmitEx { order_id: sell_oid, instrument: instrument_id, side: Side::Sell, qty: 1, kind: OrderKind::Market, tif: b'0', attrs: OrderAttrs::default() })).unwrap(); phase = 3; // Wait for sell fill let sell_deadline = Instant::now() + Duration::from_secs(15);