Skip to content

feat: add order close for 100% position-linked SL/TP and OCO#19

Open
vkbalakrishnan wants to merge 2 commits into
BitMEX:masterfrom
vkbalakrishnan:feat/order-close-100pct-sl-tp
Open

feat: add order close for 100% position-linked SL/TP and OCO#19
vkbalakrishnan wants to merge 2 commits into
BitMEX:masterfrom
vkbalakrishnan:feat/order-close-100pct-sl-tp

Conversation

@vkbalakrishnan

Copy link
Copy Markdown
Contributor

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 execInst includes Close and orderQty is 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/sell take qty as a required positional and build_order_body hardcoded "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

Invocation Order
--stop-px 50000 --trigger mark Stop-Loss 100% (Stop)
--tp-px 60000 --trigger last Take-Profit 100% (MarketIfTouched)
--stop-px … --tp-px … --trigger mark OCO bracket — legs linked via shared clOrdLinkID + contingencyType=OneCancelsTheOther, posted to /order/bulk
--side sell (no trigger) Immediate market close 100%
+ --stop-limit-px / --tp-limit-px Upgrades to StopLimit / LimitIfTouched
  • Core change: build_order_body's qty becomes Option<f64>, with orderQty inserted conditionally (mirroring the existing order amend). Buy/Sell pass Some(qty) — unchanged behaviour; close legs pass None.
  • --side and --trigger are required; --trigger is enforced via a validation error whenever --stop-px/--tp-px is set. sell closes a long, buy closes a short.
  • No new dependencies (OCO link IDs use std::time) and no new client methods (post already serializes any body, including /order/bulk).

Testing

  • 10 new unit tests over the pure close_exec_inst / build_close_plan builders (SL, TP, StopLimit, LimitIfTouched, OCO linkage, market close, qty omission).
  • 2 integration help-output tests.
  • Full suite: 108 unit + 17 integration pass; clippy --all-targets clean on new code.
  • Verified every payload via --validate dry-runs, e.g.:
$ bitmex order close XBTUSD --side sell --stop-px 50000 --tp-px 60000 --trigger mark --validate -o json
{"orders":[
  {"clOrdLinkID":"cli-oco-…","contingencyType":"OneCancelsTheOther","execInst":"MarkPrice,Close","ordType":"Stop","side":"Sell","stopPx":50000.0,"symbol":"XBTUSD",…},
  {"clOrdLinkID":"cli-oco-…","contingencyType":"OneCancelsTheOther","execInst":"MarkPrice,Close","ordType":"MarketIfTouched","side":"Sell","stopPx":60000.0,"symbol":"XBTUSD",…}
]}

Docs

Documented in agents/tool-catalog.json, CONTEXT.md, AGENTS.md, and README.md (replacing the prior order close-position placeholder).

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).
Comment thread src/cli/commands/order.rs

OrderCommand::Close {
symbol, side, stop_px, tp_px, trigger, stop_limit_px, tp_limit_px,
link_id, strategy, cl_ord_id, text, validate,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(),
    });
}

Comment thread src/cli/commands/order.rs
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(),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants