feat: add order close for 100% position-linked SL/TP and OCO#19
feat: add order close for 100% position-linked SL/TP and OCO#19vkbalakrishnan wants to merge 2 commits into
order close for 100% position-linked SL/TP and OCO#19Conversation
Place 100%-of-position Stop-Loss, Take-Profit, an OCO bracket, or an
immediate market close. Every leg omits `orderQty` so the order tracks the
full position dynamically — BitMEX renders it as SL (100%) / TP (100%) with
no resync window, unlike a fixed-size stop.
- `--stop-px --trigger <t>` -> Stop (StopLimit with --stop-limit-px)
- `--tp-px --trigger <t>` -> MarketIfTouched (LimitIfTouched with --tp-limit-px)
- `--stop-px --tp-px --trigger <t>` -> OCO bracket (linked via clOrdLinkID +
contingencyType, posted to /order/bulk)
- `--side` only -> Market close
`--side` and `--trigger` are required; `--trigger` is enforced whenever a
stop/TP trigger is set. Reuses `build_order_body` (qty made Option<f64>,
mirroring `amend`); no new dependencies or client methods.
Adds 10 unit tests over the pure body/plan builders and 2 integration
help-output tests. Documents the command in tool-catalog.json, CONTEXT.md,
AGENTS.md, and README.md (replacing the prior `close-position` placeholder).
|
|
||
| OrderCommand::Close { | ||
| symbol, side, stop_px, tp_px, trigger, stop_limit_px, tp_limit_px, | ||
| link_id, strategy, cl_ord_id, text, validate, |
There was a problem hiding this comment.
Missing validation for --stop-limit-px without --stop-px (and --tp-limit-px without --tp-px). Right now a user who passes --stop-limit-px 49900 without a trigger price gets ordType: StopLimit with no stopPx in the body, and the API rejects it with an opaque HTTP error instead of a clear local message. Suggest adding two guards here alongside the existing trigger check:
if stop_limit_px.is_some() && stop_px.is_none() {
return Err(crate::errors::BitmexError::Validation {
message: "--stop-limit-px requires --stop-px".into(),
});
}
if tp_limit_px.is_some() && tp_px.is_none() {
return Err(crate::errors::BitmexError::Validation {
message: "--tp-limit-px requires --tp-px".into(),
});
}| build_order_body( | ||
| symbol, side, None, ord_type, tp_limit_px, tp_px, None, | ||
| Some(exec_inst.to_string()), strategy.clone(), cl_ord_id, text.clone(), | ||
| ) |
There was a problem hiding this comment.
cl_ord_id is silently dropped for OCO brackets — both legs call sl_leg(None) / tp_leg(None) so the user-supplied value is never threaded in. The arg help string says "single-order placements only" but there's no guard, so passing --cl-ord-id foo with both --stop-px and --tp-px silently ignores it.
Either return a validation error when cl_ord_id.is_some() and it's the OCO arm, or at minimum make the help text explicit: e.g. Ignored for OCO brackets (both --stop-px and --tp-px set).
There was a problem hiding this comment.
Fixed. Added a validation guard in the Close arm of run() that returns BitmexError::Validation when cl_ord_id.is_some() && stop_px.is_some() && tp_px.is_some() — same pattern as the existing trigger check. The None passed to both legs in the OCO arm was intentional (two orders can't share a clOrdID), so the guard is the right enforcement point rather than threading the value through. Updated the help text to explain why, and added an integration test that verifies the error fires before any network call.
Two orders in an OCO bracket cannot share a clOrdID; passing --cl-ord-id with both --stop-px and --tp-px previously silently dropped the value. Now returns a BitmexError::Validation before any network call. Tightened the help text to explain why, and added an integration test.
What
Adds
bitmex order close— a subcommand for placing 100%-of-position Stop-Loss, Take-Profit, OCO bracket, or immediate market-close orders.Why
BitMEX renders a stop/TP as SL (100%) / TP (100%) — an order that dynamically closes the entire position at trigger with no resync — only when
execInstincludesCloseandorderQtyis omitted. The instant a quantity is specified it becomes a fixed-size stop that a bot must keep resynced as the position changes: strictly inferior.The CLI couldn't produce the qty-null payload.
order buy/selltakeqtyas a required positional andbuild_order_bodyhardcoded"orderQty": qty, so it was always serialized. This blocked riskbot (and manual use) from placing the clean 100% SL/TP the BitMEX UI creates by hand.How
--stop-px 50000 --trigger markStop)--tp-px 60000 --trigger lastMarketIfTouched)--stop-px … --tp-px … --trigger markclOrdLinkID+contingencyType=OneCancelsTheOther, posted to/order/bulk--side sell(no trigger)+ --stop-limit-px/--tp-limit-pxStopLimit/LimitIfTouchedbuild_order_body'sqtybecomesOption<f64>, withorderQtyinserted conditionally (mirroring the existingorder amend). Buy/Sell passSome(qty)— unchanged behaviour; close legs passNone.--sideand--triggerare required;--triggeris enforced via a validation error whenever--stop-px/--tp-pxis set.sellcloses a long,buycloses a short.std::time) and no new client methods (postalready serializes any body, including/order/bulk).Testing
close_exec_inst/build_close_planbuilders (SL, TP, StopLimit, LimitIfTouched, OCO linkage, market close, qty omission).clippy --all-targetsclean on new code.--validatedry-runs, e.g.:Docs
Documented in
agents/tool-catalog.json,CONTEXT.md,AGENTS.md, andREADME.md(replacing the priororder close-positionplaceholder).