use std::borrow::Cow;
use nautilus_model::enums::{
ContingencyType, LiquiditySide, MarketStatusAction, OrderSide, OrderSideSpecified, OrderStatus,
OrderType, PositionSide, TimeInForce,
};
use serde::{Deserialize, Deserializer, Serialize};
use strum::{AsRefStr, Display, EnumIter, EnumString};
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
#[serde(rename_all = "PascalCase")]
#[cfg_attr(
feature = "python",
pyo3::pyclass(
module = "nautilus_trader.core.nautilus_pyo3.bitmex",
eq,
eq_int,
from_py_object,
rename_all = "SCREAMING_SNAKE_CASE",
)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.bitmex")
)]
pub enum BitmexSymbolStatus {
Open,
Closed,
Unlisted,
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
pub enum BitmexSide {
#[serde(rename = "Buy", alias = "BUY", alias = "buy")]
Buy,
#[serde(rename = "Sell", alias = "SELL", alias = "sell")]
Sell,
}
impl From<OrderSideSpecified> for BitmexSide {
fn from(value: OrderSideSpecified) -> Self {
match value {
OrderSideSpecified::Buy => Self::Buy,
OrderSideSpecified::Sell => Self::Sell,
}
}
}
impl From<BitmexSide> for OrderSide {
fn from(side: BitmexSide) -> Self {
match side {
BitmexSide::Buy => Self::Buy,
BitmexSide::Sell => Self::Sell,
}
}
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(
module = "nautilus_trader.core.nautilus_pyo3.bitmex",
eq,
eq_int,
from_py_object
)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.bitmex")
)]
pub enum BitmexPositionSide {
#[serde(rename = "LONG", alias = "Long", alias = "long")]
Long,
#[serde(rename = "SHORT", alias = "Short", alias = "short")]
Short,
#[serde(rename = "FLAT", alias = "Flat", alias = "flat")]
Flat,
}
impl From<BitmexPositionSide> for PositionSide {
fn from(side: BitmexPositionSide) -> Self {
match side {
BitmexPositionSide::Long => Self::Long,
BitmexPositionSide::Short => Self::Short,
BitmexPositionSide::Flat => Self::Flat,
}
}
}
impl From<PositionSide> for BitmexPositionSide {
fn from(side: PositionSide) -> Self {
match side {
PositionSide::Long => Self::Long,
PositionSide::Short => Self::Short,
PositionSide::Flat | PositionSide::NoPositionSide => Self::Flat,
}
}
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
pub enum BitmexOrderType {
Market,
Limit,
Stop,
StopLimit,
MarketIfTouched,
LimitIfTouched,
Pegged,
}
impl TryFrom<OrderType> for BitmexOrderType {
type Error = anyhow::Error;
fn try_from(value: OrderType) -> Result<Self, Self::Error> {
match value {
OrderType::Market => Ok(Self::Market),
OrderType::Limit => Ok(Self::Limit),
OrderType::StopMarket => Ok(Self::Stop),
OrderType::StopLimit => Ok(Self::StopLimit),
OrderType::MarketIfTouched => Ok(Self::MarketIfTouched),
OrderType::LimitIfTouched => Ok(Self::LimitIfTouched),
OrderType::TrailingStopMarket => Ok(Self::Pegged),
OrderType::TrailingStopLimit => Ok(Self::Pegged),
OrderType::MarketToLimit => {
anyhow::bail!("MarketToLimit order type is not supported by BitMEX")
}
}
}
}
impl BitmexOrderType {
pub fn try_from_order_type(value: OrderType) -> anyhow::Result<Self> {
Self::try_from(value)
}
}
impl From<BitmexOrderType> for OrderType {
fn from(value: BitmexOrderType) -> Self {
match value {
BitmexOrderType::Market => Self::Market,
BitmexOrderType::Limit => Self::Limit,
BitmexOrderType::Stop => Self::StopMarket,
BitmexOrderType::StopLimit => Self::StopLimit,
BitmexOrderType::MarketIfTouched => Self::MarketIfTouched,
BitmexOrderType::LimitIfTouched => Self::LimitIfTouched,
BitmexOrderType::Pegged => Self::Limit,
}
}
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
pub enum BitmexOrderStatus {
New,
PendingNew,
PartiallyFilled,
Filled,
PendingReplace,
PendingCancel,
Canceled,
Rejected,
Expired,
}
impl BitmexOrderStatus {
pub fn is_terminal(self) -> bool {
matches!(
self,
Self::Filled | Self::Canceled | Self::Rejected | Self::Expired
)
}
}
impl From<BitmexOrderStatus> for OrderStatus {
fn from(value: BitmexOrderStatus) -> Self {
match value {
BitmexOrderStatus::New => Self::Accepted,
BitmexOrderStatus::PendingNew => Self::Submitted,
BitmexOrderStatus::PartiallyFilled => Self::PartiallyFilled,
BitmexOrderStatus::Filled => Self::Filled,
BitmexOrderStatus::PendingReplace => Self::PendingUpdate,
BitmexOrderStatus::PendingCancel => Self::PendingCancel,
BitmexOrderStatus::Canceled => Self::Canceled,
BitmexOrderStatus::Rejected => Self::Rejected,
BitmexOrderStatus::Expired => Self::Expired,
}
}
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
pub enum BitmexTimeInForce {
Day,
GoodTillCancel,
AtTheOpening,
ImmediateOrCancel,
FillOrKill,
GoodTillCrossing,
GoodTillDate,
AtTheClose,
GoodThroughCrossing,
AtCrossing,
}
impl TryFrom<BitmexTimeInForce> for TimeInForce {
type Error = anyhow::Error;
fn try_from(value: BitmexTimeInForce) -> Result<Self, Self::Error> {
match value {
BitmexTimeInForce::Day => Ok(Self::Day),
BitmexTimeInForce::GoodTillCancel => Ok(Self::Gtc),
BitmexTimeInForce::GoodTillDate => Ok(Self::Gtd),
BitmexTimeInForce::ImmediateOrCancel => Ok(Self::Ioc),
BitmexTimeInForce::FillOrKill => Ok(Self::Fok),
BitmexTimeInForce::AtTheOpening => Ok(Self::AtTheOpen),
BitmexTimeInForce::AtTheClose => Ok(Self::AtTheClose),
_ => anyhow::bail!("Unsupported BitmexTimeInForce: {value}"),
}
}
}
impl TryFrom<TimeInForce> for BitmexTimeInForce {
type Error = anyhow::Error;
fn try_from(value: TimeInForce) -> Result<Self, Self::Error> {
match value {
TimeInForce::Day => Ok(Self::Day),
TimeInForce::Gtc => Ok(Self::GoodTillCancel),
TimeInForce::Gtd => Ok(Self::GoodTillDate),
TimeInForce::Ioc => Ok(Self::ImmediateOrCancel),
TimeInForce::Fok => Ok(Self::FillOrKill),
TimeInForce::AtTheOpen => Ok(Self::AtTheOpening),
TimeInForce::AtTheClose => Ok(Self::AtTheClose),
}
}
}
impl BitmexTimeInForce {
pub fn try_from_time_in_force(value: TimeInForce) -> anyhow::Result<Self> {
Self::try_from(value)
}
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
pub enum BitmexContingencyType {
OneCancelsTheOther,
OneTriggersTheOther,
OneUpdatesTheOtherAbsolute,
OneUpdatesTheOtherProportional,
#[serde(rename = "")]
Unknown, }
impl From<BitmexContingencyType> for ContingencyType {
fn from(value: BitmexContingencyType) -> Self {
match value {
BitmexContingencyType::OneCancelsTheOther => Self::Oco,
BitmexContingencyType::OneTriggersTheOther => Self::Oto,
BitmexContingencyType::OneUpdatesTheOtherProportional => Self::Ouo,
BitmexContingencyType::OneUpdatesTheOtherAbsolute => Self::Ouo,
BitmexContingencyType::Unknown => Self::NoContingency,
}
}
}
impl TryFrom<ContingencyType> for BitmexContingencyType {
type Error = anyhow::Error;
fn try_from(value: ContingencyType) -> Result<Self, Self::Error> {
match value {
ContingencyType::NoContingency => Ok(Self::Unknown),
ContingencyType::Oco => Ok(Self::OneCancelsTheOther),
ContingencyType::Oto => Ok(Self::OneTriggersTheOther),
ContingencyType::Ouo => anyhow::bail!("OUO contingency type not supported by BitMEX"),
}
}
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
pub enum BitmexPegPriceType {
LastPeg,
OpeningPeg,
MidPricePeg,
MarketPeg,
PrimaryPeg,
PegToVWAP,
TrailingStopPeg,
PegToLimitPrice,
ShortSaleMinPricePeg,
#[serde(rename = "")]
Unknown, }
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
pub enum BitmexExecInstruction {
ParticipateDoNotInitiate,
AllOrNone,
MarkPrice,
IndexPrice,
LastPrice,
Close,
ReduceOnly,
Fixed,
#[serde(rename = "")]
Unknown, }
impl BitmexExecInstruction {
pub fn join(instructions: &[Self]) -> String {
instructions
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(",")
}
}
#[derive(Clone, Debug, Display, PartialEq, Eq, AsRefStr, EnumIter, EnumString, Serialize)]
pub enum BitmexExecType {
New,
Trade,
Canceled,
CancelReject,
Replaced,
Rejected,
AmendReject,
Funding,
Settlement,
Suspended,
Released,
Insurance,
Rebalance,
Liquidation,
Bankruptcy,
TrialFill,
TriggeredOrActivatedBySystem,
#[strum(disabled)]
Unknown(String),
}
impl<'de> Deserialize<'de> for BitmexExecType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.as_str() {
"New" => Ok(Self::New),
"Trade" => Ok(Self::Trade),
"Canceled" => Ok(Self::Canceled),
"CancelReject" => Ok(Self::CancelReject),
"Replaced" => Ok(Self::Replaced),
"Rejected" => Ok(Self::Rejected),
"AmendReject" => Ok(Self::AmendReject),
"Funding" => Ok(Self::Funding),
"Settlement" => Ok(Self::Settlement),
"Suspended" => Ok(Self::Suspended),
"Released" => Ok(Self::Released),
"Insurance" => Ok(Self::Insurance),
"Rebalance" => Ok(Self::Rebalance),
"Liquidation" => Ok(Self::Liquidation),
"Bankruptcy" => Ok(Self::Bankruptcy),
"TrialFill" => Ok(Self::TrialFill),
"TriggeredOrActivatedBySystem" => Ok(Self::TriggeredOrActivatedBySystem),
other => Ok(Self::Unknown(other.to_string())),
}
}
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
pub enum BitmexLiquidityIndicator {
#[serde(rename = "Added")]
#[serde(alias = "AddedLiquidity")]
Maker,
#[serde(rename = "Removed")]
#[serde(alias = "RemovedLiquidity")]
Taker,
}
impl From<BitmexLiquidityIndicator> for LiquiditySide {
fn from(value: BitmexLiquidityIndicator) -> Self {
match value {
BitmexLiquidityIndicator::Maker => Self::Maker,
BitmexLiquidityIndicator::Taker => Self::Taker,
}
}
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
#[serde(rename_all = "UPPERCASE")]
pub enum BitmexInstrumentType {
#[serde(rename = "FXXXS")]
LegacyFutures,
#[serde(rename = "FXXXN")]
LegacyFuturesN,
#[serde(rename = "FMXXS")]
FuturesSpreads,
#[serde(rename = "FFICSX")]
PredictionMarket,
#[serde(rename = "FFSCSX")]
StockPerpetual,
#[serde(rename = "FFWCSX")]
PerpetualContract,
#[serde(rename = "FFWCSF")]
PerpetualContractFx,
#[serde(rename = "FFCCSX")]
Futures,
#[serde(rename = "IFXXXP")]
Spot,
#[serde(rename = "OCECCS")]
CallOption,
#[serde(rename = "OPECCS")]
PutOption,
#[serde(rename = "SRMCSX")]
SwapRate,
#[serde(rename = "RCSXXX")]
ReferenceBasket,
#[serde(rename = "MRBXXX")]
BasketIndex,
#[serde(rename = "MRCXXX")]
CryptoIndex,
#[serde(rename = "MRFXXX")]
FxIndex,
#[serde(rename = "MRRXXX")]
LendingIndex,
#[serde(rename = "MRIXXX")]
VolatilityIndex,
#[serde(rename = "MRSXXX")]
StockIndex,
#[serde(rename = "MRVDXX")]
YieldIndex,
}
#[derive(Clone, Debug, Display, PartialEq, Eq, AsRefStr, EnumIter, EnumString, Serialize)]
pub enum BitmexProductType {
#[serde(rename = "instrument")]
All,
#[serde(rename = "CONTRACTS")]
Contracts,
#[serde(rename = "INDICES")]
Indices,
#[serde(rename = "DERIVATIVES")]
Derivatives,
#[serde(rename = "SPOT")]
Spot,
#[serde(rename = "instrument")]
#[serde(untagged)]
Specific(String),
}
impl BitmexProductType {
#[must_use]
pub fn to_subscription(&self) -> Cow<'static, str> {
match self {
Self::All => Cow::Borrowed("instrument"),
Self::Specific(symbol) => Cow::Owned(format!("instrument:{symbol}")),
Self::Contracts => Cow::Borrowed("CONTRACTS"),
Self::Indices => Cow::Borrowed("INDICES"),
Self::Derivatives => Cow::Borrowed("DERIVATIVES"),
Self::Spot => Cow::Borrowed("SPOT"),
}
}
}
impl<'de> Deserialize<'de> for BitmexProductType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.as_str() {
"instrument" => Ok(Self::All),
"CONTRACTS" => Ok(Self::Contracts),
"INDICES" => Ok(Self::Indices),
"DERIVATIVES" => Ok(Self::Derivatives),
"SPOT" => Ok(Self::Spot),
s if s.starts_with("instrument:") => {
let symbol = s.strip_prefix("instrument:").unwrap();
Ok(Self::Specific(symbol.to_string()))
}
_ => Err(serde::de::Error::custom(format!(
"Invalid product type: {s}"
))),
}
}
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
pub enum BitmexTickDirection {
PlusTick,
MinusTick,
ZeroPlusTick,
ZeroMinusTick,
}
#[derive(
Clone,
Copy,
Debug,
Display,
PartialEq,
Eq,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
pub enum BitmexInstrumentState {
Open,
Closed,
Unlisted,
Settled,
Delisted,
}
impl From<&BitmexInstrumentState> for MarketStatusAction {
fn from(state: &BitmexInstrumentState) -> Self {
match state {
BitmexInstrumentState::Open => Self::Trading,
BitmexInstrumentState::Closed => Self::Close,
BitmexInstrumentState::Settled => Self::Close,
BitmexInstrumentState::Unlisted => Self::NotAvailableForTrading,
BitmexInstrumentState::Delisted => Self::NotAvailableForTrading,
}
}
}
#[derive(
Clone, Debug, Display, PartialEq, Eq, AsRefStr, EnumIter, EnumString, Serialize, Deserialize,
)]
pub enum BitmexFairMethod {
FundingRate,
ImpactMidPrice,
LastPrice,
}
#[derive(
Clone, Debug, Display, PartialEq, Eq, AsRefStr, EnumIter, EnumString, Serialize, Deserialize,
)]
pub enum BitmexMarkMethod {
FairPrice,
FairPriceStox,
LastPrice,
LastPricePreLaunch,
CompositeIndex,
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
fn test_bitmex_side_deserialization() {
assert_eq!(
serde_json::from_str::<BitmexSide>(r#""Buy""#).unwrap(),
BitmexSide::Buy
);
assert_eq!(
serde_json::from_str::<BitmexSide>(r#""BUY""#).unwrap(),
BitmexSide::Buy
);
assert_eq!(
serde_json::from_str::<BitmexSide>(r#""buy""#).unwrap(),
BitmexSide::Buy
);
assert_eq!(
serde_json::from_str::<BitmexSide>(r#""Sell""#).unwrap(),
BitmexSide::Sell
);
assert_eq!(
serde_json::from_str::<BitmexSide>(r#""SELL""#).unwrap(),
BitmexSide::Sell
);
assert_eq!(
serde_json::from_str::<BitmexSide>(r#""sell""#).unwrap(),
BitmexSide::Sell
);
}
#[rstest]
fn test_bitmex_order_type_deserialization() {
assert_eq!(
serde_json::from_str::<BitmexOrderType>(r#""Market""#).unwrap(),
BitmexOrderType::Market
);
assert_eq!(
serde_json::from_str::<BitmexOrderType>(r#""Limit""#).unwrap(),
BitmexOrderType::Limit
);
assert_eq!(
serde_json::from_str::<BitmexOrderType>(r#""Stop""#).unwrap(),
BitmexOrderType::Stop
);
assert_eq!(
serde_json::from_str::<BitmexOrderType>(r#""StopLimit""#).unwrap(),
BitmexOrderType::StopLimit
);
assert_eq!(
serde_json::from_str::<BitmexOrderType>(r#""MarketIfTouched""#).unwrap(),
BitmexOrderType::MarketIfTouched
);
assert_eq!(
serde_json::from_str::<BitmexOrderType>(r#""LimitIfTouched""#).unwrap(),
BitmexOrderType::LimitIfTouched
);
assert_eq!(
serde_json::from_str::<BitmexOrderType>(r#""Pegged""#).unwrap(),
BitmexOrderType::Pegged
);
}
#[rstest]
fn test_instrument_type_serialization() {
assert_eq!(
serde_json::to_string(&BitmexInstrumentType::PerpetualContract).unwrap(),
r#""FFWCSX""#
);
assert_eq!(
serde_json::to_string(&BitmexInstrumentType::PerpetualContractFx).unwrap(),
r#""FFWCSF""#
);
assert_eq!(
serde_json::to_string(&BitmexInstrumentType::StockPerpetual).unwrap(),
r#""FFSCSX""#
);
assert_eq!(
serde_json::to_string(&BitmexInstrumentType::Spot).unwrap(),
r#""IFXXXP""#
);
assert_eq!(
serde_json::to_string(&BitmexInstrumentType::Futures).unwrap(),
r#""FFCCSX""#
);
assert_eq!(
serde_json::to_string(&BitmexInstrumentType::PredictionMarket).unwrap(),
r#""FFICSX""#
);
assert_eq!(
serde_json::to_string(&BitmexInstrumentType::CallOption).unwrap(),
r#""OCECCS""#
);
assert_eq!(
serde_json::to_string(&BitmexInstrumentType::PutOption).unwrap(),
r#""OPECCS""#
);
assert_eq!(
serde_json::to_string(&BitmexInstrumentType::SwapRate).unwrap(),
r#""SRMCSX""#
);
assert_eq!(
serde_json::to_string(&BitmexInstrumentType::LegacyFutures).unwrap(),
r#""FXXXS""#
);
assert_eq!(
serde_json::to_string(&BitmexInstrumentType::LegacyFuturesN).unwrap(),
r#""FXXXN""#
);
assert_eq!(
serde_json::to_string(&BitmexInstrumentType::FuturesSpreads).unwrap(),
r#""FMXXS""#
);
assert_eq!(
serde_json::to_string(&BitmexInstrumentType::ReferenceBasket).unwrap(),
r#""RCSXXX""#
);
assert_eq!(
serde_json::to_string(&BitmexInstrumentType::BasketIndex).unwrap(),
r#""MRBXXX""#
);
assert_eq!(
serde_json::to_string(&BitmexInstrumentType::CryptoIndex).unwrap(),
r#""MRCXXX""#
);
assert_eq!(
serde_json::to_string(&BitmexInstrumentType::FxIndex).unwrap(),
r#""MRFXXX""#
);
assert_eq!(
serde_json::to_string(&BitmexInstrumentType::LendingIndex).unwrap(),
r#""MRRXXX""#
);
assert_eq!(
serde_json::to_string(&BitmexInstrumentType::VolatilityIndex).unwrap(),
r#""MRIXXX""#
);
assert_eq!(
serde_json::to_string(&BitmexInstrumentType::StockIndex).unwrap(),
r#""MRSXXX""#
);
assert_eq!(
serde_json::to_string(&BitmexInstrumentType::YieldIndex).unwrap(),
r#""MRVDXX""#
);
}
#[rstest]
fn test_instrument_type_deserialization() {
assert_eq!(
serde_json::from_str::<BitmexInstrumentType>(r#""FFWCSX""#).unwrap(),
BitmexInstrumentType::PerpetualContract
);
assert_eq!(
serde_json::from_str::<BitmexInstrumentType>(r#""FFWCSF""#).unwrap(),
BitmexInstrumentType::PerpetualContractFx
);
assert_eq!(
serde_json::from_str::<BitmexInstrumentType>(r#""FFSCSX""#).unwrap(),
BitmexInstrumentType::StockPerpetual
);
assert_eq!(
serde_json::from_str::<BitmexInstrumentType>(r#""IFXXXP""#).unwrap(),
BitmexInstrumentType::Spot
);
assert_eq!(
serde_json::from_str::<BitmexInstrumentType>(r#""FFCCSX""#).unwrap(),
BitmexInstrumentType::Futures
);
assert_eq!(
serde_json::from_str::<BitmexInstrumentType>(r#""FFICSX""#).unwrap(),
BitmexInstrumentType::PredictionMarket
);
assert_eq!(
serde_json::from_str::<BitmexInstrumentType>(r#""OCECCS""#).unwrap(),
BitmexInstrumentType::CallOption
);
assert_eq!(
serde_json::from_str::<BitmexInstrumentType>(r#""OPECCS""#).unwrap(),
BitmexInstrumentType::PutOption
);
assert_eq!(
serde_json::from_str::<BitmexInstrumentType>(r#""SRMCSX""#).unwrap(),
BitmexInstrumentType::SwapRate
);
assert_eq!(
serde_json::from_str::<BitmexInstrumentType>(r#""FXXXS""#).unwrap(),
BitmexInstrumentType::LegacyFutures
);
assert_eq!(
serde_json::from_str::<BitmexInstrumentType>(r#""FXXXN""#).unwrap(),
BitmexInstrumentType::LegacyFuturesN
);
assert_eq!(
serde_json::from_str::<BitmexInstrumentType>(r#""FMXXS""#).unwrap(),
BitmexInstrumentType::FuturesSpreads
);
assert_eq!(
serde_json::from_str::<BitmexInstrumentType>(r#""RCSXXX""#).unwrap(),
BitmexInstrumentType::ReferenceBasket
);
assert_eq!(
serde_json::from_str::<BitmexInstrumentType>(r#""MRBXXX""#).unwrap(),
BitmexInstrumentType::BasketIndex
);
assert_eq!(
serde_json::from_str::<BitmexInstrumentType>(r#""MRCXXX""#).unwrap(),
BitmexInstrumentType::CryptoIndex
);
assert_eq!(
serde_json::from_str::<BitmexInstrumentType>(r#""MRFXXX""#).unwrap(),
BitmexInstrumentType::FxIndex
);
assert_eq!(
serde_json::from_str::<BitmexInstrumentType>(r#""MRRXXX""#).unwrap(),
BitmexInstrumentType::LendingIndex
);
assert_eq!(
serde_json::from_str::<BitmexInstrumentType>(r#""MRIXXX""#).unwrap(),
BitmexInstrumentType::VolatilityIndex
);
assert_eq!(
serde_json::from_str::<BitmexInstrumentType>(r#""MRSXXX""#).unwrap(),
BitmexInstrumentType::StockIndex
);
assert_eq!(
serde_json::from_str::<BitmexInstrumentType>(r#""MRVDXX""#).unwrap(),
BitmexInstrumentType::YieldIndex
);
assert!(serde_json::from_str::<BitmexInstrumentType>(r#""INVALID""#).is_err());
}
#[rstest]
fn test_subscription_strings() {
assert_eq!(BitmexProductType::All.to_subscription(), "instrument");
assert_eq!(
BitmexProductType::Specific("XBTUSD".to_string()).to_subscription(),
"instrument:XBTUSD"
);
assert_eq!(BitmexProductType::Contracts.to_subscription(), "CONTRACTS");
assert_eq!(BitmexProductType::Indices.to_subscription(), "INDICES");
assert_eq!(
BitmexProductType::Derivatives.to_subscription(),
"DERIVATIVES"
);
assert_eq!(BitmexProductType::Spot.to_subscription(), "SPOT");
}
#[rstest]
fn test_serialization() {
assert_eq!(
serde_json::to_string(&BitmexProductType::All).unwrap(),
r#""instrument""#
);
assert_eq!(
serde_json::to_string(&BitmexProductType::Specific("XBTUSD".to_string())).unwrap(),
r#""XBTUSD""#
);
assert_eq!(
serde_json::to_string(&BitmexProductType::Contracts).unwrap(),
r#""CONTRACTS""#
);
}
#[rstest]
fn test_deserialization() {
assert_eq!(
serde_json::from_str::<BitmexProductType>(r#""instrument""#).unwrap(),
BitmexProductType::All
);
assert_eq!(
serde_json::from_str::<BitmexProductType>(r#""instrument:XBTUSD""#).unwrap(),
BitmexProductType::Specific("XBTUSD".to_string())
);
assert_eq!(
serde_json::from_str::<BitmexProductType>(r#""CONTRACTS""#).unwrap(),
BitmexProductType::Contracts
);
}
#[rstest]
fn test_error_cases() {
assert!(serde_json::from_str::<BitmexProductType>(r#""invalid_type""#).is_err());
assert!(serde_json::from_str::<BitmexProductType>("123").is_err());
assert!(serde_json::from_str::<BitmexProductType>("{}").is_err());
}
#[rstest]
fn test_order_side_from_specified() {
assert_eq!(BitmexSide::from(OrderSideSpecified::Buy), BitmexSide::Buy);
assert_eq!(BitmexSide::from(OrderSideSpecified::Sell), BitmexSide::Sell);
}
#[rstest]
fn test_order_type_try_from() {
assert_eq!(
BitmexOrderType::try_from(OrderType::Market).unwrap(),
BitmexOrderType::Market
);
assert_eq!(
BitmexOrderType::try_from(OrderType::Limit).unwrap(),
BitmexOrderType::Limit
);
let result = BitmexOrderType::try_from(OrderType::MarketToLimit);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("not supported"));
}
#[rstest]
fn test_time_in_force_conversions() {
assert_eq!(
TimeInForce::try_from(BitmexTimeInForce::Day).unwrap(),
TimeInForce::Day
);
assert_eq!(
TimeInForce::try_from(BitmexTimeInForce::GoodTillCancel).unwrap(),
TimeInForce::Gtc
);
assert_eq!(
TimeInForce::try_from(BitmexTimeInForce::ImmediateOrCancel).unwrap(),
TimeInForce::Ioc
);
let result = TimeInForce::try_from(BitmexTimeInForce::GoodTillCrossing);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Unsupported"));
assert_eq!(
BitmexTimeInForce::try_from(TimeInForce::Day).unwrap(),
BitmexTimeInForce::Day
);
assert_eq!(
BitmexTimeInForce::try_from(TimeInForce::Gtc).unwrap(),
BitmexTimeInForce::GoodTillCancel
);
assert_eq!(
BitmexTimeInForce::try_from(TimeInForce::Fok).unwrap(),
BitmexTimeInForce::FillOrKill
);
}
#[rstest]
fn test_helper_methods() {
let result = BitmexOrderType::try_from_order_type(OrderType::Limit);
assert!(result.is_ok());
assert_eq!(result.unwrap(), BitmexOrderType::Limit);
let result = BitmexOrderType::try_from_order_type(OrderType::MarketToLimit);
assert!(result.is_err());
let result = BitmexTimeInForce::try_from_time_in_force(TimeInForce::Ioc);
assert!(result.is_ok());
assert_eq!(result.unwrap(), BitmexTimeInForce::ImmediateOrCancel);
}
#[rstest]
#[case(BitmexInstrumentState::Open, MarketStatusAction::Trading)]
#[case(BitmexInstrumentState::Closed, MarketStatusAction::Close)]
#[case(BitmexInstrumentState::Settled, MarketStatusAction::Close)]
#[case(
BitmexInstrumentState::Unlisted,
MarketStatusAction::NotAvailableForTrading
)]
#[case(
BitmexInstrumentState::Delisted,
MarketStatusAction::NotAvailableForTrading
)]
fn test_bitmex_instrument_state_to_market_status_action(
#[case] state: BitmexInstrumentState,
#[case] expected: MarketStatusAction,
) {
assert_eq!(MarketStatusAction::from(&state), expected);
}
}