diff --git a/.rustfmt.toml b/.rustfmt.toml index 18a1f7a1..c8cbdc5e 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,4 +1,3 @@ -binop_separator = "Back" -fn_args_layout = "Compressed" +fn_params_layout = "Compressed" newline_style = "Unix" reorder_imports = false diff --git a/src/api.rs b/src/api.rs index b69c9cc6..0ed36b78 100644 --- a/src/api.rs +++ b/src/api.rs @@ -75,6 +75,7 @@ pub enum Futures { AllOrders, UserTrades, Order, + BatchOrders, PositionRisk, Balance, PositionSide, @@ -93,6 +94,7 @@ pub enum Futures { OpenOrders, UserDataStream, Income, + LeverageBracket, } impl From for String { @@ -152,6 +154,7 @@ impl From for String { Futures::UserTrades => "/fapi/v1/userTrades", Futures::PositionSide => "/fapi/v1/positionSide/dual", Futures::Order => "/fapi/v1/order", + Futures::BatchOrders => "/fapi/v1/batchOrders", Futures::PositionRisk => "/fapi/v2/positionRisk", Futures::Balance => "/fapi/v2/balance", Futures::OpenInterest => "/fapi/v1/openInterest", @@ -169,6 +172,7 @@ impl From for String { Futures::OpenOrders => "/fapi/v1/openOrders", Futures::UserDataStream => "/fapi/v1/listenKey", Futures::Income => "/fapi/v1/income", + Futures::LeverageBracket => "/fapi/v1/leverageBracket", }, }) } diff --git a/src/client.rs b/src/client.rs index 35032262..17abddfd 100644 --- a/src/client.rs +++ b/src/client.rs @@ -174,7 +174,7 @@ impl Client { Err(ErrorKind::BinanceError(error).into()) } s => { - bail!(format!("Received response: {:?}", s)); + bail!("Received response: {:?}", s); } } } diff --git a/src/errors.rs b/src/errors.rs index 208636ed..bb58fbb0 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,7 +1,7 @@ use serde::Deserialize; use error_chain::error_chain; -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] pub struct BinanceContentError { pub code: i16, pub msg: String, diff --git a/src/futures/account.rs b/src/futures/account.rs index d7bf2c09..8705a72e 100644 --- a/src/futures/account.rs +++ b/src/futures/account.rs @@ -1,6 +1,6 @@ use std::collections::BTreeMap; use std::fmt::Display; -use crate::util::build_signed_request; +use crate::util::{build_list_of_json, build_signed_request}; use crate::errors::Result; use crate::client::Client; use crate::api::{API, Futures}; @@ -9,8 +9,8 @@ use crate::account::OrderSide; use crate::futures::model::{Order, TradeHistory}; use super::model::{ - ChangeLeverageResponse, Transaction, CanceledOrder, PositionRisk, AccountBalance, - AccountInformation, + AccountBalance, AccountInformation, CanceledOrder, ChangeLeverageResponse, PositionRisk, + Transaction, TransactionOrError, }; #[derive(Clone)] @@ -112,7 +112,7 @@ impl Display for TimeInForce { } } -struct OrderRequest { +pub struct OrderRequest { pub symbol: String, pub side: OrderSide, pub position_side: Option, @@ -129,21 +129,119 @@ struct OrderRequest { pub price_protect: Option, } -pub struct CustomOrderRequest { - pub symbol: String, - pub side: OrderSide, - pub position_side: Option, - pub order_type: OrderType, - pub time_in_force: Option, - pub qty: Option, - pub reduce_only: Option, - pub price: Option, - pub stop_price: Option, - pub close_position: Option, - pub activation_price: Option, - pub callback_rate: Option, - pub working_type: Option, - pub price_protect: Option, +impl OrderRequest { + pub fn limit_buy( + symbol: impl Into, qty: impl Into, price: f64, time_in_force: TimeInForce, + ) -> Self { + Self { + symbol: symbol.into(), + side: OrderSide::Buy, + position_side: None, + order_type: OrderType::Limit, + time_in_force: Some(time_in_force), + qty: Some(qty.into()), + reduce_only: None, + price: Some(price), + stop_price: None, + close_position: None, + activation_price: None, + callback_rate: None, + working_type: None, + price_protect: None, + } + } + pub fn limit_sell( + symbol: impl Into, qty: impl Into, price: f64, time_in_force: TimeInForce, + ) -> Self { + Self { + symbol: symbol.into(), + side: OrderSide::Sell, + position_side: None, + order_type: OrderType::Limit, + time_in_force: Some(time_in_force), + qty: Some(qty.into()), + reduce_only: None, + price: Some(price), + stop_price: None, + close_position: None, + activation_price: None, + callback_rate: None, + working_type: None, + price_protect: None, + } + } + pub fn market_buy(symbol: impl Into, qty: impl Into) -> Self { + Self { + symbol: symbol.into(), + side: OrderSide::Buy, + position_side: None, + order_type: OrderType::Market, + time_in_force: None, + qty: Some(qty.into()), + reduce_only: None, + price: None, + stop_price: None, + close_position: None, + activation_price: None, + callback_rate: None, + working_type: None, + price_protect: None, + } + } + pub fn market_sell(symbol: impl Into, qty: impl Into) -> Self { + Self { + symbol: symbol.into(), + side: OrderSide::Sell, + position_side: None, + order_type: OrderType::Market, + time_in_force: None, + qty: Some(qty.into()), + reduce_only: None, + price: None, + stop_price: None, + close_position: None, + activation_price: None, + callback_rate: None, + working_type: None, + price_protect: None, + } + } + pub fn stop_market_close_buy(symbol: impl Into, stop_price: impl Into) -> Self { + Self { + symbol: symbol.into(), + side: OrderSide::Buy, + position_side: None, + order_type: OrderType::StopMarket, + time_in_force: None, + qty: None, + reduce_only: None, + price: None, + stop_price: Some(stop_price.into()), + close_position: Some(true), + activation_price: None, + callback_rate: None, + working_type: None, + price_protect: None, + } + } + pub fn stop_market_close_sell(symbol: impl Into, stop_price: impl Into) -> Self { + Self { + symbol: symbol.into(), + side: OrderSide::Sell, + position_side: None, + order_type: OrderType::StopMarket, + time_in_force: None, + qty: None, + reduce_only: None, + price: None, + stop_price: Some(stop_price.into()), + close_position: Some(true), + activation_price: None, + callback_rate: None, + working_type: None, + price_protect: None, + } + } } pub struct IncomeRequest { @@ -208,22 +306,7 @@ impl FuturesAccount { &self, symbol: impl Into, qty: impl Into, price: f64, time_in_force: TimeInForce, ) -> Result { - let buy = OrderRequest { - symbol: symbol.into(), - side: OrderSide::Buy, - position_side: None, - order_type: OrderType::Limit, - time_in_force: Some(time_in_force), - qty: Some(qty.into()), - reduce_only: None, - price: Some(price), - stop_price: None, - close_position: None, - activation_price: None, - callback_rate: None, - working_type: None, - price_protect: None, - }; + let buy = OrderRequest::limit_buy(symbol, qty, price, time_in_force); let order = self.build_order(buy); let request = build_signed_request(order, self.recv_window)?; self.client @@ -234,22 +317,7 @@ impl FuturesAccount { &self, symbol: impl Into, qty: impl Into, price: f64, time_in_force: TimeInForce, ) -> Result { - let sell = OrderRequest { - symbol: symbol.into(), - side: OrderSide::Sell, - position_side: None, - order_type: OrderType::Limit, - time_in_force: Some(time_in_force), - qty: Some(qty.into()), - reduce_only: None, - price: Some(price), - stop_price: None, - close_position: None, - activation_price: None, - callback_rate: None, - working_type: None, - price_protect: None, - }; + let sell = OrderRequest::limit_sell(symbol, qty, price, time_in_force); let order = self.build_order(sell); let request = build_signed_request(order, self.recv_window)?; self.client @@ -262,22 +330,7 @@ impl FuturesAccount { S: Into, F: Into, { - let buy = OrderRequest { - symbol: symbol.into(), - side: OrderSide::Buy, - position_side: None, - order_type: OrderType::Market, - time_in_force: None, - qty: Some(qty.into()), - reduce_only: None, - price: None, - stop_price: None, - close_position: None, - activation_price: None, - callback_rate: None, - working_type: None, - price_protect: None, - }; + let buy = OrderRequest::market_buy(symbol, qty); let order = self.build_order(buy); let request = build_signed_request(order, self.recv_window)?; self.client @@ -290,22 +343,7 @@ impl FuturesAccount { S: Into, F: Into, { - let sell = OrderRequest { - symbol: symbol.into(), - side: OrderSide::Sell, - position_side: None, - order_type: OrderType::Market, - time_in_force: None, - qty: Some(qty.into()), - reduce_only: None, - price: None, - stop_price: None, - close_position: None, - activation_price: None, - callback_rate: None, - working_type: None, - price_protect: None, - }; + let sell = OrderRequest::market_sell(symbol, qty); let order = self.build_order(sell); let request = build_signed_request(order, self.recv_window)?; self.client @@ -346,22 +384,7 @@ impl FuturesAccount { S: Into, F: Into, { - let sell = OrderRequest { - symbol: symbol.into(), - side: OrderSide::Buy, - position_side: None, - order_type: OrderType::StopMarket, - time_in_force: None, - qty: None, - reduce_only: None, - price: None, - stop_price: Some(stop_price.into()), - close_position: Some(true), - activation_price: None, - callback_rate: None, - working_type: None, - price_protect: None, - }; + let sell = OrderRequest::stop_market_close_buy(symbol, stop_price); let order = self.build_order(sell); let request = build_signed_request(order, self.recv_window)?; self.client @@ -374,80 +397,38 @@ impl FuturesAccount { S: Into, F: Into, { - let sell = OrderRequest { - symbol: symbol.into(), - side: OrderSide::Sell, - position_side: None, - order_type: OrderType::StopMarket, - time_in_force: None, - qty: None, - reduce_only: None, - price: None, - stop_price: Some(stop_price.into()), - close_position: Some(true), - activation_price: None, - callback_rate: None, - working_type: None, - price_protect: None, - }; + let sell = OrderRequest::stop_market_close_sell(symbol, stop_price); let order = self.build_order(sell); let request = build_signed_request(order, self.recv_window)?; self.client .post_signed(API::Futures(Futures::Order), request) } - // Custom order for for professional traders - pub fn custom_order(&self, order_request: CustomOrderRequest) -> Result { - let order = OrderRequest { - symbol: order_request.symbol, - side: order_request.side, - position_side: order_request.position_side, - order_type: order_request.order_type, - time_in_force: order_request.time_in_force, - qty: order_request.qty, - reduce_only: order_request.reduce_only, - price: order_request.price, - stop_price: order_request.stop_price, - close_position: order_request.close_position, - activation_price: order_request.activation_price, - callback_rate: order_request.callback_rate, - working_type: order_request.working_type, - price_protect: order_request.price_protect, - }; + // Custom order for professional traders + pub fn custom_order(&self, order: OrderRequest) -> Result { let order = self.build_order(order); let request = build_signed_request(order, self.recv_window)?; self.client .post_signed(API::Futures(Futures::Order), request) } - // Custom order for for professional traders + // Custom batch orders for professional traders pub fn custom_batch_orders( - &self, _order_count: u64, order_requests: Vec, - ) -> Result { - let request = String::from(""); - for order_request in order_requests { - let order = OrderRequest { - symbol: order_request.symbol, - side: order_request.side, - position_side: order_request.position_side, - order_type: order_request.order_type, - time_in_force: order_request.time_in_force, - qty: order_request.qty, - reduce_only: order_request.reduce_only, - price: order_request.price, - stop_price: order_request.stop_price, - close_position: order_request.close_position, - activation_price: order_request.activation_price, - callback_rate: order_request.callback_rate, - working_type: order_request.working_type, - price_protect: order_request.price_protect, - }; - let _order = self.build_order(order); - // TODO : make a request string for batch orders api - // let request = build_signed_request(order, self.recv_window)?; + &self, order_requests: Vec, + ) -> Result> { + if order_requests.is_empty() { + return Ok(Vec::new()); } + let mut parameters = BTreeMap::new(); + let mut orders = Vec::new(); + for order in order_requests { + let order = self.build_order(order); + orders.push(order); + } + parameters.insert("batchOrders".into(), build_list_of_json(orders)); + let request = build_signed_request(parameters, self.recv_window)?; self.client - .post_signed(API::Futures(Futures::Order), request) + .post_signed(API::Futures(Futures::BatchOrders), request) } pub fn get_all_orders( @@ -689,4 +670,19 @@ impl FuturesAccount { self.client .get_signed(API::Futures(Futures::Income), Some(request)) } + + pub fn leverage_brackets( + &self, symbol: S, + ) -> Result> + where + S: Into>, + { + let mut parameters: BTreeMap = BTreeMap::new(); + if let Some(symbol) = symbol.into() { + parameters.insert("symbol".into(), symbol); + } + let request = build_signed_request(parameters, self.recv_window)?; + self.client + .get_signed(API::Futures(Futures::LeverageBracket), Some(request)) + } } diff --git a/src/futures/model.rs b/src/futures/model.rs index e6debac4..3d8e0cfd 100644 --- a/src/futures/model.rs +++ b/src/futures/model.rs @@ -1,5 +1,8 @@ use serde::{Deserialize, Serialize}; -use crate::model::{string_or_float, string_or_float_opt, string_or_bool}; +use crate::{ + errors::BinanceContentError, + model::{string_or_bool, string_or_float, string_or_float_opt}, +}; pub use crate::model::{ Asks, Bids, BookTickers, Filters, KlineSummaries, KlineSummary, RateLimit, ServerTime, @@ -282,6 +285,14 @@ pub struct Transaction { price_protect: bool, } +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Deserialize, Clone)] +#[serde(untagged)] +pub enum TransactionOrError { + Transaction(Transaction), + Error(BinanceContentError), +} + #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct CanceledOrder { @@ -625,3 +636,26 @@ pub struct Income { pub tran_id: u64, pub trade_id: String, } + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct LeverageBrackets { + pub symbol: String, + pub notional_coef: Option, + pub brackets: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Bracket { + pub bracket: i32, + pub initial_leverage: u8, + #[serde(with = "string_or_float")] + pub notional_cap: f64, + #[serde(with = "string_or_float")] + pub notional_floor: f64, + #[serde(with = "string_or_float")] + pub maint_margin_ratio: f64, + #[serde(with = "string_or_float")] + pub cum: f64, +} diff --git a/src/futures/websockets.rs b/src/futures/websockets.rs index 323f0455..926d8cd8 100755 --- a/src/futures/websockets.rs +++ b/src/futures/websockets.rs @@ -23,6 +23,7 @@ enum FuturesWebsocketAPI { Custom(String), } +#[derive(Clone, Copy)] pub enum FuturesMarket { USDM, COINM, @@ -113,12 +114,12 @@ impl<'a> FuturesWebSockets<'a> { } } - pub fn connect(&mut self, market: &FuturesMarket, subscription: &'a str) -> Result<()> { + pub fn connect(&mut self, market: &FuturesMarket, subscription: &str) -> Result<()> { self.connect_wss(&FuturesWebsocketAPI::Default.params(market, subscription)) } pub fn connect_with_config( - &mut self, market: &FuturesMarket, subscription: &'a str, config: &'a Config, + &mut self, market: &FuturesMarket, subscription: &str, config: &Config, ) -> Result<()> { self.connect_wss( &FuturesWebsocketAPI::Custom(config.ws_endpoint.clone()).params(market, subscription), @@ -138,7 +139,7 @@ impl<'a> FuturesWebSockets<'a> { self.socket = Some(answer); Ok(()) } - Err(e) => bail!(format!("Error during handshake {}", e)), + Err(e) => bail!("Error during handshake {}", e), } } @@ -194,18 +195,18 @@ impl<'a> FuturesWebSockets<'a> { pub fn event_loop(&mut self, running: &AtomicBool) -> Result<()> { while running.load(Ordering::Relaxed) { if let Some(ref mut socket) = self.socket { - let message = socket.0.read_message()?; + let message = socket.0.read()?; match message { Message::Text(msg) => { if let Err(e) = self.handle_msg(&msg) { - bail!(format!("Error on handling stream message: {}", e)); + bail!("Error on handling stream message: {}", e); } } Message::Ping(payload) => { - socket.0.write_message(Message::Pong(payload)).unwrap(); + socket.0.write(Message::Pong(payload)).unwrap(); } Message::Pong(_) | Message::Binary(_) | Message::Frame(_) => (), - Message::Close(e) => bail!(format!("Disconnected {:?}", e)), + Message::Close(e) => bail!("Disconnected {:?}", e), } } } diff --git a/src/model.rs b/src/model.rs index baedc33f..8787aef8 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1367,7 +1367,7 @@ pub(crate) mod string_or_float { pub(crate) mod string_or_float_opt { use std::fmt; - use serde::{Serializer, Deserialize, Deserializer}; + use serde::{Serializer, Deserializer}; pub fn serialize(value: &Option, serializer: S) -> Result where @@ -1384,13 +1384,6 @@ pub(crate) mod string_or_float_opt { where D: Deserializer<'de>, { - #[derive(Deserialize)] - #[serde(untagged)] - enum StringOrFloat { - String(String), - Float(f64), - } - Ok(Some(crate::model::string_or_float::deserialize( deserializer, )?)) diff --git a/src/util.rs b/src/util.rs index 5761bf4c..68aad093 100644 --- a/src/util.rs +++ b/src/util.rs @@ -14,6 +14,22 @@ pub fn build_request(parameters: BTreeMap) -> String { request } +pub fn build_list_of_json(parameters: Vec>) -> String { + let mut request = String::new(); + for param in parameters { + let mut inner = String::new(); + for (key, value) in param { + inner.push_str(&format!("\"{}\":\"{}\",", key, value)); + } + inner.pop(); + inner = format!("{{{}}},", inner); + request.push_str(&inner); + } + request.pop(); + request = format!("[{}]", request); + url::form_urlencoded::byte_serialize(request.as_bytes()).collect::() +} + pub fn build_signed_request( parameters: BTreeMap, recv_window: u64, ) -> Result { diff --git a/src/websockets.rs b/src/websockets.rs index 15f2fdd2..32b8e3e9 100644 --- a/src/websockets.rs +++ b/src/websockets.rs @@ -105,7 +105,7 @@ impl<'a> WebSockets<'a> { self.socket = Some(answer); Ok(()) } - Err(e) => bail!(format!("Error during handshake {}", e)), + Err(e) => bail!("Error during handshake {}", e), } } @@ -153,18 +153,18 @@ impl<'a> WebSockets<'a> { pub fn event_loop(&mut self, running: &AtomicBool) -> Result<()> { while running.load(Ordering::Relaxed) { if let Some(ref mut socket) = self.socket { - let message = socket.0.read_message()?; + let message = socket.0.read()?; match message { Message::Text(msg) => { if let Err(e) = self.handle_msg(&msg) { - bail!(format!("Error on handling stream message: {}", e)); + bail!("Error on handling stream message: {}", e); } } Message::Ping(payload) => { - socket.0.write_message(Message::Pong(payload)).unwrap(); + socket.0.write(Message::Pong(payload)).unwrap(); } Message::Pong(_) | Message::Binary(_) | Message::Frame(_) => (), - Message::Close(e) => bail!(format!("Disconnected {:?}", e)), + Message::Close(e) => bail!("Disconnected {:?}", e), } } } diff --git a/tests/futures_account_tests.rs b/tests/futures_account_tests.rs index a76f8169..fe11127e 100644 --- a/tests/futures_account_tests.rs +++ b/tests/futures_account_tests.rs @@ -197,7 +197,7 @@ mod tests { .set_recv_window(1234); let account: FuturesAccount = Binance::new_with_config(None, None, &config); let _ = env_logger::try_init(); - let custom_order = CustomOrderRequest { + let custom_order = OrderRequest { symbol: "SRMUSDT".into(), side: OrderSide::Sell, position_side: None, @@ -254,4 +254,26 @@ mod tests { mock.assert(); } + + #[test] + fn get_leverage_brackets() { + let mut server = Server::new(); + let mock = server + .mock("GET", "/fapi/v1/leverageBracket") + .with_header("content-type", "application/json;charset=UTF-8") + .match_query(Matcher::Regex( + "recvWindow=1234×tamp=\\d+&signature=.*".into(), + )) + .with_body_from_file("tests/mocks/futures/account/leverage_brackets.json") + .create(); + + let config = Config::default() + .set_futures_rest_api_endpoint(server.url()) + .set_recv_window(1234); + let account: FuturesAccount = Binance::new_with_config(None, None, &config); + let _ = env_logger::try_init(); + account.leverage_brackets(None).unwrap(); + + mock.assert(); + } } diff --git a/tests/mocks/futures/account/leverage_brackets.json b/tests/mocks/futures/account/leverage_brackets.json new file mode 100644 index 00000000..59bac4cf --- /dev/null +++ b/tests/mocks/futures/account/leverage_brackets.json @@ -0,0 +1,24 @@ +[ + { + "symbol": "ETHUSDT", + "notionalCoef": 1.50, + "brackets": [ + { + "bracket": 1, + "initialLeverage": 75, + "notionalCap": 10000, + "notionalFloor": 0, + "maintMarginRatio": 0.0065, + "cum": 0 + }, + { + "bracket": 2, + "initialLeverage": 50, + "notionalCap": 20000, + "notionalFloor": 10000, + "maintMarginRatio": 0.005, + "cum": 10 + } + ] + } +] \ No newline at end of file