use anyhow::Context;
use nautilus_core::{UUID4, UnixNanos};
use nautilus_model::{
enums::{AccountType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce},
events::AccountState,
identifiers::{AccountId, ClientOrderId, InstrumentId, Symbol, TradeId, Venue, VenueOrderId},
reports::{FillReport, OrderStatusReport},
types::{AccountBalance, Currency, MarginBalance, Money, Price, Quantity},
};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use ustr::Ustr;
use crate::common::{
consts::BINANCE_NAUTILUS_FUTURES_BROKER_ID,
encoder::decode_broker_id,
enums::{
BinanceAlgoStatus, BinanceAlgoType, BinanceContractStatus, BinanceFuturesOrderType,
BinanceIncomeType, BinanceMarginType, BinanceOrderStatus, BinancePositionSide,
BinancePriceMatch, BinanceSelfTradePreventionMode, BinanceSide, BinanceTimeInForce,
BinanceTradingStatus, BinanceWorkingType,
},
models::BinanceRateLimit,
};
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceServerTime {
pub server_time: i64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceFuturesTrade {
pub id: i64,
pub price: String,
pub qty: String,
pub quote_qty: String,
pub time: i64,
pub is_buyer_maker: bool,
}
#[derive(Clone, Debug)]
pub struct BinanceFuturesKline {
pub open_time: i64,
pub open: String,
pub high: String,
pub low: String,
pub close: String,
pub volume: String,
pub close_time: i64,
pub quote_volume: String,
pub num_trades: i64,
pub taker_buy_base_volume: String,
pub taker_buy_quote_volume: String,
}
impl<'de> Deserialize<'de> for BinanceFuturesKline {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let arr: Vec<Value> = Vec::deserialize(deserializer)?;
if arr.len() < 11 {
return Err(serde::de::Error::custom("Invalid kline array length"));
}
Ok(Self {
open_time: arr[0].as_i64().unwrap_or(0),
open: arr[1].as_str().unwrap_or("0").to_string(),
high: arr[2].as_str().unwrap_or("0").to_string(),
low: arr[3].as_str().unwrap_or("0").to_string(),
close: arr[4].as_str().unwrap_or("0").to_string(),
volume: arr[5].as_str().unwrap_or("0").to_string(),
close_time: arr[6].as_i64().unwrap_or(0),
quote_volume: arr[7].as_str().unwrap_or("0").to_string(),
num_trades: arr[8].as_i64().unwrap_or(0),
taker_buy_base_volume: arr[9].as_str().unwrap_or("0").to_string(),
taker_buy_quote_volume: arr[10].as_str().unwrap_or("0").to_string(),
})
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceFuturesUsdExchangeInfo {
pub timezone: String,
pub server_time: i64,
pub rate_limits: Vec<BinanceRateLimit>,
#[serde(default)]
pub exchange_filters: Vec<Value>,
#[serde(default)]
pub assets: Vec<BinanceFuturesAsset>,
pub symbols: Vec<BinanceFuturesUsdSymbol>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceFuturesAsset {
pub asset: Ustr,
pub margin_available: bool,
#[serde(default)]
pub auto_asset_exchange: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceFuturesUsdSymbol {
pub symbol: Ustr,
pub pair: Ustr,
pub contract_type: String,
pub delivery_date: i64,
pub onboard_date: i64,
pub status: BinanceTradingStatus,
pub maint_margin_percent: String,
pub required_margin_percent: String,
pub base_asset: Ustr,
pub quote_asset: Ustr,
pub margin_asset: Ustr,
pub price_precision: i32,
pub quantity_precision: i32,
pub base_asset_precision: i32,
pub quote_precision: i32,
#[serde(default)]
pub underlying_type: Option<String>,
#[serde(default)]
pub underlying_sub_type: Vec<String>,
#[serde(default)]
pub settle_plan: Option<i64>,
#[serde(default)]
pub trigger_protect: Option<String>,
#[serde(default)]
pub liquidation_fee: Option<String>,
#[serde(default)]
pub market_take_bound: Option<String>,
pub order_types: Vec<String>,
pub time_in_force: Vec<String>,
pub filters: Vec<Value>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceFuturesCoinExchangeInfo {
pub timezone: String,
pub server_time: i64,
pub rate_limits: Vec<BinanceRateLimit>,
#[serde(default)]
pub exchange_filters: Vec<Value>,
pub symbols: Vec<BinanceFuturesCoinSymbol>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceFuturesCoinSymbol {
pub symbol: Ustr,
pub pair: Ustr,
pub contract_type: String,
pub delivery_date: i64,
pub onboard_date: i64,
#[serde(default)]
pub contract_status: Option<BinanceContractStatus>,
pub contract_size: i64,
pub maint_margin_percent: String,
pub required_margin_percent: String,
pub base_asset: Ustr,
pub quote_asset: Ustr,
pub margin_asset: Ustr,
pub price_precision: i32,
pub quantity_precision: i32,
pub base_asset_precision: i32,
pub quote_precision: i32,
#[serde(default, rename = "equalQtyPrecision")]
pub equal_qty_precision: Option<i32>,
#[serde(default)]
pub trigger_protect: Option<String>,
#[serde(default)]
pub liquidation_fee: Option<String>,
#[serde(default)]
pub market_take_bound: Option<String>,
pub order_types: Vec<String>,
pub time_in_force: Vec<String>,
pub filters: Vec<Value>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceFuturesTicker24hr {
pub symbol: Ustr,
pub price_change: String,
pub price_change_percent: String,
pub weighted_avg_price: String,
pub last_price: String,
#[serde(default)]
pub last_qty: Option<String>,
pub open_price: String,
pub high_price: String,
pub low_price: String,
pub volume: String,
pub quote_volume: String,
pub open_time: i64,
pub close_time: i64,
#[serde(default)]
pub first_id: Option<i64>,
#[serde(default)]
pub last_id: Option<i64>,
#[serde(default)]
pub count: Option<i64>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceFuturesMarkPrice {
pub symbol: Ustr,
pub mark_price: String,
#[serde(default)]
pub index_price: Option<String>,
#[serde(default)]
pub estimated_settle_price: Option<String>,
#[serde(default)]
pub last_funding_rate: Option<String>,
#[serde(default)]
pub next_funding_time: Option<i64>,
#[serde(default)]
pub interest_rate: Option<String>,
pub time: i64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceOrderBook {
pub last_update_id: i64,
pub bids: Vec<(String, String)>,
pub asks: Vec<(String, String)>,
#[serde(default, rename = "E")]
pub event_time: Option<i64>,
#[serde(default, rename = "T")]
pub transaction_time: Option<i64>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceBookTicker {
pub symbol: Ustr,
pub bid_price: String,
pub bid_qty: String,
pub ask_price: String,
pub ask_qty: String,
#[serde(default)]
pub time: Option<i64>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinancePriceTicker {
pub symbol: Ustr,
pub price: String,
#[serde(default)]
pub time: Option<i64>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceFundingRate {
pub symbol: Ustr,
pub funding_rate: String,
pub funding_time: i64,
#[serde(default)]
pub mark_price: Option<String>,
#[serde(default)]
pub index_price: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceOpenInterest {
pub symbol: Ustr,
pub open_interest: String,
pub time: i64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceFuturesBalance {
#[serde(default)]
pub account_alias: Option<String>,
pub asset: Ustr,
#[serde(alias = "balance")]
pub wallet_balance: String,
#[serde(default)]
pub unrealized_profit: Option<String>,
#[serde(default)]
pub margin_balance: Option<String>,
#[serde(default)]
pub maint_margin: Option<String>,
#[serde(default)]
pub initial_margin: Option<String>,
#[serde(default)]
pub position_initial_margin: Option<String>,
#[serde(default)]
pub open_order_initial_margin: Option<String>,
#[serde(default)]
pub cross_wallet_balance: Option<String>,
#[serde(default)]
pub cross_un_pnl: Option<String>,
pub available_balance: String,
#[serde(default)]
pub max_withdraw_amount: Option<String>,
#[serde(default)]
pub margin_available: Option<bool>,
pub update_time: i64,
#[serde(default)]
pub withdraw_available: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceAccountPosition {
pub symbol: Ustr,
#[serde(default)]
pub initial_margin: Option<String>,
#[serde(default)]
pub maint_margin: Option<String>,
#[serde(default)]
pub unrealized_profit: Option<String>,
#[serde(default)]
pub position_initial_margin: Option<String>,
#[serde(default)]
pub open_order_initial_margin: Option<String>,
#[serde(default)]
pub leverage: Option<String>,
#[serde(default)]
pub isolated: Option<bool>,
#[serde(default)]
pub entry_price: Option<String>,
#[serde(default)]
pub max_notional: Option<String>,
#[serde(default)]
pub bid_notional: Option<String>,
#[serde(default)]
pub ask_notional: Option<String>,
#[serde(default)]
pub position_side: Option<BinancePositionSide>,
#[serde(default)]
pub position_amt: Option<String>,
#[serde(default)]
pub update_time: Option<i64>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinancePositionRisk {
pub symbol: Ustr,
pub position_amt: String,
pub entry_price: String,
pub mark_price: String,
#[serde(default)]
pub un_realized_profit: Option<String>,
#[serde(default)]
pub liquidation_price: Option<String>,
pub leverage: String,
#[serde(default)]
pub max_notional_value: Option<String>,
#[serde(default)]
pub margin_type: Option<BinanceMarginType>,
#[serde(default)]
pub isolated_margin: Option<String>,
#[serde(default)]
pub is_auto_add_margin: Option<String>,
#[serde(default)]
pub position_side: Option<BinancePositionSide>,
#[serde(default)]
pub notional: Option<String>,
#[serde(default)]
pub isolated_wallet: Option<String>,
#[serde(default)]
pub adl_quantile: Option<u8>,
#[serde(default)]
pub update_time: Option<i64>,
#[serde(default)]
pub break_even_price: Option<String>,
#[serde(default)]
pub bust_price: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceIncomeRecord {
#[serde(default)]
pub symbol: Option<Ustr>,
pub income_type: BinanceIncomeType,
pub income: String,
pub asset: Ustr,
pub time: i64,
#[serde(default)]
pub info: Option<String>,
#[serde(default)]
pub tran_id: Option<i64>,
#[serde(default)]
pub trade_id: Option<i64>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceUserTrade {
pub symbol: Ustr,
pub id: i64,
pub order_id: i64,
pub price: String,
pub qty: String,
#[serde(default)]
pub quote_qty: Option<String>,
pub realized_pnl: String,
pub side: BinanceSide,
#[serde(default)]
pub position_side: Option<BinancePositionSide>,
pub time: i64,
pub buyer: bool,
pub maker: bool,
#[serde(default)]
pub commission: Option<String>,
#[serde(default)]
pub commission_asset: Option<Ustr>,
#[serde(default)]
pub margin_asset: Option<Ustr>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceFuturesAccountInfo {
#[serde(default)]
pub total_initial_margin: Option<String>,
#[serde(default)]
pub total_maint_margin: Option<String>,
#[serde(default)]
pub total_wallet_balance: Option<String>,
#[serde(default)]
pub total_unrealized_profit: Option<String>,
#[serde(default)]
pub total_margin_balance: Option<String>,
#[serde(default)]
pub total_position_initial_margin: Option<String>,
#[serde(default)]
pub total_open_order_initial_margin: Option<String>,
#[serde(default)]
pub total_cross_wallet_balance: Option<String>,
#[serde(default)]
pub total_cross_un_pnl: Option<String>,
#[serde(default)]
pub available_balance: Option<String>,
#[serde(default)]
pub max_withdraw_amount: Option<String>,
#[serde(default)]
pub can_deposit: Option<bool>,
#[serde(default)]
pub can_trade: Option<bool>,
#[serde(default)]
pub can_withdraw: Option<bool>,
#[serde(default)]
pub multi_assets_margin: Option<bool>,
#[serde(default)]
pub update_time: Option<i64>,
#[serde(default)]
pub assets: Vec<BinanceFuturesBalance>,
#[serde(default)]
pub positions: Vec<BinanceAccountPosition>,
}
impl BinanceFuturesAccountInfo {
pub fn to_account_state(
&self,
account_id: AccountId,
ts_init: UnixNanos,
) -> anyhow::Result<AccountState> {
let mut balances = Vec::with_capacity(self.assets.len());
for asset in &self.assets {
let currency = Currency::get_or_create_crypto_with_context(
asset.asset.as_str(),
Some("futures balance"),
);
let total: Decimal = if asset.wallet_balance.is_empty() {
Decimal::ZERO
} else {
asset.wallet_balance.parse().context("invalid balance")?
};
let available: Decimal = if asset.available_balance.is_empty() {
Decimal::ZERO
} else {
asset
.available_balance
.parse()
.context("invalid available_balance")?
};
let locked = total - available;
let total_money = Money::from_decimal(total, currency)
.unwrap_or_else(|_| Money::new(total.to_string().parse().unwrap_or(0.0), currency));
let locked_money = Money::from_decimal(locked, currency).unwrap_or_else(|_| {
Money::new(locked.to_string().parse().unwrap_or(0.0), currency)
});
let free_money = Money::from_decimal(available, currency).unwrap_or_else(|_| {
Money::new(available.to_string().parse().unwrap_or(0.0), currency)
});
let balance = AccountBalance::new(total_money, locked_money, free_money);
balances.push(balance);
}
if balances.is_empty() {
let zero_currency = Currency::USDT();
let zero_money = Money::new(0.0, zero_currency);
let zero_balance = AccountBalance::new(zero_money, zero_money, zero_money);
balances.push(zero_balance);
}
let mut margins = Vec::new();
let initial_margin_dec = self
.total_initial_margin
.as_ref()
.and_then(|s| Decimal::from_str_exact(s).ok());
let maint_margin_dec = self
.total_maint_margin
.as_ref()
.and_then(|s| Decimal::from_str_exact(s).ok());
if let (Some(initial_margin_dec), Some(maint_margin_dec)) =
(initial_margin_dec, maint_margin_dec)
{
let has_margin = !initial_margin_dec.is_zero() || !maint_margin_dec.is_zero();
if has_margin {
let margin_currency = Currency::USDT();
let margin_instrument_id =
InstrumentId::new(Symbol::new("ACCOUNT"), Venue::new("BINANCE"));
let initial_margin = Money::from_decimal(initial_margin_dec, margin_currency)
.unwrap_or_else(|_| Money::zero(margin_currency));
let maintenance_margin = Money::from_decimal(maint_margin_dec, margin_currency)
.unwrap_or_else(|_| Money::zero(margin_currency));
let margin_balance =
MarginBalance::new(initial_margin, maintenance_margin, margin_instrument_id);
margins.push(margin_balance);
}
}
let ts_event = self
.update_time
.map_or(ts_init, |t| UnixNanos::from_millis(t as u64));
Ok(AccountState::new(
account_id,
AccountType::Margin,
balances,
margins,
true, UUID4::new(),
ts_event,
ts_init,
None,
))
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceHedgeModeResponse {
pub dual_side_position: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceLeverageResponse {
pub symbol: Ustr,
pub leverage: u32,
#[serde(default)]
pub max_notional_value: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceCancelAllOrdersResponse {
pub code: i32,
pub msg: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceFuturesOrder {
pub symbol: Ustr,
pub order_id: i64,
pub client_order_id: String,
pub orig_qty: String,
pub executed_qty: String,
pub cum_quote: String,
pub price: String,
#[serde(default)]
pub avg_price: Option<String>,
#[serde(default)]
pub stop_price: Option<String>,
pub status: BinanceOrderStatus,
pub time_in_force: BinanceTimeInForce,
#[serde(rename = "type")]
pub order_type: BinanceFuturesOrderType,
#[serde(default)]
pub orig_type: Option<BinanceFuturesOrderType>,
pub side: BinanceSide,
#[serde(default)]
pub position_side: Option<BinancePositionSide>,
#[serde(default)]
pub reduce_only: Option<bool>,
#[serde(default)]
pub close_position: Option<bool>,
#[serde(default)]
pub activate_price: Option<String>,
#[serde(default)]
pub price_rate: Option<String>,
#[serde(default)]
pub working_type: Option<BinanceWorkingType>,
#[serde(default)]
pub price_protect: Option<bool>,
#[serde(default)]
pub is_isolated: Option<bool>,
#[serde(default)]
pub good_till_date: Option<i64>,
#[serde(default)]
pub price_match: Option<BinancePriceMatch>,
#[serde(default)]
pub self_trade_prevention_mode: Option<BinanceSelfTradePreventionMode>,
#[serde(default)]
pub update_time: Option<i64>,
#[serde(default)]
pub working_type_id: Option<i64>,
}
impl BinanceFuturesOrder {
pub fn to_order_status_report(
&self,
account_id: AccountId,
instrument_id: InstrumentId,
size_precision: u8,
treat_expired_as_canceled: bool,
ts_init: UnixNanos,
) -> anyhow::Result<OrderStatusReport> {
let ts_event = self
.update_time
.map_or(ts_init, |t| UnixNanos::from_millis(t as u64));
let client_order_id = ClientOrderId::new(decode_broker_id(
&self.client_order_id,
BINANCE_NAUTILUS_FUTURES_BROKER_ID,
));
let venue_order_id = VenueOrderId::new(self.order_id.to_string());
let order_side = match self.side {
BinanceSide::Buy => OrderSide::Buy,
BinanceSide::Sell => OrderSide::Sell,
};
let order_type = self.order_type.to_nautilus_order_type();
let time_in_force = self.time_in_force.to_nautilus_time_in_force();
let order_status = self
.status
.to_nautilus_order_status(treat_expired_as_canceled);
let quantity: Decimal = self.orig_qty.parse().context("invalid orig_qty")?;
let filled_qty: Decimal = self.executed_qty.parse().context("invalid executed_qty")?;
Ok(OrderStatusReport::new(
account_id,
instrument_id,
Some(client_order_id),
venue_order_id,
order_side,
order_type,
time_in_force,
order_status,
Quantity::new(quantity.to_string().parse()?, size_precision),
Quantity::new(filled_qty.to_string().parse()?, size_precision),
ts_event,
ts_event,
ts_init,
Some(UUID4::new()),
))
}
}
impl BinanceFuturesOrderType {
#[must_use]
pub fn is_post_only(&self) -> bool {
false }
#[must_use]
pub fn to_nautilus_order_type(&self) -> OrderType {
match self {
Self::Market => OrderType::Market,
Self::Limit => OrderType::Limit,
Self::Stop => OrderType::StopLimit,
Self::StopMarket => OrderType::StopMarket,
Self::TakeProfit => OrderType::LimitIfTouched,
Self::TakeProfitMarket => OrderType::MarketIfTouched,
Self::TrailingStopMarket => OrderType::TrailingStopMarket,
Self::Liquidation | Self::Adl => OrderType::Market, Self::Unknown => OrderType::Market,
}
}
}
impl BinanceTimeInForce {
#[must_use]
pub fn to_nautilus_time_in_force(&self) -> TimeInForce {
match self {
Self::Gtc => TimeInForce::Gtc,
Self::Ioc => TimeInForce::Ioc,
Self::Fok => TimeInForce::Fok,
Self::Gtx => TimeInForce::Gtc, Self::Gtd => TimeInForce::Gtd,
Self::Rpi => TimeInForce::Ioc, Self::Unknown => TimeInForce::Gtc, }
}
}
impl BinanceOrderStatus {
#[must_use]
pub fn to_nautilus_order_status(&self, treat_expired_as_canceled: bool) -> OrderStatus {
match self {
Self::New | Self::PendingNew => OrderStatus::Accepted,
Self::PartiallyFilled => OrderStatus::PartiallyFilled,
Self::Filled | Self::NewAdl | Self::NewInsurance => OrderStatus::Filled,
Self::Canceled => OrderStatus::Canceled,
Self::PendingCancel => OrderStatus::PendingCancel,
Self::Rejected => OrderStatus::Rejected,
Self::Expired | Self::ExpiredInMatch => {
if treat_expired_as_canceled {
OrderStatus::Canceled
} else {
OrderStatus::Expired
}
}
Self::Unknown => OrderStatus::Initialized,
}
}
}
impl BinanceUserTrade {
pub fn to_fill_report(
&self,
account_id: AccountId,
instrument_id: InstrumentId,
price_precision: u8,
size_precision: u8,
ts_init: UnixNanos,
) -> anyhow::Result<FillReport> {
let ts_event = UnixNanos::from_millis(self.time as u64);
let venue_order_id = VenueOrderId::new(self.order_id.to_string());
let trade_id = TradeId::new(self.id.to_string());
let order_side = match self.side {
BinanceSide::Buy => OrderSide::Buy,
BinanceSide::Sell => OrderSide::Sell,
};
let liquidity_side = if self.maker {
LiquiditySide::Maker
} else {
LiquiditySide::Taker
};
let last_qty: Decimal = self.qty.parse().context("invalid qty")?;
let last_px: Decimal = self.price.parse().context("invalid price")?;
let commission = {
let comm_val: f64 = self
.commission
.as_ref()
.and_then(|c| c.parse().ok())
.unwrap_or(0.0);
let comm_asset = self
.commission_asset
.as_ref()
.map_or_else(Currency::USDT, Currency::from);
Money::new(comm_val, comm_asset)
};
Ok(FillReport::new(
account_id,
instrument_id,
venue_order_id,
trade_id,
order_side,
Quantity::new(last_qty.to_string().parse()?, size_precision),
Price::new(last_px.to_string().parse()?, price_precision),
commission,
liquidity_side,
None, None, ts_event,
ts_init,
Some(UUID4::new()),
))
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(untagged)]
pub enum BatchOrderResult {
Success(Box<BinanceFuturesOrder>),
Error(BatchOrderError),
}
#[derive(Clone, Debug, Deserialize)]
pub struct BatchOrderError {
pub code: i64,
pub msg: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListenKeyResponse {
pub listen_key: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceFuturesAlgoOrder {
pub algo_id: i64,
pub client_algo_id: String,
pub algo_type: BinanceAlgoType,
#[serde(rename = "orderType", alias = "type")]
pub order_type: BinanceFuturesOrderType,
pub symbol: Ustr,
pub side: BinanceSide,
#[serde(default)]
pub position_side: Option<BinancePositionSide>,
#[serde(default)]
pub time_in_force: Option<BinanceTimeInForce>,
#[serde(default)]
pub quantity: Option<String>,
#[serde(default)]
pub algo_status: Option<BinanceAlgoStatus>,
#[serde(default)]
pub trigger_price: Option<String>,
#[serde(default)]
pub price: Option<String>,
#[serde(default)]
pub working_type: Option<BinanceWorkingType>,
#[serde(default)]
pub close_position: Option<bool>,
#[serde(default)]
pub price_protect: Option<bool>,
#[serde(default)]
pub reduce_only: Option<bool>,
#[serde(default)]
pub activate_price: Option<String>,
#[serde(default)]
pub callback_rate: Option<String>,
#[serde(default)]
pub create_time: Option<i64>,
#[serde(default)]
pub update_time: Option<i64>,
#[serde(default)]
pub trigger_time: Option<i64>,
#[serde(default)]
pub actual_order_id: Option<String>,
#[serde(default)]
pub executed_qty: Option<String>,
#[serde(default)]
pub avg_price: Option<String>,
}
impl BinanceFuturesAlgoOrder {
pub fn to_order_status_report(
&self,
account_id: AccountId,
instrument_id: InstrumentId,
size_precision: u8,
ts_init: UnixNanos,
) -> anyhow::Result<OrderStatusReport> {
let ts_event = self
.update_time
.or(self.create_time)
.map_or(ts_init, |t| UnixNanos::from_millis(t as u64));
let client_order_id = ClientOrderId::new(decode_broker_id(
&self.client_algo_id,
BINANCE_NAUTILUS_FUTURES_BROKER_ID,
));
let venue_order_id = self.actual_order_id.as_ref().map_or_else(
|| VenueOrderId::new(self.algo_id.to_string()),
|id| VenueOrderId::new(id.clone()),
);
let order_side = match self.side {
BinanceSide::Buy => OrderSide::Buy,
BinanceSide::Sell => OrderSide::Sell,
};
let order_type = self.parse_order_type();
let time_in_force = self
.time_in_force
.as_ref()
.map_or(TimeInForce::Gtc, |tif| tif.to_nautilus_time_in_force());
let order_status = self.parse_order_status();
let quantity: Decimal = self
.quantity
.as_ref()
.map_or(Ok(Decimal::ZERO), |q| q.parse())
.context("invalid quantity")?;
let filled_qty: Decimal = self
.executed_qty
.as_ref()
.map_or(Ok(Decimal::ZERO), |q| q.parse())
.context("invalid executed_qty")?;
Ok(OrderStatusReport::new(
account_id,
instrument_id,
Some(client_order_id),
venue_order_id,
order_side,
order_type,
time_in_force,
order_status,
Quantity::new(quantity.to_string().parse()?, size_precision),
Quantity::new(filled_qty.to_string().parse()?, size_precision),
ts_event,
ts_event,
ts_init,
Some(UUID4::new()),
))
}
fn parse_order_type(&self) -> OrderType {
self.order_type.into()
}
fn parse_order_status(&self) -> OrderStatus {
match self.algo_status {
Some(BinanceAlgoStatus::New) => OrderStatus::Accepted,
Some(BinanceAlgoStatus::Triggering) => OrderStatus::Accepted,
Some(BinanceAlgoStatus::Triggered) => OrderStatus::Accepted,
Some(BinanceAlgoStatus::Finished) => {
if let Some(qty) = &self.executed_qty
&& let Ok(dec) = qty.parse::<Decimal>()
&& !dec.is_zero()
{
return OrderStatus::Filled;
}
OrderStatus::Canceled
}
Some(BinanceAlgoStatus::Canceled) => OrderStatus::Canceled,
Some(BinanceAlgoStatus::Expired) => OrderStatus::Expired,
Some(BinanceAlgoStatus::Rejected) => OrderStatus::Rejected,
Some(BinanceAlgoStatus::Unknown) | None => OrderStatus::Initialized,
}
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceFuturesAlgoOrderCancelResponse {
pub algo_id: i64,
pub client_algo_id: String,
pub code: String,
pub msg: String,
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
use crate::common::testing::load_fixture_string;
#[rstest]
fn test_parse_account_info_v2() {
let json = load_fixture_string("futures/http_json/account_info_v2.json");
let account: BinanceFuturesAccountInfo =
serde_json::from_str(&json).expect("Failed to parse account info");
assert_eq!(
account.total_wallet_balance,
Some("23.72469206".to_string())
);
assert_eq!(account.assets.len(), 1);
assert_eq!(account.assets[0].asset.as_str(), "USDT");
assert_eq!(account.assets[0].wallet_balance, "23.72469206");
assert_eq!(account.positions.len(), 1);
assert_eq!(account.positions[0].symbol.as_str(), "BTCUSDT");
assert_eq!(account.positions[0].leverage, Some("100".to_string()));
}
#[rstest]
fn test_account_info_to_account_state_zero_margins() {
let json = load_fixture_string("futures/http_json/account_info_v2.json");
let account: BinanceFuturesAccountInfo =
serde_json::from_str(&json).expect("Failed to parse account info");
let account_id = AccountId::from("BINANCE-001");
let ts_init = UnixNanos::from(1_000_000_000u64);
let state = account.to_account_state(account_id, ts_init).unwrap();
assert_eq!(state.account_id, account_id);
assert_eq!(state.account_type, AccountType::Margin);
assert!(!state.balances.is_empty());
assert_eq!(state.margins.len(), 0);
}
#[rstest]
fn test_account_info_to_account_state_with_margins() {
let json = r#"{
"totalInitialMargin": "500.25000000",
"totalMaintMargin": "250.75000000",
"totalWalletBalance": "10000.00000000",
"assets": [{
"asset": "USDT",
"walletBalance": "10000.00000000",
"availableBalance": "9500.00000000",
"updateTime": 1617939110373
}],
"positions": []
}"#;
let account: BinanceFuturesAccountInfo =
serde_json::from_str(json).expect("Failed to parse account info");
let account_id = AccountId::from("BINANCE-001");
let ts_init = UnixNanos::from(1_000_000_000u64);
let state = account.to_account_state(account_id, ts_init).unwrap();
assert_eq!(state.margins.len(), 1);
let margin = &state.margins[0];
assert_eq!(margin.instrument_id.symbol.as_str(), "ACCOUNT");
assert_eq!(margin.instrument_id.venue.as_str(), "BINANCE");
assert_eq!(margin.initial.as_f64(), 500.25);
assert_eq!(margin.maintenance.as_f64(), 250.75);
}
#[rstest]
fn test_account_info_to_account_state_empty_balance() {
let json = r#"{
"assets": [{
"asset": "USDT",
"walletBalance": "",
"availableBalance": "",
"updateTime": 0
}],
"positions": []
}"#;
let account: BinanceFuturesAccountInfo =
serde_json::from_str(json).expect("Failed to parse account info");
let account_id = AccountId::from("BINANCE-001");
let ts_init = UnixNanos::from(1_000_000_000u64);
let state = account.to_account_state(account_id, ts_init).unwrap();
assert_eq!(state.balances.len(), 1);
let balance = &state.balances[0];
assert_eq!(balance.total, Money::new(0.0, Currency::USDT()));
assert_eq!(balance.free, Money::new(0.0, Currency::USDT()));
assert_eq!(balance.locked, Money::new(0.0, Currency::USDT()));
}
#[rstest]
fn test_account_info_to_account_state_empty_assets() {
let json = r#"{
"assets": [],
"positions": []
}"#;
let account: BinanceFuturesAccountInfo =
serde_json::from_str(json).expect("Failed to parse account info");
let account_id = AccountId::from("BINANCE-001");
let ts_init = UnixNanos::from(1_000_000_000u64);
let state = account.to_account_state(account_id, ts_init).unwrap();
assert_eq!(state.balances.len(), 1);
let balance = &state.balances[0];
assert_eq!(balance.total, Money::new(0.0, Currency::USDT()));
}
#[rstest]
fn test_parse_position_risk() {
let json = load_fixture_string("futures/http_json/position_risk.json");
let positions: Vec<BinancePositionRisk> =
serde_json::from_str(&json).expect("Failed to parse position risk");
assert_eq!(positions.len(), 1);
assert_eq!(positions[0].symbol.as_str(), "BTCUSDT");
assert_eq!(positions[0].position_amt, "0.001");
assert_eq!(positions[0].mark_price, "51000.0");
assert_eq!(positions[0].leverage, "20");
}
#[rstest]
fn test_parse_balance_with_v1_field() {
let json = load_fixture_string("futures/http_json/balance.json");
let balances: Vec<BinanceFuturesBalance> =
serde_json::from_str(&json).expect("Failed to parse balance");
assert_eq!(balances.len(), 1);
assert_eq!(balances[0].asset.as_str(), "USDT");
assert_eq!(balances[0].wallet_balance, "122.12345678");
assert_eq!(balances[0].available_balance, "122.12345678");
}
#[rstest]
fn test_parse_balance_with_v2_field() {
let json = r#"{
"asset": "USDT",
"walletBalance": "100.00000000",
"availableBalance": "100.00000000",
"updateTime": 1617939110373
}"#;
let balance: BinanceFuturesBalance =
serde_json::from_str(json).expect("Failed to parse balance");
assert_eq!(balance.asset.as_str(), "USDT");
assert_eq!(balance.wallet_balance, "100.00000000");
}
#[rstest]
fn test_parse_order() {
let json = load_fixture_string("futures/http_json/order_response.json");
let order: BinanceFuturesOrder =
serde_json::from_str(&json).expect("Failed to parse order");
assert_eq!(order.order_id, 12345678);
assert_eq!(order.symbol.as_str(), "BTCUSDT");
assert_eq!(order.status, BinanceOrderStatus::New);
assert_eq!(order.time_in_force, BinanceTimeInForce::Gtc);
assert_eq!(order.side, BinanceSide::Buy);
assert_eq!(order.order_type, BinanceFuturesOrderType::Limit);
assert_eq!(order.price_match, Some(BinancePriceMatch::None));
assert_eq!(
order.self_trade_prevention_mode,
Some(BinanceSelfTradePreventionMode::None)
);
}
#[rstest]
fn test_parse_hedge_mode_response() {
let json = r#"{"dualSidePosition": true}"#;
let response: BinanceHedgeModeResponse =
serde_json::from_str(json).expect("Failed to parse hedge mode");
assert!(response.dual_side_position);
}
#[rstest]
fn test_parse_leverage_response() {
let json = r#"{"symbol": "BTCUSDT", "leverage": 20, "maxNotionalValue": "250000"}"#;
let response: BinanceLeverageResponse =
serde_json::from_str(json).expect("Failed to parse leverage");
assert_eq!(response.symbol.as_str(), "BTCUSDT");
assert_eq!(response.leverage, 20);
}
#[rstest]
fn test_parse_listen_key_response() {
let json =
r#"{"listenKey": "pqia91ma19a5s61cv6a81va65sdf19v8a65a1a5s61cv6a81va65sdf19v8a65a1"}"#;
let response: ListenKeyResponse =
serde_json::from_str(json).expect("Failed to parse listen key");
assert!(!response.listen_key.is_empty());
}
#[rstest]
fn test_parse_account_position() {
let json = r#"{
"symbol": "ETHUSDT",
"initialMargin": "100.00",
"maintMargin": "50.00",
"unrealizedProfit": "10.00",
"positionInitialMargin": "100.00",
"openOrderInitialMargin": "0",
"leverage": "10",
"isolated": true,
"entryPrice": "2000.00",
"maxNotional": "100000",
"bidNotional": "0",
"askNotional": "0",
"positionSide": "LONG",
"positionAmt": "0.5",
"updateTime": 1625474304765
}"#;
let position: BinanceAccountPosition =
serde_json::from_str(json).expect("Failed to parse account position");
assert_eq!(position.symbol.as_str(), "ETHUSDT");
assert_eq!(position.leverage, Some("10".to_string()));
assert_eq!(position.isolated, Some(true));
assert_eq!(position.position_side, Some(BinancePositionSide::Long));
}
#[rstest]
fn test_parse_algo_order() {
let json = r#"{
"algoId": 123456789,
"clientAlgoId": "test-algo-order-1",
"algoType": "CONDITIONAL",
"type": "STOP_MARKET",
"symbol": "BTCUSDT",
"side": "BUY",
"positionSide": "BOTH",
"timeInForce": "GTC",
"quantity": "0.001",
"algoStatus": "NEW",
"triggerPrice": "45000.00",
"workingType": "MARK_PRICE",
"reduceOnly": false,
"createTime": 1625474304765,
"updateTime": 1625474304765
}"#;
let order: BinanceFuturesAlgoOrder =
serde_json::from_str(json).expect("Failed to parse algo order");
assert_eq!(order.algo_id, 123456789);
assert_eq!(order.client_algo_id, "test-algo-order-1");
assert_eq!(order.algo_type, BinanceAlgoType::Conditional);
assert_eq!(order.order_type, BinanceFuturesOrderType::StopMarket);
assert_eq!(order.symbol.as_str(), "BTCUSDT");
assert_eq!(order.side, BinanceSide::Buy);
assert_eq!(order.algo_status, Some(BinanceAlgoStatus::New));
assert_eq!(order.trigger_price, Some("45000.00".to_string()));
}
#[rstest]
fn test_parse_algo_order_triggered() {
let json = r#"{
"algoId": 123456789,
"clientAlgoId": "test-algo-order-2",
"algoType": "CONDITIONAL",
"type": "TAKE_PROFIT",
"symbol": "ETHUSDT",
"side": "SELL",
"algoStatus": "TRIGGERED",
"triggerPrice": "2500.00",
"price": "2500.00",
"actualOrderId": "987654321",
"executedQty": "0.5",
"avgPrice": "2499.50"
}"#;
let order: BinanceFuturesAlgoOrder =
serde_json::from_str(json).expect("Failed to parse triggered algo order");
assert_eq!(order.algo_status, Some(BinanceAlgoStatus::Triggered));
assert_eq!(order.order_type, BinanceFuturesOrderType::TakeProfit);
assert_eq!(order.actual_order_id, Some("987654321".to_string()));
assert_eq!(order.executed_qty, Some("0.5".to_string()));
}
#[rstest]
fn test_parse_algo_order_cancel_response() {
let json = r#"{
"algoId": 123456789,
"clientAlgoId": "test-algo-order-1",
"code": "200",
"msg": "success"
}"#;
let response: BinanceFuturesAlgoOrderCancelResponse =
serde_json::from_str(json).expect("Failed to parse algo cancel response");
assert_eq!(response.algo_id, 123456789);
assert_eq!(response.client_algo_id, "test-algo-order-1");
assert_eq!(response.code, "200");
assert_eq!(response.msg, "success");
}
#[rstest]
fn test_order_to_report_decodes_broker_id() {
let json = r#"{
"orderId": 12345678,
"symbol": "BTCUSDT",
"status": "NEW",
"clientOrderId": "x-aHRE4BCj-T0000000000000",
"price": "50000.00",
"avgPrice": "0.00",
"origQty": "0.001",
"executedQty": "0.000",
"cumQuote": "0.00",
"timeInForce": "GTC",
"type": "LIMIT",
"reduceOnly": false,
"closePosition": false,
"side": "BUY",
"positionSide": "BOTH",
"stopPrice": "0.00",
"workingType": "CONTRACT_PRICE",
"priceProtect": false,
"origType": "LIMIT",
"priceMatch": "NONE",
"selfTradePreventionMode": "NONE",
"goodTillDate": 0,
"time": 1625474304765,
"updateTime": 1625474304765
}"#;
let order: BinanceFuturesOrder = serde_json::from_str(json).unwrap();
let account_id = AccountId::from("BINANCE-FUTURES-001");
let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
let ts_init = UnixNanos::from(1_000_000_000u64);
let report = order
.to_order_status_report(account_id, instrument_id, 3, false, ts_init)
.unwrap();
assert_eq!(
report.client_order_id,
Some(ClientOrderId::from("O-20200101-000000-000-000-0")),
);
}
#[rstest]
fn test_algo_order_to_report_decodes_broker_id() {
let json = r#"{
"algoId": 123456789,
"clientAlgoId": "x-aHRE4BCj-Rmy-algo-order-1",
"algoType": "CONDITIONAL",
"type": "STOP_MARKET",
"symbol": "BTCUSDT",
"side": "BUY",
"positionSide": "BOTH",
"timeInForce": "GTC",
"quantity": "0.001",
"algoStatus": "NEW",
"triggerPrice": "45000.00",
"workingType": "MARK_PRICE",
"reduceOnly": false,
"createTime": 1625474304765,
"updateTime": 1625474304765
}"#;
let order: BinanceFuturesAlgoOrder = serde_json::from_str(json).unwrap();
let account_id = AccountId::from("BINANCE-FUTURES-001");
let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
let ts_init = UnixNanos::from(1_000_000_000u64);
let report = order
.to_order_status_report(account_id, instrument_id, 3, ts_init)
.unwrap();
assert_eq!(
report.client_order_id,
Some(ClientOrderId::from("my-algo-order-1")),
);
}
#[rstest]
#[case(BinanceOrderStatus::Expired, false, OrderStatus::Expired)]
#[case(BinanceOrderStatus::Expired, true, OrderStatus::Canceled)]
#[case(BinanceOrderStatus::ExpiredInMatch, false, OrderStatus::Expired)]
#[case(BinanceOrderStatus::ExpiredInMatch, true, OrderStatus::Canceled)]
fn test_to_nautilus_order_status_expired_respects_treat_as_canceled(
#[case] status: BinanceOrderStatus,
#[case] treat_expired_as_canceled: bool,
#[case] expected: OrderStatus,
) {
let result = status.to_nautilus_order_status(treat_expired_as_canceled);
assert_eq!(result, expected);
}
}