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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 98 additions & 59 deletions src/api/client/tests.rs

Large diffs are not rendered by default.

10 changes: 8 additions & 2 deletions src/bin/bench_market_order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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");
Expand Down
10 changes: 8 additions & 2 deletions src/bin/benchmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
175 changes: 88 additions & 87 deletions src/client_core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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" => {
Expand All @@ -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)),
};
Expand Down Expand Up @@ -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");
}
}

}
Loading