use nautilus_model::enums::OrderSide;
use rust_decimal::Decimal;
use crate::common::enums::BinancePositionSide;
#[must_use]
pub(crate) fn determine_position_side(
is_hedge_mode: bool,
order_side: OrderSide,
reduce_only: bool,
) -> Option<BinancePositionSide> {
if !is_hedge_mode {
return None;
}
Some(if reduce_only {
match order_side {
OrderSide::Buy => BinancePositionSide::Short,
OrderSide::Sell => BinancePositionSide::Long,
_ => BinancePositionSide::Both,
}
} else {
match order_side {
OrderSide::Buy => BinancePositionSide::Long,
OrderSide::Sell => BinancePositionSide::Short,
_ => BinancePositionSide::Both,
}
})
}
pub(crate) fn trailing_offset_to_callback_rate(offset: Decimal) -> anyhow::Result<Decimal> {
let rate = offset / rust_decimal::Decimal::ONE_HUNDRED;
let min_rate = rust_decimal::Decimal::new(1, 1);
let max_rate = rust_decimal::Decimal::new(100, 1);
if rate < min_rate || rate > max_rate {
anyhow::bail!("callbackRate {rate}% out of Binance range [{min_rate}, {max_rate}]");
}
Ok(rate)
}
pub(crate) fn trailing_offset_to_callback_rate_string(offset: Decimal) -> anyhow::Result<String> {
let rate = trailing_offset_to_callback_rate(offset)?;
Ok(format_callback_rate(rate))
}
#[must_use]
pub(crate) fn format_callback_rate(rate: Decimal) -> String {
let normalized = rate.normalize();
if normalized.scale() == 0 {
format!("{normalized}.0")
} else {
normalized.to_string()
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
fn test_trailing_offset_to_callback_rate_preserves_precision() {
let rate = trailing_offset_to_callback_rate(Decimal::from(25)).unwrap();
assert_eq!(rate, Decimal::new(25, 2));
}
#[rstest]
fn test_trailing_offset_to_callback_rate_string_formats_whole_percent() {
let rate = trailing_offset_to_callback_rate_string(Decimal::from(100)).unwrap();
assert_eq!(rate, "1.0");
}
#[rstest]
fn test_trailing_offset_to_callback_rate_rejects_out_of_range_values() {
let error = trailing_offset_to_callback_rate(Decimal::from(5)).unwrap_err();
assert_eq!(
error.to_string(),
"callbackRate 0.05% out of Binance range [0.1, 10.0]"
);
}
#[rstest]
#[case::one_way_buy(false, OrderSide::Buy, false, None)]
#[case::one_way_sell(false, OrderSide::Sell, false, None)]
#[case::one_way_buy_reduce(false, OrderSide::Buy, true, None)]
#[case::hedge_open_buy(true, OrderSide::Buy, false, Some(BinancePositionSide::Long))]
#[case::hedge_open_sell(true, OrderSide::Sell, false, Some(BinancePositionSide::Short))]
#[case::hedge_close_buy(true, OrderSide::Buy, true, Some(BinancePositionSide::Short))]
#[case::hedge_close_sell(true, OrderSide::Sell, true, Some(BinancePositionSide::Long))]
#[case::hedge_no_side(true, OrderSide::NoOrderSide, false, Some(BinancePositionSide::Both))]
fn test_determine_position_side(
#[case] is_hedge_mode: bool,
#[case] order_side: OrderSide,
#[case] reduce_only: bool,
#[case] expected: Option<BinancePositionSide>,
) {
assert_eq!(
determine_position_side(is_hedge_mode, order_side, reduce_only),
expected,
);
}
}