Skip to content
Merged
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
16 changes: 16 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,22 @@ bitmex execution trade-history --count 20 -o json
bitmex wallet balance -o json
```

### Hedge Mode (MultiWay)

Hedge Mode is an account-level setting that lets you hold independent Long and Short positions on the same contract (uncapped derivatives only). Switching is rejected while the account has open orders or isolated-margin positions.

```bash
bitmex account position-mode multiway --yes -o json # enable (alias: hedge)
bitmex account position-mode oneway --yes -o json # disable (netting)
```

Once enabled, tag each order leg with `--strategy Long` or `--strategy Short`. Each position carries a `strategy` field (`Long`/`Short`/`OneWay`); the account carries `positionMode`.

```bash
bitmex order buy XBTUSD 100 --price 50000 --strategy Long --yes -o json
bitmex order sell XBTUSD 100 --price 52000 --strategy Short --yes -o json
```

### Tick size and lot size alignment

Every instrument enforces a minimum price increment (`tickSize`) and minimum quantity increment (`lotSize`). Submitting a price or quantity that isn't a multiple of these will return a `400 Invalid price` or `400 Invalid quantity` error.
Expand Down
10 changes: 8 additions & 2 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ Use `bitmex market instrument --active -o json` to list all tradeable instrument
| Group | Auth | Description |
|-------|------|-------------|
| `market` | No | Instruments, quotes, trades, orderbook, funding, liquidations, stats |
| `order` | Yes | Place, amend, cancel orders |
| `position` | Yes | List positions, set leverage, manage margin |
| `order` | Yes | Place, amend, cancel orders (`--strategy Long/Short` for Hedge Mode legs) |
| `position` | Yes | List positions, set leverage, manage margin, switch Hedge Mode |
| `execution` | Yes | Fill history, trade history with PnL |
| `wallet` | Yes | Balances, deposits, withdrawals, transfers |
| `staking` | Yes | Staking positions and instruments |
Expand Down Expand Up @@ -190,6 +190,12 @@ bitmex order list --symbol XBTUSD -o json
bitmex position list -o json
bitmex position leverage XBTUSD 10 -o json

# Hedge Mode (MultiWay): independent Long + Short on one contract
bitmex account position-mode multiway --yes -o json # enable (alias: hedge)
bitmex account position-mode oneway --yes -o json # disable (netting)
bitmex order buy XBTUSD 100 --price 50000 --strategy Long --yes -o json
bitmex order sell XBTUSD 100 --price 52000 --strategy Short --yes -o json

# WebSocket (NDJSON to stdout)
bitmex ws trade:XBTUSD orderBookL2_25:XBTUSD
bitmex ws --auth position order execution
Expand Down
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ bitmex announce urgent
```bash
bitmex order buy XBTUSD 100 --price 50000 [--order-type Limit] [--validate]
bitmex order sell XBTUSD 100 --price 52000 [--tif GoodTillCancel]
bitmex order buy XBTUSD 100 --price 50000 --strategy Long # Hedge Mode leg
bitmex order sell XBTUSD 100 --price 52000 --strategy Short # Hedge Mode leg
bitmex order amend --order-id <id> --price 51000
bitmex order cancel --order-id <id>
bitmex order cancel-all [--symbol XBTUSD]
Expand All @@ -180,14 +182,37 @@ bitmex order list [--symbol XBTUSD]
### Positions (auth required)

```bash
bitmex position list [--symbol XBTUSD]
bitmex position list [--symbol XBTUSD] # table shows the `strategy` column
bitmex position leverage XBTUSD 10
bitmex position cross-leverage XBTUSD 5
bitmex position isolate XBTUSD --enabled
bitmex position risk-limit XBTUSD 20000000000
bitmex position transfer-margin XBTUSD 100000 # satoshis
```

### Hedge Mode (MultiWay) (auth required)

Hedge Mode lets you hold independent Long and Short positions on the same
contract instead of netting into one. It is an account-level setting and applies
to uncapped derivatives only. Switching is rejected while the account has open
orders or isolated-margin positions.

```bash
bitmex account position-mode multiway # enable Hedge Mode (alias: hedge)
bitmex account position-mode oneway # back to One-Way (netting)
bitmex position mode multiway # alias for `account position-mode`
```

Once enabled, tag each order leg with its strategy:

```bash
bitmex order buy XBTUSD 100 --price 50000 --strategy Long
bitmex order sell XBTUSD 100 --price 52000 --strategy Short
```

See [What is Hedge Mode](https://support.bitmex.com/hc/en-gb/articles/32985620308381-What-is-Hedge-Mode)
and [How traders use Hedge Mode](https://support.bitmex.com/hc/en-gb/articles/36691242524317-How-can-traders-use-Hedge-Mode-to-improve-their-trading-and-strategies).

### Execution history (auth required)

```bash
Expand Down
19 changes: 17 additions & 2 deletions agents/tool-catalog.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions src/cli/commands/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ use serde_json::{json, Value};

use crate::exchange::client::ExchangeClient;
use crate::cli::commands::helpers::build_query;
use crate::cli::commands::position_mode::{self, PositionModeArg};
use crate::config::Credentials;
use crate::errors::Result;
use crate::cli::output::CommandOutput;
use crate::AppContext;

#[derive(Debug, Subcommand)]
pub(crate) enum AccountCommand {
Expand Down Expand Up @@ -52,12 +54,26 @@ pub(crate) enum AccountCommand {
#[arg(long)]
currency: Option<String>,
},
/// Switch position mode: oneway (netting) or multiway (Hedge Mode).
///
/// Hedge Mode lets you hold independent Long and Short positions on the
/// same contract. The switch is rejected if the account has open orders or
/// isolated-margin positions.
PositionMode {
/// Target mode: `oneway` or `multiway` (alias `hedge`).
#[arg(value_enum)]
mode: PositionModeArg,
/// Paired/sub-account to switch (defaults to the calling account).
#[arg(long)]
target_account_id: Option<i64>,
},
}

pub(crate) async fn run(
cmd: AccountCommand,
client: &impl ExchangeClient,
creds: &Credentials,
ctx: &AppContext,
) -> Result<CommandOutput> {
match cmd {
AccountCommand::Me => {
Expand Down Expand Up @@ -122,5 +138,9 @@ pub(crate) async fn run(
let val = client.post("/user/marginingMode", &body, creds).await?;
Ok(CommandOutput::from_json(val))
}
AccountCommand::PositionMode {
mode,
target_account_id,
} => position_mode::run(client, creds, ctx, mode, target_account_id).await,
}
}
1 change: 1 addition & 0 deletions src/cli/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub(crate) mod notifications;
pub(crate) mod order;
pub(crate) mod porl;
pub(crate) mod position;
pub(crate) mod position_mode;
pub(crate) mod referral;
pub(crate) mod staking;
pub(crate) mod subaccount;
Expand Down
76 changes: 70 additions & 6 deletions src/cli/commands/order.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,30 @@
/// Order management commands: buy, sell, amend, cancel, cancel-all, cancel-after, close-position.
use clap::Subcommand;
use clap::{Subcommand, ValueEnum};
use serde_json::{json, Value};

/// Position strategy for an order leg under Hedge (MultiWay) mode.
///
/// In One-Way mode leave this unset (or `oneway`); in Hedge Mode pass `long`
/// or `short` to target the corresponding position bucket.
#[derive(Debug, Clone, Copy, ValueEnum)]
pub(crate) enum OrderStrategy {
#[value(name = "oneway", alias = "one-way")]
OneWay,
Long,
Short,
}

impl OrderStrategy {
/// The exact string the BitMEX API expects for the `strategy` field.
fn as_api(self) -> &'static str {
match self {
OrderStrategy::OneWay => "OneWay",
OrderStrategy::Long => "Long",
OrderStrategy::Short => "Short",
}
}
}

use crate::exchange::client::ExchangeClient;
use crate::cli::commands::helpers::confirm_destructive;
use crate::config::Credentials;
Expand All @@ -26,6 +49,10 @@ pub(crate) enum OrderCommand {
/// Execution instructions (e.g. ParticipateDoNotInitiate, ReduceOnly).
#[arg(long)]
exec_inst: Option<String>,
/// Position strategy for Hedge Mode: `long` or `short`. Requires hedge
/// mode enabled; omit (or `oneway`) for One-Way accounts.
#[arg(long, value_enum)]
strategy: Option<OrderStrategy>,
#[arg(long)]
cl_ord_id: Option<String>,
#[arg(long)]
Expand All @@ -48,6 +75,10 @@ pub(crate) enum OrderCommand {
tif: Option<String>,
#[arg(long)]
exec_inst: Option<String>,
/// Position strategy for Hedge Mode: `long` or `short`. Requires hedge
/// mode enabled; omit (or `oneway`) for One-Way accounts.
#[arg(long, value_enum)]
strategy: Option<OrderStrategy>,
#[arg(long)]
cl_ord_id: Option<String>,
#[arg(long)]
Expand Down Expand Up @@ -108,6 +139,7 @@ pub(crate) enum OrderCommand {
},
}

#[allow(clippy::too_many_arguments)]
fn build_order_body(
symbol: &str,
side: &str,
Expand All @@ -117,6 +149,7 @@ fn build_order_body(
stop_px: Option<f64>,
tif: Option<String>,
exec_inst: Option<String>,
strategy: Option<String>,
cl_ord_id: Option<String>,
text: Option<String>,
) -> Value {
Expand All @@ -130,6 +163,7 @@ fn build_order_body(
if let Some(p) = stop_px { body["stopPx"] = json!(p); }
if let Some(t) = tif { body["timeInForce"] = Value::String(t); }
if let Some(e) = exec_inst { body["execInst"] = Value::String(e); }
if let Some(s) = strategy { body["strategy"] = Value::String(s); }
if let Some(c) = cl_ord_id { body["clOrdID"] = Value::String(c); }
body["text"] = Value::String(text.unwrap_or_else(|| "Submitted via CLI.".to_string()));
body
Expand All @@ -143,9 +177,9 @@ pub(crate) async fn run(
) -> Result<CommandOutput> {
match cmd {
OrderCommand::Buy {
symbol, qty, order_type, price, stop_px, tif, exec_inst, cl_ord_id, text, validate,
symbol, qty, order_type, price, stop_px, tif, exec_inst, strategy, cl_ord_id, text, validate,
} => {
let body = build_order_body(&symbol, "Buy", qty, &order_type, price, stop_px, tif, exec_inst, cl_ord_id, text);
let body = build_order_body(&symbol, "Buy", qty, &order_type, price, stop_px, tif, exec_inst, strategy.map(|s| s.as_api().to_string()), cl_ord_id, text);
if validate {
return Ok(CommandOutput::from_json(body));
}
Expand All @@ -157,9 +191,9 @@ pub(crate) async fn run(
}

OrderCommand::Sell {
symbol, qty, order_type, price, stop_px, tif, exec_inst, cl_ord_id, text, validate,
symbol, qty, order_type, price, stop_px, tif, exec_inst, strategy, cl_ord_id, text, validate,
} => {
let body = build_order_body(&symbol, "Sell", qty, &order_type, price, stop_px, tif, exec_inst, cl_ord_id, text);
let body = build_order_body(&symbol, "Sell", qty, &order_type, price, stop_px, tif, exec_inst, strategy.map(|s| s.as_api().to_string()), cl_ord_id, text);
if validate {
return Ok(CommandOutput::from_json(body));
}
Expand Down Expand Up @@ -229,10 +263,40 @@ pub(crate) async fn run(
Ok(CommandOutput::builder()
.data(val)
.columns(&[
"orderID", "symbol", "side", "orderQty", "price",
"orderID", "symbol", "side", "strategy", "orderQty", "price",
"ordType", "ordStatus", "avgPx", "leavesQty", "timestamp",
])
.build())
}
}
}

#[cfg(test)]
mod tests {
use super::*;

fn sample_body(strategy: Option<String>) -> Value {
build_order_body(
"XBTUSD", "Buy", 100.0, "Limit", Some(50000.0), None, None, None, strategy, None, None,
)
}

#[test]
fn order_strategy_maps_to_exact_api_strings() {
assert_eq!(OrderStrategy::OneWay.as_api(), "OneWay");
assert_eq!(OrderStrategy::Long.as_api(), "Long");
assert_eq!(OrderStrategy::Short.as_api(), "Short");
}

#[test]
fn build_order_body_includes_strategy_when_set() {
let body = sample_body(Some(OrderStrategy::Long.as_api().to_string()));
assert_eq!(body["strategy"], "Long");
}

#[test]
fn build_order_body_omits_strategy_when_unset() {
let body = sample_body(None);
assert!(body.get("strategy").is_none());
}
}
20 changes: 19 additions & 1 deletion src/cli/commands/position.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use serde_json::json;

use crate::exchange::client::ExchangeClient;
use crate::cli::commands::helpers::{build_query, confirm_destructive};
use crate::cli::commands::position_mode::{self, PositionModeArg};
use crate::config::Credentials;
use crate::errors::Result;
use crate::cli::output::CommandOutput;
Expand Down Expand Up @@ -47,6 +48,18 @@ pub(crate) enum PositionCommand {
/// Amount in satoshis.
amount: i64,
},
/// Switch position mode: oneway (netting) or multiway (Hedge Mode).
///
/// Alias for `account position-mode`. Hedge Mode lets you hold independent
/// Long and Short positions on the same contract.
Mode {
/// Target mode: `oneway` or `multiway` (alias `hedge`).
#[arg(value_enum)]
mode: PositionModeArg,
/// Paired/sub-account to switch (defaults to the calling account).
#[arg(long)]
target_account_id: Option<i64>,
},
}

pub(crate) async fn run(
Expand All @@ -70,7 +83,7 @@ pub(crate) async fn run(
Ok(CommandOutput::builder()
.data(val)
.columns(&[
"symbol", "currentQty", "avgEntryPrice", "markPrice",
"symbol", "strategy", "currentQty", "avgEntryPrice", "markPrice",
"liquidationPrice", "unrealisedPnl", "realisedPnl",
"leverage", "crossMargin", "marginCallPrice",
])
Expand Down Expand Up @@ -129,5 +142,10 @@ pub(crate) async fn run(
.await?;
Ok(CommandOutput::from_json(val))
}

PositionCommand::Mode {
mode,
target_account_id,
} => position_mode::run(client, creds, ctx, mode, target_account_id).await,
}
}
Loading