diff --git a/src/api/client/tests.rs b/src/api/client/tests.rs index 98f2fe81..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,17 +351,17 @@ 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), } } #[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,17 +441,17 @@ 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), } } #[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 { @@ -429,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 { @@ -448,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), } } @@ -477,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), } } @@ -498,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), } } @@ -522,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 { @@ -536,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 ── @@ -605,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), } } @@ -625,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), } } @@ -645,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), } } @@ -664,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), } } @@ -682,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] @@ -695,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] @@ -709,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] @@ -723,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] @@ -737,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] @@ -750,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] @@ -763,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] @@ -777,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] @@ -791,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] @@ -805,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] @@ -819,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] @@ -833,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] @@ -846,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] @@ -859,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] @@ -872,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] @@ -885,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] @@ -899,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"), @@ -917,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"), @@ -937,7 +974,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] @@ -951,7 +989,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] @@ -1017,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"), @@ -1086,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] @@ -1125,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/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/client_core.rs b/src/client_core.rs index 3aaeb264..2f2c7087 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 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, + 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 @@ -1468,78 +1476,50 @@ 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(), })); } - // 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) } - 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" => { @@ -1554,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)), }; @@ -1944,4 +1909,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 0c39e6a1..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 } @@ -562,16 +530,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 } @@ -584,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 } @@ -598,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 } @@ -613,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 } @@ -628,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 } @@ -642,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 } @@ -656,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 } @@ -670,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 } @@ -685,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 } @@ -700,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 } @@ -714,11 +699,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 +783,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 } @@ -821,6 +814,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 +830,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 } @@ -1002,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); @@ -1026,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); @@ -1349,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"), @@ -1507,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); @@ -1624,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); @@ -1637,18 +1644,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"), } } @@ -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..79b4efbe 100644 --- a/src/engine/hot_loop/order_builder.rs +++ b/src/engine/hot_loop/order_builder.rs @@ -38,72 +38,6 @@ 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, @@ -140,78 +74,10 @@ pub(crate) fn drain_and_send_orders( } 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, @@ -245,817 +111,19 @@ pub(crate) fn drain_and_send_orders( ]; 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::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( - 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 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), - (11, &clord_str), - (1, account_id), - (21, "2"), - (55, &symbol), - (54, side_str), - (38, &qty_str), - (40, "PB"), // OrdType = Pegged to Benchmark - (44, &price_str), // Limit price - (59, "0"), - (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 - ]) - } - OrderRequest::SubmitLimitAuc { order_id, instrument, side, qty, price } => { - context.insert_order(crate::types::Order::new( - 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 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), // Limit price - (59, "8"), // TIF = Auction - (60, &now), - (167, &sec_type_str), - (100, &destination), - (6210, &destination), - (15, "USD"), - (204, "0"), - ]) - } - OrderRequest::SubmitMtlAuc { order_id, instrument, side, qty } => { - context.insert_order(crate::types::Order::new( - 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 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, "K"), // OrdType = Market to Limit - (59, "8"), // TIF = Auction - (60, &now), - (167, &sec_type_str), - (100, &destination), - (6210, &destination), - (15, "USD"), - (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, - )); - 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 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), // Decimal qty (e.g., "0.5") - (40, "2"), // OrdType = Limit - (44, &price_str), - (59, "0"), - (60, &now), - (167, &sec_type_str), - (100, &destination), - (6210, &destination), - (15, "USD"), - (204, "0"), - ]) + } + conn.send_fix(&fields) } - 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 } => { + 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'3', b'0', stop_price, + 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 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(); @@ -1068,43 +136,31 @@ pub(crate) fn drain_and_send_orders( (55, &symbol), (54, side_str), (38, &qty_str), - (40, "3"), // OrdType = Stop - (99, &stop_str), // StopPx - (59, "0"), + (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"), - (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)); + if outside_rth { + fields.push((6433, "1")); } conn.send_fix(&fields) } - OrderRequest::SubmitMtl { order_id, instrument, side, qty } => { + OrderRequest::SubmitLimitIoc { 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'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(); @@ -1117,8 +173,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, "3"), // TIF = IOC (60, &now), (167, &sec_type_str), (100, &destination), @@ -1127,14 +184,15 @@ pub(crate) fn drain_and_send_orders( (204, "0"), ]) } - OrderRequest::SubmitMktPrt { 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'U', 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(); @@ -1147,8 +205,9 @@ pub(crate) fn drain_and_send_orders( (55, &symbol), (54, side_str), (38, &qty_str), - (40, "U"), // OrdType = Market with Protection - (59, "0"), + (40, "2"), // OrdType = Limit + (44, &price_str), + (59, "4"), // TIF = FOK (60, &now), (167, &sec_type_str), (100, &destination), @@ -1157,84 +216,113 @@ pub(crate) fn drain_and_send_orders( (204, "0"), ]) } - OrderRequest::SubmitStpPrt { order_id, instrument, side, qty, stop_price } => { - context.insert_order(crate::types::Order::new( - order_id, instrument, side, qty, 0, crate::types::ORD_STP_PRT, b'0', stop_price, - )); - 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 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 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, "SP"), // OrdType = Stop with Protection - (99, &stop_str), // StopPx - (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::SubmitMidPrice { order_id, instrument, side, qty, price_cap } => { + ]); + + // 2. Take-profit child: limit 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, + 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 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![ + 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, "MIDPX"), // OrdType = Mid-Price - (59, "0"), + (40, "2"), // Limit + (44, &tp_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) + ]); + + // 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::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), @@ -1245,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), @@ -1275,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), @@ -1305,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), @@ -1335,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), @@ -1370,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. @@ -1632,6 +722,11 @@ 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), + 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, @@ -1677,7 +772,14 @@ fn send_order_ex( fields.push((44, format_price(price).to_string())); fields.push((99, format_price(stop_price).to_string())); } - K::TrailingStop { trail_amt, trail_stop_price } => { + 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, .. } => { // 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(); @@ -1685,11 +787,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. @@ -1698,9 +798,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. @@ -1710,7 +809,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())), @@ -1733,29 +831,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. @@ -1764,6 +845,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())); @@ -1865,6 +964,88 @@ 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())); + } + } + + // 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 { + 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) } @@ -2056,4 +1237,207 @@ mod tests { assert_eq!(context.order(8).unwrap().status, OrderStatus::Filled); 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. + #[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..f976a9c6 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 }, @@ -508,39 +508,37 @@ 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, + }, + /// 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. #[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, @@ -580,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 { @@ -663,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 @@ -686,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, @@ -702,91 +612,7 @@ pub enum OrderRequest { qty: u32, 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, - 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.). - 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 { @@ -817,13 +643,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 { @@ -833,27 +652,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, }, @@ -875,43 +673,16 @@ 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::SubmitAdaptive { 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::SubmitAlgo { order_id, .. } | Self::SubmitPegBench { order_id, .. } | Self::SubmitLimitAuc { order_id, .. } | 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, } @@ -924,43 +695,16 @@ 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::SubmitAdaptive { 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::SubmitAlgo { instrument, .. } | Self::SubmitPegBench { instrument, .. } | Self::SubmitLimitAuc { instrument, .. } | Self::SubmitMtlAuc { instrument, .. } - | Self::SubmitWhatIf { instrument, .. } | Self::SubmitLimitFractional { instrument, .. } - | Self::SubmitAdjustableStop { instrument, .. } | Self::SubmitEx { instrument, .. } | Self::SubmitBracket { instrument, .. } => Some(*instrument), } @@ -978,58 +722,38 @@ 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::SubmitAdaptive { price, .. } - | Self::SubmitAlgo { price, .. } - | Self::SubmitWhatIf { 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); } 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::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, .. + } => { + 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 } @@ -1568,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()); @@ -1597,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()); @@ -1757,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 @@ -1806,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); } @@ -1839,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!( @@ -1873,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 0aee62ec..42739b0c 100644 --- a/tests/ib_paper_compat/common.rs +++ b/tests/ib_paper_compat/common.rs @@ -508,50 +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::SubmitAdaptive { 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::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::SubmitAdjustableStop { order_id, .. } => *order_id, - OrderRequest::SubmitTrailingStopPctEx { order_id, .. } => *order_id, - OrderRequest::SubmitEx { 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 eba8d7c7..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) } @@ -729,7 +705,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) } @@ -738,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) } @@ -756,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) } @@ -765,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) } @@ -774,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) } @@ -783,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) } @@ -804,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(); @@ -865,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) } @@ -874,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) } @@ -883,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) } @@ -892,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) } @@ -901,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) } @@ -910,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) } @@ -919,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) } @@ -928,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) } @@ -937,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) } @@ -946,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) } @@ -955,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) } @@ -964,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) } @@ -973,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) } @@ -982,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) } @@ -992,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) } @@ -1002,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) } @@ -1012,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![ @@ -1030,8 +1008,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 +1019,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 +1030,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 +1041,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 +1052,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 +1063,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) } @@ -1117,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) } @@ -1137,8 +1121,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); @@ -1227,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(); @@ -1338,7 +1325,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) } @@ -1413,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; } @@ -1476,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 { @@ -1494,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; @@ -1624,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); @@ -1704,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); @@ -1777,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); @@ -1856,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); @@ -2013,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; } } @@ -2059,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); 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; } }