use nautilus_model::{enums::OrderSide, types::Currency};
use rust_decimal::Decimal;
use crate::common::enums::BinancePositionSide;
const BNFCR_ASSET: &str = "BNFCR";
#[must_use]
pub(crate) fn normalize_futures_asset<T: AsRef<str>>(
asset: T,
bnfcr_currency: Currency,
) -> Currency {
let code = asset.as_ref().trim();
if code.eq_ignore_ascii_case(BNFCR_ASSET) {
bnfcr_currency
} else {
Currency::get_or_create_crypto_with_context(code, Some("futures asset"))
}
}
#[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,
}
})
}
#[must_use]
pub(crate) const fn reduce_only_param(
reduce_only: bool,
position_side: Option<BinancePositionSide>,
) -> Option<bool> {
if reduce_only && position_side.is_none() {
Some(true)
} else {
None
}
}
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 nautilus_model::enums::CurrencyType;
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,
);
}
#[rstest]
#[case::one_way(false, None, None)]
#[case::one_way_reduce(true, None, Some(true))]
#[case::hedge_open(false, Some(BinancePositionSide::Long), None)]
#[case::hedge_close_long(true, Some(BinancePositionSide::Long), None)]
#[case::hedge_close_short(true, Some(BinancePositionSide::Short), None)]
fn test_reduce_only_param(
#[case] reduce_only: bool,
#[case] position_side: Option<BinancePositionSide>,
#[case] expected: Option<bool>,
) {
assert_eq!(reduce_only_param(reduce_only, position_side), expected);
}
#[rstest]
#[case::bnfcr_to_usdt("BNFCR", Currency::USDT(), Currency::USDT())]
#[case::bnfcr_to_usdc("BNFCR", Currency::USDC(), Currency::USDC())]
#[case::bnfcr_trim_and_case(" bnfcr ", Currency::USDC(), Currency::USDC())]
#[case::known_asset_bypasses_alias("USDT", Currency::USDC(), Currency::USDT())]
fn test_normalize_futures_asset_resolves_currency(
#[case] asset: &str,
#[case] bnfcr_currency: Currency,
#[case] expected: Currency,
) {
assert_eq!(normalize_futures_asset(asset, bnfcr_currency), expected);
}
#[rstest]
fn test_normalize_futures_asset_registers_unknown_as_crypto() {
let currency = normalize_futures_asset("XYZ", Currency::USDT());
assert_eq!(currency.code.as_str(), "XYZ");
assert_eq!(currency.currency_type, CurrencyType::Crypto);
}
}