use pyo3::prelude::*;
use serde_json;
use ustr::Ustr;
use crate::{
common::{
enums::{
BybitBboSideType, BybitMarketUnit, BybitOrderSide, BybitOrderType, BybitPositionIdx,
BybitProductType, BybitTimeInForce, BybitTpSlMode, BybitTriggerType,
},
parse::parse_bbo_level,
},
websocket::{error::BybitWsError, messages},
};
#[pyclass(from_py_object)]
#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.bybit")]
#[derive(Clone, Debug)]
pub struct BybitWsPlaceOrderParams {
#[pyo3(get, set)]
pub category: BybitProductType,
#[pyo3(get, set)]
pub symbol: String,
#[pyo3(get, set)]
pub side: String,
#[pyo3(get, set)]
pub order_type: String,
#[pyo3(get, set)]
pub qty: String,
#[pyo3(get, set)]
pub is_leverage: Option<i32>,
#[pyo3(get, set)]
pub market_unit: Option<String>,
#[pyo3(get, set)]
pub price: Option<String>,
#[pyo3(get, set)]
pub time_in_force: Option<String>,
#[pyo3(get, set)]
pub order_link_id: Option<String>,
#[pyo3(get, set)]
pub reduce_only: Option<bool>,
#[pyo3(get, set)]
pub close_on_trigger: Option<bool>,
#[pyo3(get, set)]
pub trigger_price: Option<String>,
#[pyo3(get, set)]
pub trigger_by: Option<String>,
#[pyo3(get, set)]
pub trigger_direction: Option<i32>,
#[pyo3(get, set)]
pub tpsl_mode: Option<String>,
#[pyo3(get, set)]
pub take_profit: Option<String>,
#[pyo3(get, set)]
pub stop_loss: Option<String>,
#[pyo3(get, set)]
pub tp_trigger_by: Option<String>,
#[pyo3(get, set)]
pub sl_trigger_by: Option<String>,
#[pyo3(get, set)]
pub sl_trigger_price: Option<String>,
#[pyo3(get, set)]
pub tp_trigger_price: Option<String>,
#[pyo3(get, set)]
pub sl_order_type: Option<String>,
#[pyo3(get, set)]
pub tp_order_type: Option<String>,
#[pyo3(get, set)]
pub sl_limit_price: Option<String>,
#[pyo3(get, set)]
pub tp_limit_price: Option<String>,
#[pyo3(get, set)]
pub order_iv: Option<String>,
#[pyo3(get, set)]
pub mmp: Option<bool>,
#[pyo3(get, set)]
pub position_idx: Option<BybitPositionIdx>,
#[pyo3(get, set)]
pub bbo_side_type: Option<String>,
#[pyo3(get, set)]
pub bbo_level: Option<String>,
}
#[pymethods]
#[pyo3_stub_gen::derive::gen_stub_pymethods]
impl BybitWsPlaceOrderParams {
#[new]
#[pyo3(signature = (
category,
symbol,
side,
order_type,
qty,
is_leverage=None,
market_unit=None,
price=None,
time_in_force=None,
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,
))]
#[expect(clippy::too_many_arguments)]
fn py_new(
category: BybitProductType,
symbol: String,
side: String,
order_type: String,
qty: String,
is_leverage: Option<i32>,
market_unit: Option<String>,
price: Option<String>,
time_in_force: Option<String>,
order_link_id: Option<String>,
reduce_only: Option<bool>,
close_on_trigger: Option<bool>,
trigger_price: Option<String>,
trigger_by: Option<String>,
trigger_direction: Option<i32>,
tpsl_mode: Option<String>,
take_profit: Option<String>,
stop_loss: Option<String>,
tp_trigger_by: Option<String>,
sl_trigger_by: Option<String>,
sl_trigger_price: Option<String>,
tp_trigger_price: Option<String>,
sl_order_type: Option<String>,
tp_order_type: Option<String>,
sl_limit_price: Option<String>,
tp_limit_price: Option<String>,
order_iv: Option<String>,
mmp: Option<bool>,
position_idx: Option<BybitPositionIdx>,
bbo_side_type: Option<String>,
bbo_level: Option<String>,
) -> Self {
Self {
category,
symbol,
side,
order_type,
qty,
is_leverage,
market_unit,
price,
time_in_force,
order_link_id,
reduce_only,
close_on_trigger,
trigger_price,
trigger_by,
trigger_direction,
tpsl_mode,
take_profit,
stop_loss,
tp_trigger_by,
sl_trigger_by,
sl_trigger_price,
tp_trigger_price,
sl_order_type,
tp_order_type,
sl_limit_price,
tp_limit_price,
order_iv,
mmp,
position_idx,
bbo_side_type,
bbo_level,
}
}
}
impl TryFrom<BybitWsPlaceOrderParams> for messages::BybitWsPlaceOrderParams {
type Error = BybitWsError;
fn try_from(params: BybitWsPlaceOrderParams) -> Result<Self, Self::Error> {
let side: BybitOrderSide =
serde_json::from_str(&format!("\"{}\"", params.side)).map_err(|e| {
BybitWsError::ClientError(format!("Invalid side '{}': {}", params.side, e))
})?;
let order_type: BybitOrderType =
serde_json::from_str(&format!("\"{}\"", params.order_type)).map_err(|e| {
BybitWsError::ClientError(format!(
"Invalid order_type '{}': {}",
params.order_type, e
))
})?;
let time_in_force = params
.time_in_force
.map(|v| {
serde_json::from_str::<BybitTimeInForce>(&format!("\"{v}\"")).map_err(|e| {
BybitWsError::ClientError(format!("Invalid time_in_force '{v}': {e}"))
})
})
.transpose()?;
let trigger_by = params
.trigger_by
.map(|v| {
serde_json::from_str::<BybitTriggerType>(&format!("\"{v}\"")).map_err(|e| {
BybitWsError::ClientError(format!("Invalid trigger_by '{v}': {e}"))
})
})
.transpose()?;
let tp_trigger_by = params
.tp_trigger_by
.map(|v| {
serde_json::from_str::<BybitTriggerType>(&format!("\"{v}\"")).map_err(|e| {
BybitWsError::ClientError(format!("Invalid tp_trigger_by '{v}': {e}"))
})
})
.transpose()?;
let sl_trigger_by = params
.sl_trigger_by
.map(|v| {
serde_json::from_str::<BybitTriggerType>(&format!("\"{v}\"")).map_err(|e| {
BybitWsError::ClientError(format!("Invalid sl_trigger_by '{v}': {e}"))
})
})
.transpose()?;
let sl_order_type = params
.sl_order_type
.map(|v| {
serde_json::from_str::<BybitOrderType>(&format!("\"{v}\"")).map_err(|e| {
BybitWsError::ClientError(format!("Invalid sl_order_type '{v}': {e}"))
})
})
.transpose()?;
let tp_order_type = params
.tp_order_type
.map(|v| {
serde_json::from_str::<BybitOrderType>(&format!("\"{v}\"")).map_err(|e| {
BybitWsError::ClientError(format!("Invalid tp_order_type '{v}': {e}"))
})
})
.transpose()?;
let tpsl_mode = params
.tpsl_mode
.map(|v| {
serde_json::from_str::<BybitTpSlMode>(&format!("\"{v}\""))
.map_err(|e| BybitWsError::ClientError(format!("Invalid tpsl_mode '{v}': {e}")))
})
.transpose()?;
let market_unit = params
.market_unit
.map(|v| {
serde_json::from_str::<BybitMarketUnit>(&format!("\"{v}\"")).map_err(|e| {
BybitWsError::ClientError(format!("Invalid market_unit '{v}': {e}"))
})
})
.transpose()?;
let bbo_side_type = params
.bbo_side_type
.map(|v| {
serde_json::from_str::<BybitBboSideType>(&format!("\"{v}\"")).map_err(|e| {
BybitWsError::ClientError(format!("Invalid bbo_side_type '{v}': {e}"))
})
})
.transpose()?;
let bbo_level = params
.bbo_level
.map(parse_bbo_level)
.transpose()
.map_err(|e| BybitWsError::ClientError(e.to_string()))?;
if bbo_side_type.is_some() != bbo_level.is_some() {
return Err(BybitWsError::ClientError(
"'bbo_side_type' and 'bbo_level' must be provided together".to_string(),
));
}
Ok(Self {
category: params.category,
symbol: Ustr::from(¶ms.symbol),
side,
order_type,
qty: params.qty,
is_leverage: params.is_leverage,
market_unit,
price: params.price,
time_in_force,
order_link_id: params.order_link_id,
reduce_only: params.reduce_only,
close_on_trigger: params.close_on_trigger,
trigger_price: params.trigger_price,
trigger_by,
trigger_direction: params.trigger_direction,
tpsl_mode,
take_profit: params.take_profit,
stop_loss: params.stop_loss,
tp_trigger_by,
sl_trigger_by,
sl_trigger_price: params.sl_trigger_price,
tp_trigger_price: params.tp_trigger_price,
sl_order_type,
tp_order_type,
sl_limit_price: params.sl_limit_price,
tp_limit_price: params.tp_limit_price,
order_iv: params.order_iv,
mmp: params.mmp,
position_idx: params.position_idx,
bbo_side_type,
bbo_level,
})
}
}
impl From<messages::BybitWsPlaceOrderParams> for BybitWsPlaceOrderParams {
fn from(params: messages::BybitWsPlaceOrderParams) -> Self {
let side = serde_json::to_string(¶ms.side)
.expect("Failed to serialize BybitOrderSide")
.trim_matches('"')
.to_string();
let order_type = serde_json::to_string(¶ms.order_type)
.expect("Failed to serialize BybitOrderType")
.trim_matches('"')
.to_string();
let time_in_force = params.time_in_force.map(|v| {
serde_json::to_string(&v)
.expect("Failed to serialize BybitTimeInForce")
.trim_matches('"')
.to_string()
});
let trigger_by = params.trigger_by.map(|v| {
serde_json::to_string(&v)
.expect("Failed to serialize BybitTriggerType")
.trim_matches('"')
.to_string()
});
let tp_trigger_by = params.tp_trigger_by.map(|v| {
serde_json::to_string(&v)
.expect("Failed to serialize BybitTriggerType")
.trim_matches('"')
.to_string()
});
let sl_trigger_by = params.sl_trigger_by.map(|v| {
serde_json::to_string(&v)
.expect("Failed to serialize BybitTriggerType")
.trim_matches('"')
.to_string()
});
let sl_order_type = params.sl_order_type.map(|v| {
serde_json::to_string(&v)
.expect("Failed to serialize BybitOrderType")
.trim_matches('"')
.to_string()
});
let tp_order_type = params.tp_order_type.map(|v| {
serde_json::to_string(&v)
.expect("Failed to serialize BybitOrderType")
.trim_matches('"')
.to_string()
});
let tpsl_mode = params.tpsl_mode.map(|v| {
serde_json::to_string(&v)
.expect("Failed to serialize BybitTpSlMode")
.trim_matches('"')
.to_string()
});
let market_unit = params.market_unit.map(|v| {
serde_json::to_string(&v)
.expect("Failed to serialize BybitMarketUnit")
.trim_matches('"')
.to_string()
});
let bbo_side_type = params.bbo_side_type.map(|v| {
serde_json::to_string(&v)
.expect("Failed to serialize BybitBboSideType")
.trim_matches('"')
.to_string()
});
Self {
category: params.category,
symbol: params.symbol.to_string(),
side,
order_type,
qty: params.qty,
is_leverage: params.is_leverage,
market_unit,
price: params.price,
time_in_force,
order_link_id: params.order_link_id,
reduce_only: params.reduce_only,
close_on_trigger: params.close_on_trigger,
trigger_price: params.trigger_price,
trigger_by,
trigger_direction: params.trigger_direction,
tpsl_mode,
take_profit: params.take_profit,
stop_loss: params.stop_loss,
tp_trigger_by,
sl_trigger_by,
sl_trigger_price: params.sl_trigger_price,
tp_trigger_price: params.tp_trigger_price,
sl_order_type,
tp_order_type,
sl_limit_price: params.sl_limit_price,
tp_limit_price: params.tp_limit_price,
order_iv: params.order_iv,
mmp: params.mmp,
position_idx: params.position_idx,
bbo_side_type,
bbo_level: params.bbo_level,
}
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
fn place_order_params(
bbo_side_type: Option<&str>,
bbo_level: Option<&str>,
) -> BybitWsPlaceOrderParams {
BybitWsPlaceOrderParams {
category: BybitProductType::Linear,
symbol: "BTCUSDT".to_string(),
side: "Buy".to_string(),
order_type: "Limit".to_string(),
qty: "0.001".to_string(),
is_leverage: None,
market_unit: None,
price: None,
time_in_force: Some("GTC".to_string()),
order_link_id: Some("test-bbo-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: None,
mmp: None,
position_idx: None,
bbo_side_type: bbo_side_type.map(str::to_string),
bbo_level: bbo_level.map(str::to_string),
}
}
#[rstest]
fn test_place_order_params_try_from_accepts_bbo_pair() {
let params = place_order_params(Some("Queue"), Some("2"));
let result = messages::BybitWsPlaceOrderParams::try_from(params).unwrap();
assert_eq!(result.bbo_side_type, Some(BybitBboSideType::Queue));
assert_eq!(result.bbo_level.as_deref(), Some("2"));
}
#[rstest]
fn test_place_order_params_try_from_rejects_unpaired_bbo() {
let params = place_order_params(Some("Queue"), None);
let err = messages::BybitWsPlaceOrderParams::try_from(params).unwrap_err();
assert!(
err.to_string()
.contains("'bbo_side_type' and 'bbo_level' must be provided together")
);
}
#[rstest]
fn test_place_order_params_try_from_rejects_invalid_bbo_level() {
let params = place_order_params(Some("Queue"), Some("6"));
let err = messages::BybitWsPlaceOrderParams::try_from(params).unwrap_err();
assert!(err.to_string().contains("invalid 'bbo_level'"));
}
}
#[pyclass(from_py_object)]
#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.bybit")]
#[derive(Clone, Debug)]
pub struct BybitWsAmendOrderParams {
#[pyo3(get, set)]
pub category: BybitProductType,
#[pyo3(get, set)]
pub symbol: String,
#[pyo3(get, set)]
pub order_id: Option<String>,
#[pyo3(get, set)]
pub order_link_id: Option<String>,
#[pyo3(get, set)]
pub qty: Option<String>,
#[pyo3(get, set)]
pub price: Option<String>,
#[pyo3(get, set)]
pub trigger_price: Option<String>,
#[pyo3(get, set)]
pub take_profit: Option<String>,
#[pyo3(get, set)]
pub stop_loss: Option<String>,
#[pyo3(get, set)]
pub tp_trigger_by: Option<String>,
#[pyo3(get, set)]
pub sl_trigger_by: Option<String>,
#[pyo3(get, set)]
pub order_iv: Option<String>,
}
#[pymethods]
#[pyo3_stub_gen::derive::gen_stub_pymethods]
impl BybitWsAmendOrderParams {
#[new]
#[expect(clippy::too_many_arguments)]
fn py_new(
category: BybitProductType,
symbol: String,
order_id: Option<String>,
order_link_id: Option<String>,
qty: Option<String>,
price: Option<String>,
trigger_price: Option<String>,
take_profit: Option<String>,
stop_loss: Option<String>,
tp_trigger_by: Option<String>,
sl_trigger_by: Option<String>,
order_iv: Option<String>,
) -> Self {
Self {
category,
symbol,
order_id,
order_link_id,
qty,
price,
trigger_price,
take_profit,
stop_loss,
tp_trigger_by,
sl_trigger_by,
order_iv,
}
}
}
impl TryFrom<BybitWsAmendOrderParams> for messages::BybitWsAmendOrderParams {
type Error = BybitWsError;
fn try_from(params: BybitWsAmendOrderParams) -> Result<Self, Self::Error> {
let tp_trigger_by = params
.tp_trigger_by
.map(|v| {
serde_json::from_str::<BybitTriggerType>(&format!("\"{v}\"")).map_err(|e| {
BybitWsError::ClientError(format!("Invalid tp_trigger_by '{v}': {e}"))
})
})
.transpose()?;
let sl_trigger_by = params
.sl_trigger_by
.map(|v| {
serde_json::from_str::<BybitTriggerType>(&format!("\"{v}\"")).map_err(|e| {
BybitWsError::ClientError(format!("Invalid sl_trigger_by '{v}': {e}"))
})
})
.transpose()?;
Ok(Self {
category: params.category,
symbol: Ustr::from(¶ms.symbol),
order_id: params.order_id,
order_link_id: params.order_link_id,
qty: params.qty,
price: params.price,
trigger_price: params.trigger_price,
take_profit: params.take_profit,
stop_loss: params.stop_loss,
tp_trigger_by,
sl_trigger_by,
order_iv: params.order_iv,
})
}
}
impl From<messages::BybitWsAmendOrderParams> for BybitWsAmendOrderParams {
fn from(params: messages::BybitWsAmendOrderParams) -> Self {
let tp_trigger_by = params.tp_trigger_by.map(|v| {
serde_json::to_string(&v)
.expect("Failed to serialize BybitTriggerType")
.trim_matches('"')
.to_string()
});
let sl_trigger_by = params.sl_trigger_by.map(|v| {
serde_json::to_string(&v)
.expect("Failed to serialize BybitTriggerType")
.trim_matches('"')
.to_string()
});
Self {
category: params.category,
symbol: params.symbol.to_string(),
order_id: params.order_id,
order_link_id: params.order_link_id,
qty: params.qty,
price: params.price,
trigger_price: params.trigger_price,
take_profit: params.take_profit,
stop_loss: params.stop_loss,
tp_trigger_by,
sl_trigger_by,
order_iv: params.order_iv,
}
}
}
#[pyclass(from_py_object)]
#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.bybit")]
#[derive(Clone, Debug)]
pub struct BybitWsCancelOrderParams {
#[pyo3(get, set)]
pub category: BybitProductType,
#[pyo3(get, set)]
pub symbol: String,
#[pyo3(get, set)]
pub order_id: Option<String>,
#[pyo3(get, set)]
pub order_link_id: Option<String>,
}
#[pymethods]
#[pyo3_stub_gen::derive::gen_stub_pymethods]
impl BybitWsCancelOrderParams {
#[new]
fn py_new(
category: BybitProductType,
symbol: String,
order_id: Option<String>,
order_link_id: Option<String>,
) -> Self {
Self {
category,
symbol,
order_id,
order_link_id,
}
}
}
impl TryFrom<BybitWsCancelOrderParams> for messages::BybitWsCancelOrderParams {
type Error = BybitWsError;
fn try_from(params: BybitWsCancelOrderParams) -> Result<Self, Self::Error> {
Ok(Self {
category: params.category,
symbol: Ustr::from(¶ms.symbol),
order_id: params.order_id,
order_link_id: params.order_link_id,
})
}
}
impl From<messages::BybitWsCancelOrderParams> for BybitWsCancelOrderParams {
fn from(params: messages::BybitWsCancelOrderParams) -> Self {
Self {
category: params.category,
symbol: params.symbol.to_string(),
order_id: params.order_id,
order_link_id: params.order_link_id,
}
}
}
#[pyclass(from_py_object)]
#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.bybit")]
#[derive(Clone, Debug)]
pub struct BybitTickersParams {
#[pyo3(get, set)]
pub category: BybitProductType,
#[pyo3(get, set)]
pub symbol: Option<String>,
#[pyo3(get, set)]
pub base_coin: Option<String>,
#[pyo3(get, set)]
pub exp_date: Option<String>,
}
#[pymethods]
#[pyo3_stub_gen::derive::gen_stub_pymethods]
impl BybitTickersParams {
#[new]
#[pyo3(signature = (category, symbol=None, base_coin=None, exp_date=None))]
fn py_new(
category: BybitProductType,
symbol: Option<String>,
base_coin: Option<String>,
exp_date: Option<String>,
) -> Self {
Self {
category,
symbol,
base_coin,
exp_date,
}
}
}
impl From<BybitTickersParams> for crate::http::query::BybitTickersParams {
fn from(params: BybitTickersParams) -> Self {
Self {
category: params.category,
symbol: params.symbol,
base_coin: params.base_coin,
exp_date: params.exp_date,
}
}
}