use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use ustr::Ustr;
use crate::{
common::{
enums::{
BybitBboSideType, BybitCancelType, BybitCreateType, BybitExecType, BybitMarketUnit,
BybitOrderSide, BybitOrderStatus, BybitOrderType, BybitPositionIdx, BybitPositionSide,
BybitPositionStatus, BybitProductType, BybitSmpType, BybitStopOrderType,
BybitTimeInForce, BybitTpSlMode, BybitTriggerDirection, BybitTriggerType,
BybitWsOrderRequestOp,
},
parse::{
deserialize_decimal_or_zero, deserialize_optional_decimal_or_zero,
deserialize_optional_decimal_str,
},
},
websocket::enums::BybitWsOperation,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BybitSubscription {
pub op: BybitWsOperation,
pub args: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub req_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BybitAuthRequest {
pub op: BybitWsOperation,
pub args: Vec<serde_json::Value>,
}
#[derive(Debug, Clone)]
pub enum BybitWsFrame {
Auth(BybitWsAuthResponse),
Subscription(BybitWsSubscriptionMsg),
OrderResponse(BybitWsOrderResponse),
ErrorResponse(BybitWsResponse),
Orderbook(BybitWsOrderbookDepthMsg),
Trade(BybitWsTradeMsg),
Kline(BybitWsKlineMsg),
TickerLinear(BybitWsTickerLinearMsg),
TickerOption(BybitWsTickerOptionMsg),
AccountOrder(BybitWsAccountOrderMsg),
AccountExecution(BybitWsAccountExecutionMsg),
AccountExecutionFast(BybitWsAccountExecutionFastMsg),
AccountWallet(BybitWsAccountWalletMsg),
AccountPosition(BybitWsAccountPositionMsg),
Unknown(Value),
Reconnected,
}
#[derive(Debug, Clone)]
pub enum BybitWsMessage {
Auth(BybitWsAuthResponse),
OrderResponse(BybitWsOrderResponse),
Orderbook(BybitWsOrderbookDepthMsg),
Trade(BybitWsTradeMsg),
Kline(BybitWsKlineMsg),
TickerLinear(BybitWsTickerLinearMsg),
TickerOption(BybitWsTickerOptionMsg),
AccountOrder(BybitWsAccountOrderMsg),
AccountExecution(BybitWsAccountExecutionMsg),
AccountExecutionFast(BybitWsAccountExecutionFastMsg),
AccountWallet(BybitWsAccountWalletMsg),
AccountPosition(BybitWsAccountPositionMsg),
Error(BybitWebSocketError),
Reconnected,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.bybit")
)]
pub struct BybitWebSocketError {
pub code: i64,
pub message: String,
#[serde(default)]
pub conn_id: Option<String>,
#[serde(default)]
pub topic: Option<String>,
#[serde(default)]
pub req_id: Option<String>,
}
impl BybitWebSocketError {
#[must_use]
pub fn new(code: i64, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
conn_id: None,
topic: None,
req_id: None,
}
}
#[must_use]
pub fn from_response(response: &BybitWsResponse) -> Self {
let message = response.ret_msg.clone().unwrap_or_else(|| {
let mut parts = vec![];
if let Some(op) = &response.op {
parts.push(format!("op={op}"));
}
if let Some(topic) = &response.topic {
parts.push(format!("topic={topic}"));
}
if let Some(success) = response.success {
parts.push(format!("success={success}"));
}
if parts.is_empty() {
"Bybit websocket error (no error message provided)".to_string()
} else {
format!("Bybit websocket error: {}", parts.join(", "))
}
});
Self {
code: response.ret_code.unwrap_or_default(),
message,
conn_id: response.conn_id.clone(),
topic: response.topic.map(|t| t.to_string()),
req_id: response.req_id.clone(),
}
}
#[must_use]
pub fn from_message(message: impl Into<String>) -> Self {
Self::new(-1, message)
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsRequest<T> {
#[serde(skip_serializing_if = "Option::is_none")]
pub req_id: Option<String>,
pub op: BybitWsOrderRequestOp,
pub header: BybitWsHeader,
pub args: Vec<T>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
pub struct BybitWsHeader {
pub x_bapi_timestamp: String,
#[serde(rename = "Referer", skip_serializing_if = "Option::is_none")]
pub referer: Option<String>,
}
impl BybitWsHeader {
#[must_use]
pub fn now() -> Self {
Self::with_referer(None)
}
#[must_use]
pub fn with_referer(referer: Option<String>) -> Self {
use nautilus_core::time::get_atomic_clock_realtime;
Self {
x_bapi_timestamp: get_atomic_clock_realtime().get_time_ms().to_string(),
referer,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsPlaceOrderParams {
pub category: BybitProductType,
pub symbol: Ustr,
pub side: BybitOrderSide,
pub order_type: BybitOrderType,
pub qty: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_leverage: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub market_unit: Option<BybitMarketUnit>,
#[serde(skip_serializing_if = "Option::is_none")]
pub price: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub time_in_force: Option<BybitTimeInForce>,
#[serde(skip_serializing_if = "Option::is_none")]
pub order_link_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reduce_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub close_on_trigger: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger_price: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger_by: Option<BybitTriggerType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger_direction: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tpsl_mode: Option<BybitTpSlMode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub take_profit: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_loss: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tp_trigger_by: Option<BybitTriggerType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sl_trigger_by: Option<BybitTriggerType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sl_trigger_price: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tp_trigger_price: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sl_order_type: Option<BybitOrderType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tp_order_type: Option<BybitOrderType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sl_limit_price: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tp_limit_price: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub order_iv: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mmp: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub position_idx: Option<BybitPositionIdx>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bbo_side_type: Option<BybitBboSideType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bbo_level: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsAmendOrderParams {
pub category: BybitProductType,
pub symbol: Ustr,
#[serde(skip_serializing_if = "Option::is_none")]
pub order_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub order_link_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub qty: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub price: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger_price: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub take_profit: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_loss: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tp_trigger_by: Option<BybitTriggerType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sl_trigger_by: Option<BybitTriggerType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub order_iv: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsCancelOrderParams {
pub category: BybitProductType,
pub symbol: Ustr,
#[serde(skip_serializing_if = "Option::is_none")]
pub order_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub order_link_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsBatchCancelItem {
pub symbol: Ustr,
#[serde(skip_serializing_if = "Option::is_none")]
pub order_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub order_link_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BybitWsBatchCancelOrderArgs {
pub category: BybitProductType,
pub request: Vec<BybitWsBatchCancelItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsBatchPlaceItem {
pub symbol: Ustr,
pub side: BybitOrderSide,
pub order_type: BybitOrderType,
pub qty: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_leverage: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub market_unit: Option<BybitMarketUnit>,
#[serde(skip_serializing_if = "Option::is_none")]
pub price: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub time_in_force: Option<BybitTimeInForce>,
#[serde(skip_serializing_if = "Option::is_none")]
pub order_link_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reduce_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub close_on_trigger: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger_price: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger_by: Option<BybitTriggerType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger_direction: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tpsl_mode: Option<BybitTpSlMode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub take_profit: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_loss: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tp_trigger_by: Option<BybitTriggerType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sl_trigger_by: Option<BybitTriggerType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sl_trigger_price: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tp_trigger_price: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sl_order_type: Option<BybitOrderType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tp_order_type: Option<BybitOrderType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sl_limit_price: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tp_limit_price: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub order_iv: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mmp: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub position_idx: Option<BybitPositionIdx>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bbo_side_type: Option<BybitBboSideType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bbo_level: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BybitWsBatchPlaceOrderArgs {
pub category: BybitProductType,
pub request: Vec<BybitWsBatchPlaceItem>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BybitWsSubscriptionMsg {
pub success: bool,
pub op: BybitWsOperation,
#[serde(default)]
pub conn_id: Option<String>,
#[serde(default)]
pub req_id: Option<String>,
#[serde(default)]
pub ret_msg: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BybitWsResponse {
#[serde(default)]
pub op: Option<BybitWsOperation>,
#[serde(default)]
pub topic: Option<Ustr>,
#[serde(default)]
pub success: Option<bool>,
#[serde(default)]
pub conn_id: Option<String>,
#[serde(default)]
pub req_id: Option<String>,
#[serde(default)]
pub ret_code: Option<i64>,
#[serde(default)]
pub ret_msg: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsOrderResponse {
pub op: Ustr,
#[serde(default)]
pub conn_id: Option<String>,
pub ret_code: i64,
pub ret_msg: String,
#[serde(default)]
pub data: Value,
#[serde(default)]
pub req_id: Option<String>,
#[serde(default)]
pub header: Option<Value>,
#[serde(default)]
pub ret_ext_info: Option<Value>,
}
impl BybitWsOrderResponse {
#[must_use]
pub fn extract_batch_errors(&self) -> Vec<BybitBatchOrderError> {
self.ret_ext_info
.as_ref()
.and_then(|ext| ext.get("list"))
.and_then(|list| list.as_array())
.map(|arr| {
arr.iter()
.filter_map(|item| {
let code = item.get("code")?.as_i64()?;
let msg = item.get("msg")?.as_str()?.to_string();
Some(BybitBatchOrderError { code, msg })
})
.collect()
})
.unwrap_or_default()
}
}
#[derive(Clone, Debug)]
pub struct BybitBatchOrderError {
pub code: i64,
pub msg: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsAuthResponse {
pub op: BybitWsOperation,
#[serde(default)]
pub conn_id: Option<String>,
#[serde(default)]
pub ret_code: Option<i64>,
#[serde(default)]
pub ret_msg: Option<String>,
#[serde(default)]
pub success: Option<bool>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsKline {
pub start: i64,
pub end: i64,
pub interval: Ustr,
pub open: String,
pub close: String,
pub high: String,
pub low: String,
pub volume: String,
pub turnover: String,
pub confirm: bool,
pub timestamp: i64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsKlineMsg {
pub topic: Ustr,
pub ts: i64,
#[serde(rename = "type")]
pub msg_type: Ustr,
pub data: Vec<BybitWsKline>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BybitWsOrderbookDepth {
pub s: Ustr,
pub b: Vec<Vec<String>>,
pub a: Vec<Vec<String>>,
pub u: i64,
pub seq: i64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsOrderbookDepthMsg {
pub topic: Ustr,
#[serde(rename = "type")]
pub msg_type: Ustr,
pub ts: i64,
pub data: BybitWsOrderbookDepth,
#[serde(default)]
pub cts: Option<i64>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsTickerLinear {
pub symbol: Ustr,
#[serde(default)]
pub tick_direction: Option<String>,
#[serde(default)]
pub price24h_pcnt: Option<String>,
#[serde(default)]
pub last_price: Option<String>,
#[serde(default)]
pub prev_price24h: Option<String>,
#[serde(default)]
pub high_price24h: Option<String>,
#[serde(default)]
pub low_price24h: Option<String>,
#[serde(default)]
pub prev_price1h: Option<String>,
#[serde(default)]
pub mark_price: Option<String>,
#[serde(default)]
pub index_price: Option<String>,
#[serde(default)]
pub open_interest: Option<String>,
#[serde(default)]
pub open_interest_value: Option<String>,
#[serde(default)]
pub turnover24h: Option<String>,
#[serde(default)]
pub volume24h: Option<String>,
#[serde(default)]
pub next_funding_time: Option<String>,
#[serde(default)]
pub funding_rate: Option<String>,
#[serde(default)]
pub bid1_price: Option<String>,
#[serde(default)]
pub bid1_size: Option<String>,
#[serde(default)]
pub ask1_price: Option<String>,
#[serde(default)]
pub ask1_size: Option<String>,
#[serde(default)]
pub funding_interval_hour: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsTickerLinearMsg {
pub topic: Ustr,
#[serde(rename = "type")]
pub msg_type: Ustr,
pub ts: i64,
#[serde(default)]
pub cs: Option<i64>,
pub data: BybitWsTickerLinear,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsTickerOption {
pub symbol: Ustr,
pub bid_price: String,
pub bid_size: String,
pub bid_iv: String,
pub ask_price: String,
pub ask_size: String,
pub ask_iv: String,
pub last_price: String,
pub high_price24h: String,
pub low_price24h: String,
pub mark_price: String,
pub index_price: String,
pub mark_price_iv: String,
pub underlying_price: String,
pub open_interest: String,
pub turnover24h: String,
pub volume24h: String,
pub total_volume: String,
pub total_turnover: String,
pub delta: String,
pub gamma: String,
pub vega: String,
pub theta: String,
pub predicted_delivery_price: String,
pub change24h: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsTickerOptionMsg {
#[serde(default)]
pub id: Option<String>,
pub topic: Ustr,
#[serde(rename = "type")]
pub msg_type: Ustr,
pub ts: i64,
pub data: BybitWsTickerOption,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BybitWsTrade {
#[serde(rename = "T")]
pub t: i64,
#[serde(rename = "s")]
pub s: Ustr,
#[serde(rename = "S")]
pub taker_side: BybitOrderSide,
#[serde(rename = "v")]
pub v: String,
#[serde(rename = "p")]
pub p: String,
#[serde(rename = "i")]
pub i: String,
#[serde(rename = "BT")]
pub bt: bool,
#[serde(rename = "L")]
#[serde(default)]
pub l: Option<String>,
#[serde(rename = "id")]
#[serde(default)]
pub id: Option<Ustr>,
#[serde(rename = "mP")]
#[serde(default)]
pub m_p: Option<String>,
#[serde(rename = "iP")]
#[serde(default)]
pub i_p: Option<String>,
#[serde(rename = "mIv")]
#[serde(default)]
pub m_iv: Option<String>,
#[serde(rename = "iv")]
#[serde(default)]
pub iv: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsTradeMsg {
pub topic: Ustr,
#[serde(rename = "type")]
pub msg_type: Ustr,
pub ts: i64,
pub data: Vec<BybitWsTrade>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsAccountOrder {
pub category: BybitProductType,
pub symbol: Ustr,
pub order_id: Ustr,
pub side: BybitOrderSide,
pub order_type: BybitOrderType,
pub cancel_type: BybitCancelType,
pub price: String,
pub qty: String,
pub order_iv: String,
pub time_in_force: BybitTimeInForce,
pub order_status: BybitOrderStatus,
pub order_link_id: Ustr,
pub last_price_on_created: Ustr,
pub reduce_only: bool,
pub leaves_qty: String,
pub leaves_value: String,
pub cum_exec_qty: String,
pub cum_exec_value: String,
pub avg_price: String,
pub block_trade_id: Ustr,
pub position_idx: i32,
pub cum_exec_fee: String,
pub created_time: String,
pub updated_time: String,
pub reject_reason: Ustr,
pub trigger_price: String,
pub take_profit: String,
pub stop_loss: String,
pub tp_trigger_by: BybitTriggerType,
pub sl_trigger_by: BybitTriggerType,
pub tp_limit_price: String,
pub sl_limit_price: String,
pub close_on_trigger: bool,
pub place_type: Ustr,
pub smp_type: BybitSmpType,
pub smp_group: i32,
pub smp_order_id: Ustr,
pub fee_currency: Ustr,
pub trigger_by: BybitTriggerType,
pub stop_order_type: BybitStopOrderType,
pub trigger_direction: BybitTriggerDirection,
#[serde(default)]
pub tpsl_mode: Option<BybitTpSlMode>,
#[serde(default)]
pub create_type: Option<BybitCreateType>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsAccountOrderMsg {
pub topic: Ustr,
pub id: String,
pub creation_time: i64,
pub data: Vec<BybitWsAccountOrder>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsAccountExecution {
pub category: BybitProductType,
pub symbol: Ustr,
pub exec_fee: String,
pub exec_id: String,
pub exec_price: String,
pub exec_qty: String,
pub exec_type: BybitExecType,
pub exec_value: String,
pub is_maker: bool,
pub fee_rate: String,
pub trade_iv: String,
pub mark_iv: String,
pub block_trade_id: Ustr,
pub mark_price: String,
pub index_price: String,
pub underlying_price: String,
pub leaves_qty: String,
pub order_id: Ustr,
pub order_link_id: Ustr,
pub order_price: String,
pub order_qty: String,
pub order_type: BybitOrderType,
pub side: BybitOrderSide,
pub exec_time: String,
pub is_leverage: String,
pub closed_size: String,
pub seq: i64,
pub stop_order_type: BybitStopOrderType,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsAccountExecutionMsg {
pub topic: Ustr,
pub id: String,
pub creation_time: i64,
pub data: Vec<BybitWsAccountExecution>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsAccountExecutionFast {
pub category: BybitProductType,
pub symbol: Ustr,
pub exec_id: String,
pub exec_price: String,
pub exec_qty: String,
pub order_id: Ustr,
pub order_link_id: Ustr,
pub side: BybitOrderSide,
pub exec_time: String,
pub is_maker: bool,
#[serde(default = "default_ws_execution_fast_seq")]
pub seq: i64,
}
const fn default_ws_execution_fast_seq() -> i64 {
-1
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsAccountExecutionFastMsg {
pub topic: Ustr,
#[serde(default)]
pub id: String,
pub creation_time: i64,
pub data: Vec<BybitWsAccountExecutionFast>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsAccountWalletCoin {
pub coin: Ustr,
#[serde(deserialize_with = "deserialize_decimal_or_zero")]
pub wallet_balance: Decimal,
pub available_to_withdraw: String,
pub available_to_borrow: String,
pub accrued_interest: String,
#[serde(
default,
rename = "totalOrderIM",
deserialize_with = "deserialize_optional_decimal_or_zero"
)]
pub total_order_im: Decimal,
#[serde(
default,
rename = "totalPositionIM",
deserialize_with = "deserialize_optional_decimal_or_zero"
)]
pub total_position_im: Decimal,
#[serde(default, rename = "totalPositionMM")]
pub total_position_mm: Option<String>,
pub equity: String,
#[serde(default, deserialize_with = "deserialize_optional_decimal_or_zero")]
pub spot_borrow: Decimal,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsAccountWallet {
pub total_wallet_balance: String,
pub total_equity: String,
pub total_available_balance: String,
pub total_margin_balance: String,
pub total_initial_margin: String,
pub total_maintenance_margin: String,
#[serde(rename = "accountIMRate")]
pub account_im_rate: String,
#[serde(rename = "accountMMRate")]
pub account_mm_rate: String,
#[serde(rename = "accountLTV")]
pub account_ltv: String,
pub coin: Vec<BybitWsAccountWalletCoin>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsAccountWalletMsg {
pub topic: Ustr,
pub id: String,
pub creation_time: i64,
pub data: Vec<BybitWsAccountWallet>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsAccountPosition {
pub category: BybitProductType,
pub symbol: Ustr,
pub side: BybitPositionSide,
pub size: String,
pub position_idx: i32,
pub trade_mode: i32,
pub position_value: String,
pub risk_id: i64,
pub risk_limit_value: String,
#[serde(deserialize_with = "deserialize_optional_decimal_str")]
pub entry_price: Option<Decimal>,
pub mark_price: String,
pub leverage: String,
pub position_balance: String,
pub auto_add_margin: i32,
#[serde(rename = "positionIM")]
pub position_im: String,
#[serde(rename = "positionIMByMp")]
pub position_im_by_mp: String,
#[serde(rename = "positionMM")]
pub position_mm: String,
#[serde(rename = "positionMMByMp")]
pub position_mm_by_mp: String,
pub liq_price: String,
pub bust_price: String,
pub tpsl_mode: BybitTpSlMode,
pub take_profit: String,
pub stop_loss: String,
pub trailing_stop: String,
pub unrealised_pnl: String,
pub session_avg_price: String,
pub cur_realised_pnl: String,
pub cum_realised_pnl: String,
pub position_status: BybitPositionStatus,
pub adl_rank_indicator: i32,
pub created_time: String,
pub updated_time: String,
#[serde(default = "default_ws_position_seq")]
pub seq: i64,
#[serde(default)]
pub is_reduce_only: bool,
#[serde(default)]
pub mmr_sys_updated_time: String,
#[serde(default)]
pub leverage_sys_updated_time: String,
#[serde(default)]
pub open_time: i64,
}
const fn default_ws_position_seq() -> i64 {
-1
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BybitWsAccountPositionMsg {
pub topic: Ustr,
pub id: String,
pub creation_time: i64,
pub data: Vec<BybitWsAccountPosition>,
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
use crate::common::testing::load_test_json;
#[rstest]
fn deserialize_account_execution_fast_msg() {
let json = load_test_json("ws_account_execution_fast.json");
let msg: BybitWsAccountExecutionFastMsg = serde_json::from_str(&json).unwrap();
assert_eq!(msg.id, "");
assert_eq!(msg.creation_time, 1_716_800_399_338);
assert_eq!(msg.data.len(), 1);
let exec = &msg.data[0];
assert_eq!(exec.category, BybitProductType::Linear);
assert_eq!(exec.symbol, Ustr::from("ICPUSDT"));
assert_eq!(exec.exec_id, "3510f361-0add-5c7b-a2e7-9679810944fc");
assert_eq!(exec.exec_price, "12.015");
assert_eq!(exec.exec_qty, "3000");
assert_eq!(
exec.order_id,
Ustr::from("443d63fa-b4c3-4297-b7b1-23bca88b04dc")
);
assert_eq!(exec.order_link_id, Ustr::from("test-order-link-001"));
assert_eq!(exec.side, BybitOrderSide::Sell);
assert!(!exec.is_maker);
assert_eq!(exec.exec_time, "1716800399334");
assert_eq!(exec.seq, 34_771_365_464);
}
#[rstest]
fn deserialize_account_execution_fast_msg_accepts_envelope_id() {
let json = load_test_json("ws_account_execution_fast_envelope_id.json");
let msg: BybitWsAccountExecutionFastMsg = serde_json::from_str(&json).unwrap();
assert_eq!(msg.id, "fast-1");
assert!(msg.data.is_empty());
}
#[rstest]
fn deserialize_account_position_with_open_time() {
let json = load_test_json("ws_account_position_with_open_time.json");
let position: BybitWsAccountPosition = serde_json::from_str(&json).unwrap();
assert_eq!(position.open_time, 1_700_000_000_123);
}
#[rstest]
fn serialize_place_params_includes_order_iv_when_set() {
let params = BybitWsPlaceOrderParams {
category: BybitProductType::Option,
symbol: Ustr::from("BTC-30JUN25-100000-C"),
side: BybitOrderSide::Buy,
order_type: BybitOrderType::Limit,
qty: "0.1".to_string(),
is_leverage: None,
market_unit: None,
price: Some("500".to_string()),
time_in_force: Some(BybitTimeInForce::Gtc),
order_link_id: Some("test-1".to_string()),
reduce_only: None,
close_on_trigger: None,
trigger_price: None,
trigger_by: None,
trigger_direction: None,
tpsl_mode: None,
take_profit: None,
stop_loss: None,
tp_trigger_by: None,
sl_trigger_by: None,
sl_trigger_price: None,
tp_trigger_price: None,
sl_order_type: None,
tp_order_type: None,
sl_limit_price: None,
tp_limit_price: None,
order_iv: Some("0.80".to_string()),
mmp: Some(true),
position_idx: None,
bbo_side_type: None,
bbo_level: None,
};
let json = serde_json::to_string(¶ms).unwrap();
assert!(json.contains("\"orderIv\":\"0.80\""));
assert!(json.contains("\"mmp\":true"));
}
#[rstest]
fn serialize_place_params_omits_order_iv_when_none() {
let params = BybitWsPlaceOrderParams {
category: BybitProductType::Linear,
symbol: Ustr::from("BTCUSDT"),
side: BybitOrderSide::Buy,
order_type: BybitOrderType::Limit,
qty: "0.01".to_string(),
is_leverage: None,
market_unit: None,
price: Some("50000".to_string()),
time_in_force: Some(BybitTimeInForce::Gtc),
order_link_id: None,
reduce_only: None,
close_on_trigger: None,
trigger_price: None,
trigger_by: None,
trigger_direction: None,
tpsl_mode: None,
take_profit: None,
stop_loss: None,
tp_trigger_by: None,
sl_trigger_by: None,
sl_trigger_price: None,
tp_trigger_price: None,
sl_order_type: None,
tp_order_type: None,
sl_limit_price: None,
tp_limit_price: None,
order_iv: None,
mmp: None,
position_idx: None,
bbo_side_type: None,
bbo_level: None,
};
let json = serde_json::to_string(¶ms).unwrap();
assert!(!json.contains("orderIv"));
assert!(!json.contains("mmp"));
assert!(!json.contains("positionIdx"));
}
#[rstest]
fn serialize_place_params_includes_bbo_when_set() {
let params = BybitWsPlaceOrderParams {
category: BybitProductType::Linear,
symbol: Ustr::from("BTCUSDT"),
side: BybitOrderSide::Buy,
order_type: BybitOrderType::Limit,
qty: "0.01".to_string(),
is_leverage: None,
market_unit: None,
price: None,
time_in_force: Some(BybitTimeInForce::Gtc),
order_link_id: None,
reduce_only: None,
close_on_trigger: None,
trigger_price: None,
trigger_by: None,
trigger_direction: None,
tpsl_mode: None,
take_profit: None,
stop_loss: None,
tp_trigger_by: None,
sl_trigger_by: None,
sl_trigger_price: None,
tp_trigger_price: None,
sl_order_type: None,
tp_order_type: None,
sl_limit_price: None,
tp_limit_price: None,
order_iv: None,
mmp: None,
position_idx: None,
bbo_side_type: Some(BybitBboSideType::Queue),
bbo_level: Some("2".to_string()),
};
let json = serde_json::to_string(¶ms).unwrap();
assert!(json.contains("\"bboSideType\":\"Queue\""));
assert!(json.contains("\"bboLevel\":\"2\""));
assert!(!json.contains("\"price\""));
}
#[rstest]
#[case(BybitPositionIdx::BuyHedge, 1)]
#[case(BybitPositionIdx::SellHedge, 2)]
fn serialize_place_params_includes_position_idx_when_set(
#[case] idx: BybitPositionIdx,
#[case] expected: i32,
) {
let params = BybitWsPlaceOrderParams {
category: BybitProductType::Linear,
symbol: Ustr::from("BTCUSDT"),
side: BybitOrderSide::Buy,
order_type: BybitOrderType::Limit,
qty: "0.01".to_string(),
is_leverage: None,
market_unit: None,
price: Some("50000".to_string()),
time_in_force: Some(BybitTimeInForce::Gtc),
order_link_id: None,
reduce_only: None,
close_on_trigger: None,
trigger_price: None,
trigger_by: None,
trigger_direction: None,
tpsl_mode: None,
take_profit: None,
stop_loss: None,
tp_trigger_by: None,
sl_trigger_by: None,
sl_trigger_price: None,
tp_trigger_price: None,
sl_order_type: None,
tp_order_type: None,
sl_limit_price: None,
tp_limit_price: None,
order_iv: None,
mmp: None,
position_idx: Some(idx),
bbo_side_type: None,
bbo_level: None,
};
let json = serde_json::to_string(¶ms).unwrap();
assert!(json.contains(&format!("\"positionIdx\":{expected}")));
}
#[rstest]
#[case(None)]
#[case(Some(BybitPositionIdx::OneWay))]
#[case(Some(BybitPositionIdx::BuyHedge))]
#[case(Some(BybitPositionIdx::SellHedge))]
fn place_params_position_idx_roundtrip(#[case] idx: Option<BybitPositionIdx>) {
let params = BybitWsPlaceOrderParams {
category: BybitProductType::Linear,
symbol: Ustr::from("BTCUSDT"),
side: BybitOrderSide::Buy,
order_type: BybitOrderType::Limit,
qty: "0.01".to_string(),
is_leverage: None,
market_unit: None,
price: Some("50000".to_string()),
time_in_force: Some(BybitTimeInForce::Gtc),
order_link_id: None,
reduce_only: None,
close_on_trigger: None,
trigger_price: None,
trigger_by: None,
trigger_direction: None,
tpsl_mode: None,
take_profit: None,
stop_loss: None,
tp_trigger_by: None,
sl_trigger_by: None,
sl_trigger_price: None,
tp_trigger_price: None,
sl_order_type: None,
tp_order_type: None,
sl_limit_price: None,
tp_limit_price: None,
order_iv: None,
mmp: None,
position_idx: idx,
bbo_side_type: None,
bbo_level: None,
};
let json = serde_json::to_string(¶ms).unwrap();
let decoded: BybitWsPlaceOrderParams = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.position_idx, idx);
}
#[rstest]
fn serialize_amend_params_includes_order_iv_when_set() {
let params = BybitWsAmendOrderParams {
category: BybitProductType::Option,
symbol: Ustr::from("BTC-30JUN25-100000-C"),
order_id: None,
order_link_id: Some("test-1".to_string()),
qty: None,
price: None,
trigger_price: None,
take_profit: None,
stop_loss: None,
tp_trigger_by: None,
sl_trigger_by: None,
order_iv: Some("0.90".to_string()),
};
let json = serde_json::to_string(¶ms).unwrap();
assert!(json.contains("\"orderIv\":\"0.90\""));
}
#[rstest]
fn deserialize_account_order_frame_uses_enums() {
let json = load_test_json("ws_account_order.json");
let frame: BybitWsAccountOrderMsg = serde_json::from_str(&json).unwrap();
let order = &frame.data[0];
assert_eq!(order.cancel_type, BybitCancelType::CancelByUser);
assert_eq!(order.tp_trigger_by, BybitTriggerType::MarkPrice);
assert_eq!(order.sl_trigger_by, BybitTriggerType::LastPrice);
assert_eq!(order.tpsl_mode, Some(BybitTpSlMode::Full));
assert_eq!(order.create_type, Some(BybitCreateType::CreateByUser));
assert_eq!(order.side, BybitOrderSide::Buy);
}
#[rstest]
fn deserialize_ws_account_position_without_conditional_fields() {
let json = r#"{
"topic": "position",
"id": "1",
"creationTime": 1697673900000,
"data": [{
"category": "linear",
"symbol": "LTCUSDT",
"side": "",
"size": "0",
"positionIdx": 0,
"tradeMode": 0,
"positionValue": "0",
"riskId": 1,
"riskLimitValue": "150",
"entryPrice": "",
"markPrice": "70.00",
"leverage": "10",
"positionBalance": "0",
"autoAddMargin": 0,
"positionIM": "0",
"positionIMByMp": "0",
"positionMM": "0",
"positionMMByMp": "0",
"liqPrice": "",
"bustPrice": "",
"tpslMode": "Full",
"takeProfit": "0",
"stopLoss": "0",
"trailingStop": "0",
"unrealisedPnl": "0",
"sessionAvgPrice": "0",
"curRealisedPnl": "0",
"cumRealisedPnl": "0",
"positionStatus": "Normal",
"adlRankIndicator": 0,
"createdTime": "1676538056258",
"updatedTime": "1697673600012"
}]
}"#;
let msg: BybitWsAccountPositionMsg = serde_json::from_str(json)
.expect("Failed to parse WS account position with missing conditional fields");
let position = &msg.data[0];
assert!(!position.is_reduce_only);
assert_eq!(position.seq, -1);
assert_eq!(position.mmr_sys_updated_time, "");
assert_eq!(position.leverage_sys_updated_time, "");
}
}