nautilus_binance/futures/
conversions.rs1use nautilus_model::enums::OrderSide;
19use rust_decimal::Decimal;
20
21use crate::common::enums::BinancePositionSide;
22
23#[must_use]
29pub(crate) fn determine_position_side(
30 is_hedge_mode: bool,
31 order_side: OrderSide,
32 reduce_only: bool,
33) -> Option<BinancePositionSide> {
34 if !is_hedge_mode {
35 return None;
36 }
37
38 Some(if reduce_only {
39 match order_side {
40 OrderSide::Buy => BinancePositionSide::Short,
41 OrderSide::Sell => BinancePositionSide::Long,
42 _ => BinancePositionSide::Both,
43 }
44 } else {
45 match order_side {
46 OrderSide::Buy => BinancePositionSide::Long,
47 OrderSide::Sell => BinancePositionSide::Short,
48 _ => BinancePositionSide::Both,
49 }
50 })
51}
52
53pub(crate) fn trailing_offset_to_callback_rate(offset: Decimal) -> anyhow::Result<Decimal> {
60 let rate = offset / rust_decimal::Decimal::ONE_HUNDRED;
61 let min_rate = rust_decimal::Decimal::new(1, 1);
62 let max_rate = rust_decimal::Decimal::new(100, 1);
63
64 if rate < min_rate || rate > max_rate {
65 anyhow::bail!("callbackRate {rate}% out of Binance range [{min_rate}, {max_rate}]");
66 }
67
68 Ok(rate)
69}
70
71pub(crate) fn trailing_offset_to_callback_rate_string(offset: Decimal) -> anyhow::Result<String> {
77 let rate = trailing_offset_to_callback_rate(offset)?;
78 Ok(format_callback_rate(rate))
79}
80
81#[must_use]
85pub(crate) fn format_callback_rate(rate: Decimal) -> String {
86 let normalized = rate.normalize();
87
88 if normalized.scale() == 0 {
89 format!("{normalized}.0")
90 } else {
91 normalized.to_string()
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use rstest::rstest;
98
99 use super::*;
100
101 #[rstest]
102 fn test_trailing_offset_to_callback_rate_preserves_precision() {
103 let rate = trailing_offset_to_callback_rate(Decimal::from(25)).unwrap();
104 assert_eq!(rate, Decimal::new(25, 2));
105 }
106
107 #[rstest]
108 fn test_trailing_offset_to_callback_rate_string_formats_whole_percent() {
109 let rate = trailing_offset_to_callback_rate_string(Decimal::from(100)).unwrap();
110 assert_eq!(rate, "1.0");
111 }
112
113 #[rstest]
114 fn test_trailing_offset_to_callback_rate_rejects_out_of_range_values() {
115 let error = trailing_offset_to_callback_rate(Decimal::from(5)).unwrap_err();
116 assert_eq!(
117 error.to_string(),
118 "callbackRate 0.05% out of Binance range [0.1, 10.0]"
119 );
120 }
121
122 #[rstest]
123 #[case::one_way_buy(false, OrderSide::Buy, false, None)]
124 #[case::one_way_sell(false, OrderSide::Sell, false, None)]
125 #[case::one_way_buy_reduce(false, OrderSide::Buy, true, None)]
126 #[case::hedge_open_buy(true, OrderSide::Buy, false, Some(BinancePositionSide::Long))]
127 #[case::hedge_open_sell(true, OrderSide::Sell, false, Some(BinancePositionSide::Short))]
128 #[case::hedge_close_buy(true, OrderSide::Buy, true, Some(BinancePositionSide::Short))]
129 #[case::hedge_close_sell(true, OrderSide::Sell, true, Some(BinancePositionSide::Long))]
130 #[case::hedge_no_side(true, OrderSide::NoOrderSide, false, Some(BinancePositionSide::Both))]
131 fn test_determine_position_side(
132 #[case] is_hedge_mode: bool,
133 #[case] order_side: OrderSide,
134 #[case] reduce_only: bool,
135 #[case] expected: Option<BinancePositionSide>,
136 ) {
137 assert_eq!(
138 determine_position_side(is_hedge_mode, order_side, reduce_only),
139 expected,
140 );
141 }
142}