From b23948b4bd967a7ae9e25b5726a27abd599d6f41 Mon Sep 17 00:00:00 2001 From: alplabin <122352306+alplabin@users.noreply.github.com> Date: Wed, 30 Oct 2024 18:53:31 +0900 Subject: [PATCH] Release v1.3.0 --- CHANGELOG.md | 31 ++ Cargo.toml | 2 +- README.md | 8 + examples/Cargo.toml | 52 ++ examples/market/book_ticker.rs | 4 +- examples/market/klines.rs | 4 +- .../rolling_window_price_change_statistics.rs | 5 +- examples/market/ticker_price.rs | 2 +- examples/market/ticker_trading_day.rs | 20 + examples/market/ticker_twenty_four_hr.rs | 6 +- examples/market/ui_klines.rs | 18 + examples/tokio_tungstenite.rs | 8 + examples/trade/get_allocations.rs | 19 + examples/trade/get_commission_rates.rs | 19 + examples/trade/get_prevented_matches.rs | 21 + examples/trade/new_oco_order.rs | 15 +- examples/trade/new_order.rs | 3 +- examples/trade/new_oto_order.rs | 31 ++ examples/trade/new_otoco_order.rs | 36 ++ examples/wallet/account_info.rs | 19 + examples/wallet/balance.rs | 19 + examples/wallet/delist_schedule.rs | 19 + examples/wallet/deposit_address_list.rs | 19 + examples/wallet/deposit_credit_apply.rs | 19 + examples/wallet/transfer_history.rs | 19 + src/market/agg_trades.rs | 3 +- src/market/avg_price.rs | 2 +- src/market/book_ticker.rs | 16 +- src/market/depth.rs | 8 +- src/market/exchange_info.rs | 60 ++- src/market/historical_trades.rs | 2 +- src/market/klines.rs | 13 +- src/market/mod.rs | 12 + .../rolling_window_price_change_statistics.rs | 31 +- src/market/ticker_price.rs | 14 +- src/market/ticker_trading_day.rs | 120 +++++ src/market/ticker_twenty_four_hr.rs | 32 +- src/market/trades.rs | 2 +- src/market/ui_klines.rs | 133 +++++ src/market_stream/avg_price.rs | 35 ++ src/market_stream/mod.rs | 6 + src/stream/close_listen_key.rs | 2 +- src/stream/new_listen_key.rs | 2 +- src/stream/renew_listen_key.rs | 2 +- src/trade/account.rs | 16 +- src/trade/all_orders.rs | 9 +- src/trade/get_allocations.rs | 153 ++++++ src/trade/get_commission_rates.rs | 76 +++ src/trade/get_oco_order.rs | 2 +- src/trade/get_oco_orders.rs | 2 +- src/trade/get_open_oco_orders.rs | 2 +- src/trade/get_order.rs | 2 +- src/trade/get_prevented_matches.rs | 150 ++++++ src/trade/mod.rs | 62 ++- src/trade/my_trades.rs | 14 +- src/trade/new_oco_order.rs | 287 +++++++--- src/trade/new_order_test.rs | 14 + src/trade/new_oto_order.rs | 368 +++++++++++++ src/trade/new_otoco_order.rs | 510 ++++++++++++++++++ src/trade/open_orders.rs | 4 +- src/trade/order.rs | 24 + src/trade/order_limit_usage.rs | 2 +- src/utils.rs | 18 +- src/version.rs | 2 +- src/wallet/account_info.rs | 92 ++++ src/wallet/balance.rs | 94 ++++ src/wallet/delist_schedule.rs | 92 ++++ src/wallet/deposit_address_list.rs | 94 ++++ src/wallet/deposit_credit_apply.rs | 125 +++++ src/wallet/dustable_assets.rs | 11 + src/wallet/mod.rs | 36 ++ src/wallet/transfer_history.rs | 147 +++++ src/wallet/withdraw_history.rs | 4 +- 73 files changed, 3139 insertions(+), 186 deletions(-) create mode 100644 examples/market/ticker_trading_day.rs create mode 100644 examples/market/ui_klines.rs create mode 100644 examples/trade/get_allocations.rs create mode 100644 examples/trade/get_commission_rates.rs create mode 100644 examples/trade/get_prevented_matches.rs create mode 100644 examples/trade/new_oto_order.rs create mode 100644 examples/trade/new_otoco_order.rs create mode 100644 examples/wallet/account_info.rs create mode 100644 examples/wallet/balance.rs create mode 100644 examples/wallet/delist_schedule.rs create mode 100644 examples/wallet/deposit_address_list.rs create mode 100644 examples/wallet/deposit_credit_apply.rs create mode 100644 examples/wallet/transfer_history.rs create mode 100644 src/market/ticker_trading_day.rs create mode 100644 src/market/ui_klines.rs create mode 100644 src/market_stream/avg_price.rs create mode 100644 src/trade/get_allocations.rs create mode 100644 src/trade/get_commission_rates.rs create mode 100644 src/trade/get_prevented_matches.rs create mode 100644 src/trade/new_oto_order.rs create mode 100644 src/trade/new_otoco_order.rs create mode 100644 src/wallet/account_info.rs create mode 100644 src/wallet/balance.rs create mode 100644 src/wallet/delist_schedule.rs create mode 100644 src/wallet/deposit_address_list.rs create mode 100644 src/wallet/deposit_credit_apply.rs create mode 100644 src/wallet/transfer_history.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b00ffb..f21ecec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Changelog +## 1.3.0 - 2024-10-31 +### Added +- Market endpoints + - `GET /api/v3/ticker/tradingDay` + - `GET /api/v3/uiKlines` + +- Trade endpoints + - `GET /api/v3/myAllocations` + - `GET /api/v3/account/commission` + - `GET /api/v3/myPreventedMatches` + +- Wallet endpoints + - `GET /sapi/v1/account/info` + - `GET /sapi/v1/asset/wallet/balance` + - `GET /sapi/v1/spot/delist-schedule` + - `GET /sapi/v1/capital/deposit/address/list` + - `POST /sapi/v1/capital/deposit/credit-apply` + - `GET /sapi/v1/asset/custody/transfer-history` + +- WebsocketStream: + - `@avgPrice` + +### Updated +- Updated deprecated trade endpoint `POST /api/v3/order/oco` to `POST /api/v3/orderList/oco` +- Added parameters `permissions`, `showPermissionSets` and `symbolStatus` to endpoint `GET /api/v3/exchangeInfo` +- Added parameter `time_zone` to endpoint `GET /api/v3/klines` +- Added parameter `type` to endpoints `GET /api/v3/ticker` and `GET /api/v3/ticker/24hr` +- Added parameter `omitZeroBalances` to endpoint `GET /api/v3/account` +- Added parameter `computeCommissionRates` to endpoint `POST /api/v3/order/test` +- Updated `ed25519` and `hmac` signature to allow the function to handle any error type that implements the `Error` trait, including `ed25519_dalek::pkcs8::Error`, without needing explicit conversions between error types. + ## 1.2.1 - 2024-10-03 ### Updated - Updated url links diff --git a/Cargo.toml b/Cargo.toml index 6882b1c..adf8501 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "binance_spot_connector_rust" -version = "1.2.1" +version = "1.3.0" authors = ["Binance"] edition = "2021" resolver = "2" diff --git a/README.md b/README.md index f66accb..5b7d0fc 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,7 @@ use binance_spot_connector_rust::{ market::klines::KlineInterval, market_stream::kline::KlineStream, tokio_tungstenite::BinanceWebSocketClient, }; +use std::time::Duration; use env_logger::Builder; use futures_util::StreamExt; @@ -124,8 +125,15 @@ async fn main() { &KlineStream::new("BTCUSDT", KlineInterval::Minutes1).into() ]) .await; + // Start a timer for 10 seconds + let timer = tokio::time::Instant::now(); + let duration = Duration::new(10, 0); // Read messages while let Some(message) = conn.as_mut().next().await { + if timer.elapsed() >= duration { + log::info!("10 seconds elapsed, exiting loop."); + break; // Exit the loop after 10 seconds + } match message { Ok(message) => { let binary_data = message.into_data(); diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 7bfab1a..057ab9c 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -61,6 +61,10 @@ path="market/agg_trades.rs" name="market_klines" path="market/klines.rs" +[[example]] +name="market_ui_klines" +path="market/ui_klines.rs" + [[example]] name="market_avg_price" path="market/avg_price.rs" @@ -69,6 +73,10 @@ path="market/avg_price.rs" name="market_ticker_twenty_four_hr" path="market/ticker_twenty_four_hr.rs" +[[example]] +name="market_ticker_trading_day" +path="market/ticker_trading_day.rs" + [[example]] name="market_ticker_price" path="market/ticker_price.rs" @@ -117,6 +125,14 @@ path="trade/all_orders.rs" name="trade_new_oco_order" path="trade/new_oco_order.rs" +[[example]] +name="trade_new_oto_order" +path="trade/new_oto_order.rs" + +[[example]] +name="trade_new_otoco_order" +path="trade/new_otoco_order.rs" + [[example]] name="trade_get_oco_order" path="trade/get_oco_order.rs" @@ -125,6 +141,14 @@ path="trade/get_oco_order.rs" name="trade_cancel_oco_order" path="trade/cancel_oco_order.rs" +[[example]] +name="trade_get_allocations" +path="trade/get_allocations.rs" + +[[example]] +name="trade_get_commission_rates" +path="trade/get_commission_rates.rs" + [[example]] name="trade_get_oco_orders" path="trade/get_oco_orders.rs" @@ -133,6 +157,10 @@ path="trade/get_oco_orders.rs" name="trade_get_open_oco_orders" path="trade/get_open_oco_orders.rs" +[[example]] +name="get_prevented_matches" +path="trade/get_prevented_matches.rs" + [[example]] name="trade_account" path="trade/account.rs" @@ -365,6 +393,30 @@ path="wallet/user_asset.rs" name="wallet_api_key_permission" path="wallet/api_key_permission.rs" +[[example]] +name="wallet_account_info" +path="wallet/account_info.rs" + +[[example]] +name="wallet_balance" +path="wallet/balance.rs" + +[[example]] +name="wallet_delist_schedule" +path="wallet/delist_schedule.rs" + +[[example]] +name="wallet_deposit_address_list" +path="wallet/deposit_address_list.rs" + +[[example]] +name="wallet_deposit_credit_apply" +path="wallet/deposit_credit_apply.rs" + +[[example]] +name="wallet_transfer_history" +path="wallet/transfer_history.rs" + [[example]] name="stream_new_listen_key" path="stream/new_listen_key.rs" diff --git a/examples/market/book_ticker.rs b/examples/market/book_ticker.rs index 5263f0d..be9879e 100644 --- a/examples/market/book_ticker.rs +++ b/examples/market/book_ticker.rs @@ -11,9 +11,7 @@ async fn main() -> Result<(), Error> { .init(); let client = BinanceHttpClient::default(); - let request = market::book_ticker() - .symbol("BNBUSDT") - .symbols(vec!["BTCUSDT", "BNBBTC"]); + let request = market::book_ticker().symbols(vec!["BTCUSDT", "BNBBTC"]); let data = client.send(request).await?.into_body_str().await?; log::info!("{}", data); Ok(()) diff --git a/examples/market/klines.rs b/examples/market/klines.rs index acf68e9..5dd4c91 100644 --- a/examples/market/klines.rs +++ b/examples/market/klines.rs @@ -11,9 +11,7 @@ async fn main() -> Result<(), Error> { .init(); let client = BinanceHttpClient::default(); - let request = market::klines("BNBUSDT", KlineInterval::Hours1) - .start_time(1654079109000) - .end_time(1654079209000); + let request = market::klines("BNBUSDT", KlineInterval::Hours1).limit(5); let data = client.send(request).await?.into_body_str().await?; log::info!("{}", data); Ok(()) diff --git a/examples/market/rolling_window_price_change_statistics.rs b/examples/market/rolling_window_price_change_statistics.rs index f928239..a0939aa 100644 --- a/examples/market/rolling_window_price_change_statistics.rs +++ b/examples/market/rolling_window_price_change_statistics.rs @@ -11,9 +11,8 @@ async fn main() -> Result<(), Error> { .init(); let client = BinanceHttpClient::default(); - let request = market::rolling_window_price_change_statistics() - .symbol("BNBUSDT") - .symbols(vec!["BTCUSDT", "BNBBTC"]); + let request = + market::rolling_window_price_change_statistics().symbols(vec!["BTCUSDT", "BNBBTC"]); let data = client.send(request).await?.into_body_str().await?; log::info!("{}", data); Ok(()) diff --git a/examples/market/ticker_price.rs b/examples/market/ticker_price.rs index 48b7475..1cc0c2c 100644 --- a/examples/market/ticker_price.rs +++ b/examples/market/ticker_price.rs @@ -11,7 +11,7 @@ async fn main() -> Result<(), Error> { .init(); let client = BinanceHttpClient::default(); - let request = market::ticker_price().symbols(vec!["BTCUSDT", "BNBBTC"]); + let request = market::ticker_price().symbols(vec!["BTCUSDT", "BNBUSDT"]); let data = client.send(request).await?.into_body_str().await?; log::info!("{}", data); Ok(()) diff --git a/examples/market/ticker_trading_day.rs b/examples/market/ticker_trading_day.rs new file mode 100644 index 0000000..50e2f8d --- /dev/null +++ b/examples/market/ticker_trading_day.rs @@ -0,0 +1,20 @@ +use binance_spot_connector_rust::{ + hyper::{BinanceHttpClient, Error}, + market::{self, rolling_window_price_change_statistics::TickerType}, +}; +use env_logger::Builder; + +#[tokio::main] +async fn main() -> Result<(), Error> { + Builder::from_default_env() + .filter(None, log::LevelFilter::Info) + .init(); + + let client = BinanceHttpClient::default(); + let request = market::ticker_trading_day() + .symbol("BNBUSDT") + .ticker_type(TickerType::Mini); + let data = client.send(request).await?.into_body_str().await?; + log::info!("{}", data); + Ok(()) +} diff --git a/examples/market/ticker_twenty_four_hr.rs b/examples/market/ticker_twenty_four_hr.rs index de32e17..1692c89 100644 --- a/examples/market/ticker_twenty_four_hr.rs +++ b/examples/market/ticker_twenty_four_hr.rs @@ -1,6 +1,6 @@ use binance_spot_connector_rust::{ hyper::{BinanceHttpClient, Error}, - market, + market::{self, rolling_window_price_change_statistics::TickerType}, }; use env_logger::Builder; @@ -12,8 +12,8 @@ async fn main() -> Result<(), Error> { let client = BinanceHttpClient::default(); let request = market::ticker_twenty_four_hr() - .symbol("BNBUSDT") - .symbols(vec!["BTCUSDT", "BNBBTC"]); + .symbols(vec!["BTCUSDT", "BNBBTC"]) + .ticker_type(TickerType::Mini); let data = client.send(request).await?.into_body_str().await?; log::info!("{}", data); Ok(()) diff --git a/examples/market/ui_klines.rs b/examples/market/ui_klines.rs new file mode 100644 index 0000000..f33a694 --- /dev/null +++ b/examples/market/ui_klines.rs @@ -0,0 +1,18 @@ +use binance_spot_connector_rust::{ + hyper::{BinanceHttpClient, Error}, + market::{self, klines::KlineInterval}, +}; +use env_logger::Builder; + +#[tokio::main] +async fn main() -> Result<(), Error> { + Builder::from_default_env() + .filter(None, log::LevelFilter::Info) + .init(); + + let client = BinanceHttpClient::default(); + let request = market::ui_klines("BNBUSDT", KlineInterval::Hours1).limit(5); + let data = client.send(request).await?.into_body_str().await?; + log::info!("{}", data); + Ok(()) +} diff --git a/examples/tokio_tungstenite.rs b/examples/tokio_tungstenite.rs index c6fe8fc..841a1e1 100644 --- a/examples/tokio_tungstenite.rs +++ b/examples/tokio_tungstenite.rs @@ -4,6 +4,7 @@ use binance_spot_connector_rust::{ }; use env_logger::Builder; use futures_util::StreamExt; +use std::time::Duration; const BINANCE_WSS_BASE_URL: &str = "wss://stream.binance.com:9443/ws"; @@ -21,8 +22,15 @@ async fn main() { &KlineStream::new("BTCUSDT", KlineInterval::Minutes1).into() ]) .await; + // Start a timer for 10 seconds + let timer = tokio::time::Instant::now(); + let duration = Duration::new(10, 0); // Read messages while let Some(message) = conn.as_mut().next().await { + if timer.elapsed() >= duration { + log::info!("10 seconds elapsed, exiting loop."); + break; // Exit the loop after 10 seconds + } match message { Ok(message) => { let data = message.into_data(); diff --git a/examples/trade/get_allocations.rs b/examples/trade/get_allocations.rs new file mode 100644 index 0000000..4a9249f --- /dev/null +++ b/examples/trade/get_allocations.rs @@ -0,0 +1,19 @@ +use binance_spot_connector_rust::{ + http::Credentials, + hyper::{BinanceHttpClient, Error}, + trade, +}; +use env_logger::Builder; + +#[tokio::main] +async fn main() -> Result<(), Error> { + Builder::from_default_env() + .filter(None, log::LevelFilter::Info) + .init(); + let credentials = Credentials::from_hmac("api-key".to_owned(), "api-secret".to_owned()); + let client = BinanceHttpClient::default().credentials(credentials); + let request = trade::get_allocations("BNBUSDT").limit(500); + let data = client.send(request).await?.into_body_str().await?; + log::info!("{}", data); + Ok(()) +} diff --git a/examples/trade/get_commission_rates.rs b/examples/trade/get_commission_rates.rs new file mode 100644 index 0000000..3e866f6 --- /dev/null +++ b/examples/trade/get_commission_rates.rs @@ -0,0 +1,19 @@ +use binance_spot_connector_rust::{ + http::Credentials, + hyper::{BinanceHttpClient, Error}, + trade, +}; +use env_logger::Builder; + +#[tokio::main] +async fn main() -> Result<(), Error> { + Builder::from_default_env() + .filter(None, log::LevelFilter::Info) + .init(); + let credentials = Credentials::from_hmac("api-key".to_owned(), "api-secret".to_owned()); + let client = BinanceHttpClient::default().credentials(credentials); + let request = trade::get_commission_rates("BNBUSDT"); + let data = client.send(request).await?.into_body_str().await?; + log::info!("{}", data); + Ok(()) +} diff --git a/examples/trade/get_prevented_matches.rs b/examples/trade/get_prevented_matches.rs new file mode 100644 index 0000000..5f16b71 --- /dev/null +++ b/examples/trade/get_prevented_matches.rs @@ -0,0 +1,21 @@ +use binance_spot_connector_rust::{ + http::Credentials, + hyper::{BinanceHttpClient, Error}, + trade, +}; +use env_logger::Builder; + +#[tokio::main] +async fn main() -> Result<(), Error> { + Builder::from_default_env() + .filter(None, log::LevelFilter::Info) + .init(); + let credentials = Credentials::from_hmac("api-key".to_owned(), "api-secret".to_owned()); + let client = BinanceHttpClient::default().credentials(credentials); + let request = trade::get_prevented_matches("BNBUSDT") + .order_id(11) + .limit(500); + let data = client.send(request).await?.into_body_str().await?; + log::info!("{}", data); + Ok(()) +} diff --git a/examples/trade/new_oco_order.rs b/examples/trade/new_oco_order.rs index 603253a..860b19c 100644 --- a/examples/trade/new_oco_order.rs +++ b/examples/trade/new_oco_order.rs @@ -16,9 +16,18 @@ async fn main() -> Result<(), Error> { .init(); let credentials = Credentials::from_hmac("api-key".to_owned(), "api-secret".to_owned()); let client = BinanceHttpClient::default().credentials(credentials); - let request = trade::new_oco_order("BNBUSDT", Side::Sell, dec!(0.1), dec!(400.15), dec!(390.3)) - .stop_limit_price(dec!(380.3)) - .stop_limit_time_in_force(TimeInForce::Gtc); + let request = trade::new_oco_order( + "BNBUSDT", + Side::Sell, + dec!(1.0), + "LIMIT_MAKER", + "STOP_LOSS_LIMIT", + ) + .above_price(dec!(610.1)) + .below_price(dec!(600.3)) + .below_stop_price(dec!(598.2)) + .below_trailing_delta(dec!(60)) + .below_time_in_force(TimeInForce::Gtc); let data = client.send(request).await?.into_body_str().await?; log::info!("{}", data); Ok(()) diff --git a/examples/trade/new_order.rs b/examples/trade/new_order.rs index 974a6fa..2a97fbb 100644 --- a/examples/trade/new_order.rs +++ b/examples/trade/new_order.rs @@ -12,8 +12,7 @@ async fn main() -> Result<(), Error> { .filter(None, log::LevelFilter::Debug) .init(); let credentials = Credentials::from_hmac("the_api_key".to_owned(), "the_api_secret".to_owned()); - let client = - BinanceHttpClient::with_url("https://testnet.binance.vision").credentials(credentials); + let client = BinanceHttpClient::default().credentials(credentials); let request = trade::new_order("BNBUSDT", Side::Sell, "MARKET").quantity(dec!(0.1)); let data = client.send(request).await?.into_body_str().await?; log::info!("{}", data); diff --git a/examples/trade/new_oto_order.rs b/examples/trade/new_oto_order.rs new file mode 100644 index 0000000..df44f62 --- /dev/null +++ b/examples/trade/new_oto_order.rs @@ -0,0 +1,31 @@ +use binance_spot_connector_rust::{ + http::Credentials, + hyper::{BinanceHttpClient, Error}, + trade::{ + self, + order::{Side, TimeInForce, WorkingMandatoryParams}, + }, +}; +use env_logger::Builder; +use rust_decimal_macros::dec; + +#[tokio::main] +async fn main() -> Result<(), Error> { + Builder::from_default_env() + .filter(None, log::LevelFilter::Info) + .init(); + let credentials = Credentials::from_hmac("the_api_key".to_owned(), "the_api_secret".to_owned()); + let client = BinanceHttpClient::default().credentials(credentials); + let request = trade::new_oto_order( + "BNBUSDT", + WorkingMandatoryParams::new("LIMIT", Side::Buy, dec!(596.0), dec!(1.0)), + "LIMIT_MAKER", + Side::Buy, + dec!(1.0), + ) + .working_time_in_force(TimeInForce::Gtc) + .pending_price(dec!(598.1)); + let data = client.send(request).await?.into_body_str().await?; + log::info!("{}", data); + Ok(()) +} diff --git a/examples/trade/new_otoco_order.rs b/examples/trade/new_otoco_order.rs new file mode 100644 index 0000000..7f65e45 --- /dev/null +++ b/examples/trade/new_otoco_order.rs @@ -0,0 +1,36 @@ +use binance_spot_connector_rust::{ + http::Credentials, + hyper::{BinanceHttpClient, Error}, + trade::{ + self, + order::{Side, TimeInForce, WorkingMandatoryParams}, + }, +}; +use env_logger::Builder; +use rust_decimal_macros::dec; + +#[tokio::main] +async fn main() -> Result<(), Error> { + Builder::from_default_env() + .filter(None, log::LevelFilter::Info) + .init(); + let credentials = Credentials::from_hmac("the_api_key".to_owned(), "the_api_secret".to_owned()); + let client = BinanceHttpClient::default().credentials(credentials); + let request = trade::new_otoco_order( + "BNBUSDT", + WorkingMandatoryParams::new("LIMIT", Side::Sell, dec!(305), dec!(0.5)), + Side::Sell, + dec!(0.5), + "LIMIT_MAKER", + ) + .working_time_in_force(TimeInForce::Gtc) + .pending_above_price(dec!(308)) + .pending_below_type("STOP_LOSS_LIMIT") + .pending_below_stop_price(dec!(300.5)) + .pending_below_trailing_delta(dec!(30)) + .pending_below_time_in_force(TimeInForce::Gtc) + .pending_below_price(dec!(301)); + let data = client.send(request).await?.into_body_str().await?; + log::info!("{}", data); + Ok(()) +} diff --git a/examples/wallet/account_info.rs b/examples/wallet/account_info.rs new file mode 100644 index 0000000..00607b6 --- /dev/null +++ b/examples/wallet/account_info.rs @@ -0,0 +1,19 @@ +use binance_spot_connector_rust::{ + http::Credentials, + hyper::{BinanceHttpClient, Error}, + wallet, +}; +use env_logger::Builder; + +#[tokio::main] +async fn main() -> Result<(), Error> { + Builder::from_default_env() + .filter(None, log::LevelFilter::Info) + .init(); + let credentials = Credentials::from_hmac("api-key".to_owned(), "api-secret".to_owned()); + let client = BinanceHttpClient::default().credentials(credentials); + let request = wallet::account_info().recv_window(5000); + let data = client.send(request).await?.into_body_str().await?; + log::info!("{}", data); + Ok(()) +} diff --git a/examples/wallet/balance.rs b/examples/wallet/balance.rs new file mode 100644 index 0000000..31ed35a --- /dev/null +++ b/examples/wallet/balance.rs @@ -0,0 +1,19 @@ +use binance_spot_connector_rust::{ + http::Credentials, + hyper::{BinanceHttpClient, Error}, + wallet, +}; +use env_logger::Builder; + +#[tokio::main] +async fn main() -> Result<(), Error> { + Builder::from_default_env() + .filter(None, log::LevelFilter::Info) + .init(); + let credentials = Credentials::from_hmac("api-key".to_owned(), "api-secret".to_owned()); + let client = BinanceHttpClient::default().credentials(credentials); + let request = wallet::balance().recv_window(5000); + let data = client.send(request).await?.into_body_str().await?; + log::info!("{}", data); + Ok(()) +} diff --git a/examples/wallet/delist_schedule.rs b/examples/wallet/delist_schedule.rs new file mode 100644 index 0000000..982a35a --- /dev/null +++ b/examples/wallet/delist_schedule.rs @@ -0,0 +1,19 @@ +use binance_spot_connector_rust::{ + http::Credentials, + hyper::{BinanceHttpClient, Error}, + wallet, +}; +use env_logger::Builder; + +#[tokio::main] +async fn main() -> Result<(), Error> { + Builder::from_default_env() + .filter(None, log::LevelFilter::Info) + .init(); + let credentials = Credentials::from_hmac("api-key".to_owned(), "api-secret".to_owned()); + let client = BinanceHttpClient::default().credentials(credentials); + let request = wallet::delist_schedule().recv_window(5000); + let data = client.send(request).await?.into_body_str().await?; + log::info!("{}", data); + Ok(()) +} diff --git a/examples/wallet/deposit_address_list.rs b/examples/wallet/deposit_address_list.rs new file mode 100644 index 0000000..44f0532 --- /dev/null +++ b/examples/wallet/deposit_address_list.rs @@ -0,0 +1,19 @@ +use binance_spot_connector_rust::{ + http::Credentials, + hyper::{BinanceHttpClient, Error}, + wallet, +}; +use env_logger::Builder; + +#[tokio::main] +async fn main() -> Result<(), Error> { + Builder::from_default_env() + .filter(None, log::LevelFilter::Info) + .init(); + let credentials = Credentials::from_hmac("api-key".to_owned(), "api-secret".to_owned()); + let client = BinanceHttpClient::default().credentials(credentials); + let request = wallet::deposit_address_list("BNB").network("ETH"); + let data = client.send(request).await?.into_body_str().await?; + log::info!("{}", data); + Ok(()) +} diff --git a/examples/wallet/deposit_credit_apply.rs b/examples/wallet/deposit_credit_apply.rs new file mode 100644 index 0000000..a88ee88 --- /dev/null +++ b/examples/wallet/deposit_credit_apply.rs @@ -0,0 +1,19 @@ +use binance_spot_connector_rust::{ + http::Credentials, + hyper::{BinanceHttpClient, Error}, + wallet, +}; +use env_logger::Builder; + +#[tokio::main] +async fn main() -> Result<(), Error> { + Builder::from_default_env() + .filter(None, log::LevelFilter::Info) + .init(); + let credentials = Credentials::from_hmac("api-key".to_owned(), "api-secret".to_owned()); + let client = BinanceHttpClient::default().credentials(credentials); + let request = wallet::deposit_credit_apply().deposit_id(1); + let data = client.send(request).await?.into_body_str().await?; + log::info!("{}", data); + Ok(()) +} diff --git a/examples/wallet/transfer_history.rs b/examples/wallet/transfer_history.rs new file mode 100644 index 0000000..1b957f2 --- /dev/null +++ b/examples/wallet/transfer_history.rs @@ -0,0 +1,19 @@ +use binance_spot_connector_rust::{ + http::Credentials, + hyper::{BinanceHttpClient, Error}, + wallet, +}; +use env_logger::Builder; + +#[tokio::main] +async fn main() -> Result<(), Error> { + Builder::from_default_env() + .filter(None, log::LevelFilter::Info) + .init(); + let credentials = Credentials::from_hmac("api-key".to_owned(), "api-secret".to_owned()); + let client = BinanceHttpClient::default().credentials(credentials); + let request = wallet::transfer_history("a@a", 1695205406000, 1695208406000); + let data = client.send(request).await?.into_body_str().await?; + log::info!("{}", data); + Ok(()) +} diff --git a/src/market/agg_trades.rs b/src/market/agg_trades.rs index b00db8c..5701286 100644 --- a/src/market/agg_trades.rs +++ b/src/market/agg_trades.rs @@ -3,10 +3,9 @@ use crate::http::{request::Request, Method}; /// `GET /api/v3/aggTrades` /// /// Get compressed, aggregate trades. Trades that fill at the time, from the same order, with the same price will have the quantity aggregated. -/// * If `startTime` and `endTime` are sent, time between startTime and endTime must be less than 1 hour. /// * If `fromId`, `startTime`, and `endTime` are not sent, the most recent aggregate trades will be returned. /// -/// Weight(IP): 1 +/// Weight(IP): 2 /// /// # Example /// diff --git a/src/market/avg_price.rs b/src/market/avg_price.rs index dc27796..6334271 100644 --- a/src/market/avg_price.rs +++ b/src/market/avg_price.rs @@ -4,7 +4,7 @@ use crate::http::{request::Request, Method}; /// /// Current average price for a symbol. /// -/// Weight(IP): 1 +/// Weight(IP): 2 /// /// # Example /// diff --git a/src/market/book_ticker.rs b/src/market/book_ticker.rs index 5a8df89..b953839 100644 --- a/src/market/book_ticker.rs +++ b/src/market/book_ticker.rs @@ -7,15 +7,15 @@ use crate::http::{request::Request, Method}; /// * If the symbol is not sent, bookTickers for all symbols will be returned in an array. /// /// Weight(IP): -/// * `1` for a single symbol; -/// * `2` when the symbol parameter is omitted; +/// * `2` for a single symbol; +/// * `4` when the symbol parameter is omitted; /// /// # Example /// /// ``` /// use binance_spot_connector_rust::market; /// -/// let request = market::book_ticker().symbol("BNBUSDT").symbols(vec!["BTCUSDT","BNBBTC"]); +/// let request = market::book_ticker().symbols(vec!["BTCUSDT","BNBBTC"]); /// ``` pub struct BookTicker { symbol: Option, @@ -79,10 +79,7 @@ mod tests { #[test] fn market_book_ticker_convert_to_request_test() { - let request: Request = BookTicker::new() - .symbol("BNBUSDT") - .symbols(vec!["BTCUSDT", "BNBBTC"]) - .into(); + let request: Request = BookTicker::new().symbols(vec!["BTCUSDT", "BNBBTC"]).into(); assert_eq!( request, @@ -90,10 +87,7 @@ mod tests { path: "/api/v3/ticker/bookTicker".to_owned(), credentials: None, method: Method::Get, - params: vec![ - ("symbol".to_owned(), "BNBUSDT".to_string()), - ("symbols".to_owned(), "[\"BTCUSDT\",\"BNBBTC\"]".to_string()), - ], + params: vec![("symbols".to_owned(), "[\"BTCUSDT\",\"BNBBTC\"]".to_string())], sign: false } ); diff --git a/src/market/depth.rs b/src/market/depth.rs index d11ae3c..0cf54bf 100644 --- a/src/market/depth.rs +++ b/src/market/depth.rs @@ -4,10 +4,10 @@ use crate::http::{request::Request, Method}; /// /// | Limit | Weight(IP) | /// |---------------------|-------------| -/// | 1-100 | 1 | -/// | 101-500 | 5 | -/// | 501-1000 | 10 | -/// | 1001-5000 | 50 | +/// | 1-100 | 5 | +/// | 101-500 | 25 | +/// | 501-1000 | 50 | +/// | 1001-5000 | 250 | /// /// # Example /// diff --git a/src/market/exchange_info.rs b/src/market/exchange_info.rs index 4dc92ef..7ac519c 100644 --- a/src/market/exchange_info.rs +++ b/src/market/exchange_info.rs @@ -1,23 +1,37 @@ use crate::http::{request::Request, Method}; +use strum::Display; + +#[derive(Copy, Clone, Display)] +#[strum(serialize_all = "UPPERCASE")] +pub enum SymbolStatus { + Trading, + Halt, + Break, +} /// `GET /api/v3/exchangeInfo` /// /// Current exchange trading rules and symbol information /// /// * If any symbol provided in either symbol or symbols do not exist, the endpoint will throw an error. +/// * Permissions can support single or multiple values (e.g. SPOT, ["MARGIN","LEVERAGED"]). This cannot be used in combination with symbol or symbols. +/// * If permissions parameter not provided, all symbols that have either SPOT, MARGIN, or LEVERAGED permission will be exposed. /// -/// Weight(IP): 10 +/// Weight(IP): 20 /// /// # Example /// /// ``` /// use binance_spot_connector_rust::market; /// -/// let request = market::exchange_info().symbol("BNBUSDT").symbols(vec!["BTCUSDT","BNBBTC"]); +/// let request = market::exchange_info().symbols(vec!["BTCUSDT","BNBBTC"]); /// ``` pub struct ExchangeInfo { symbol: Option, symbols: Option>, + permissions: Option>, + show_permission_sets: Option, + symbol_status: Option, } impl ExchangeInfo { @@ -25,6 +39,9 @@ impl ExchangeInfo { Self { symbol: None, symbols: None, + permissions: None, + show_permission_sets: None, + symbol_status: None, } } @@ -37,6 +54,21 @@ impl ExchangeInfo { self.symbols = Some(symbols.iter().map(|s| s.to_string()).collect()); self } + + pub fn permissions(mut self, permissions: Vec<&str>) -> Self { + self.permissions = Some(permissions.iter().map(|s| s.to_string()).collect()); + self + } + + pub fn show_permission_sets(mut self, show_permission_sets: bool) -> Self { + self.show_permission_sets = Some(show_permission_sets); + self + } + + pub fn symbol_status(mut self, symbol_status: SymbolStatus) -> Self { + self.symbol_status = Some(symbol_status); + self + } } impl From for Request { @@ -54,6 +86,24 @@ impl From for Request { )); } + if let Some(permissions) = request.permissions { + params.push(( + "permissions".to_owned(), + format!("[\"{}\"]", permissions.join("\",\"")), + )); + } + + if let Some(show_permission_sets) = request.show_permission_sets { + params.push(( + "showPermissionSets".to_owned(), + show_permission_sets.to_string(), + )); + } + + if let Some(symbol_status) = request.symbol_status { + params.push(("symbolStatus".to_owned(), symbol_status.to_string())); + } + Request { path: "/api/v3/exchangeInfo".to_owned(), method: Method::Get, @@ -78,7 +128,6 @@ mod tests { #[test] fn market_exchange_info_convert_to_request_test() { let request: Request = ExchangeInfo::new() - .symbol("BNBUSDT") .symbols(vec!["BTCUSDT", "BNBBTC"]) .into(); @@ -88,10 +137,7 @@ mod tests { path: "/api/v3/exchangeInfo".to_owned(), credentials: None, method: Method::Get, - params: vec![ - ("symbol".to_owned(), "BNBUSDT".to_string()), - ("symbols".to_owned(), "[\"BTCUSDT\",\"BNBBTC\"]".to_string()), - ], + params: vec![("symbols".to_owned(), "[\"BTCUSDT\",\"BNBBTC\"]".to_string())], sign: false } ); diff --git a/src/market/historical_trades.rs b/src/market/historical_trades.rs index ac2689e..481ee61 100644 --- a/src/market/historical_trades.rs +++ b/src/market/historical_trades.rs @@ -6,7 +6,7 @@ use crate::http::{request::Request, Credentials, Method}; /// /// Get older market trades. /// -/// Weight(IP): 5 +/// Weight(IP): 25 /// /// # Example /// diff --git a/src/market/klines.rs b/src/market/klines.rs index 10ff2bf..d91cd00 100644 --- a/src/market/klines.rs +++ b/src/market/klines.rs @@ -42,7 +42,7 @@ pub enum KlineInterval { /// /// * If `startTime` and `endTime` are not sent, the most recent klines are returned. /// -/// Weight(IP): 1 +/// Weight(IP): 2 /// /// # Example /// @@ -58,6 +58,7 @@ pub struct Klines { interval: KlineInterval, start_time: Option, end_time: Option, + time_zone: Option, limit: Option, } @@ -68,6 +69,7 @@ impl Klines { interval, start_time: None, end_time: None, + time_zone: None, limit: None, } } @@ -82,6 +84,11 @@ impl Klines { self } + pub fn time_zone(mut self, time_zone: &str) -> Self { + self.time_zone = Some(time_zone.to_owned()); + self + } + pub fn limit(mut self, limit: u32) -> Self { self.limit = Some(limit); self @@ -103,6 +110,10 @@ impl From for Request { params.push(("endTime".to_owned(), end_time.to_string())); } + if let Some(time_zone) = request.time_zone { + params.push(("timeZone".to_owned(), time_zone)); + } + if let Some(limit) = request.limit { params.push(("limit".to_owned(), limit.to_string())); } diff --git a/src/market/mod.rs b/src/market/mod.rs index 85f38b2..bc305e1 100644 --- a/src/market/mod.rs +++ b/src/market/mod.rs @@ -10,9 +10,11 @@ pub mod klines; pub mod ping; pub mod rolling_window_price_change_statistics; pub mod ticker_price; +pub mod ticker_trading_day; pub mod ticker_twenty_four_hr; pub mod time; pub mod trades; +pub mod ui_klines; use agg_trades::AggTrades; use avg_price::AvgPrice; @@ -24,9 +26,11 @@ use klines::{KlineInterval, Klines}; use ping::Ping; use rolling_window_price_change_statistics::RollingWindowPriceChangeStatistics; use ticker_price::TickerPrice; +use ticker_trading_day::TickerTradingDay; use ticker_twenty_four_hr::Ticker24hr; use time::Time; use trades::Trades; +use ui_klines::UIKlines; pub fn ping() -> Ping { Ping::new() @@ -68,6 +72,10 @@ pub fn ticker_twenty_four_hr() -> Ticker24hr { Ticker24hr::new() } +pub fn ticker_trading_day() -> TickerTradingDay { + TickerTradingDay::new() +} + pub fn ticker_price() -> TickerPrice { TickerPrice::new() } @@ -79,3 +87,7 @@ pub fn book_ticker() -> BookTicker { pub fn rolling_window_price_change_statistics() -> RollingWindowPriceChangeStatistics { RollingWindowPriceChangeStatistics::new() } + +pub fn ui_klines(symbol: &str, interval: KlineInterval) -> UIKlines { + UIKlines::new(symbol, interval) +} diff --git a/src/market/rolling_window_price_change_statistics.rs b/src/market/rolling_window_price_change_statistics.rs index c32bc94..ecf52d0 100644 --- a/src/market/rolling_window_price_change_statistics.rs +++ b/src/market/rolling_window_price_change_statistics.rs @@ -1,4 +1,12 @@ use crate::http::{request::Request, Method}; +use strum::Display; + +#[derive(Copy, Clone, Display)] +#[strum(serialize_all = "UPPERCASE")] +pub enum TickerType { + Full, + Mini, +} /// `GET /api/v3/ticker` /// @@ -8,21 +16,22 @@ use crate::http::{request::Request, Method}; /// /// E.g. If the closeTime is 1641287867099 (January 04, 2022 09:17:47:099 UTC) , and the windowSize is 1d. the openTime will be: 1641201420000 (January 3, 2022, 09:17:00 UTC) /// -/// Weight(IP): 2 for each requested symbol regardless of windowSize. +/// Weight(IP): 4 for each requested symbol regardless of windowSize. /// -/// The weight for this request will cap at 100 once the number of symbols in the request is more than 50. +/// The weight for this request will cap at 200 once the number of symbols in the request is more than 50. /// /// # Example /// /// ``` /// use binance_spot_connector_rust::market; /// -/// let request = market::rolling_window_price_change_statistics().symbol("BNBUSDT").symbols(vec!["BTCUSDT","BNBBTC"]); +/// let request = market::rolling_window_price_change_statistics().symbols(vec!["BTCUSDT","BNBBTC"]); /// ``` pub struct RollingWindowPriceChangeStatistics { symbol: Option, symbols: Option>, window_size: Option, + ticker_type: Option, } impl RollingWindowPriceChangeStatistics { @@ -31,6 +40,7 @@ impl RollingWindowPriceChangeStatistics { symbol: None, symbols: None, window_size: None, + ticker_type: None, } } @@ -48,6 +58,11 @@ impl RollingWindowPriceChangeStatistics { self.window_size = Some(window_size.to_owned()); self } + + pub fn ticker_type(mut self, ticker_type: TickerType) -> Self { + self.ticker_type = Some(ticker_type); + self + } } impl Default for RollingWindowPriceChangeStatistics { @@ -75,6 +90,10 @@ impl From for Request { params.push(("windowSize".to_owned(), window_size)); } + if let Some(ticker_type) = request.ticker_type { + params.push(("type".to_owned(), ticker_type.to_string())); + } + Request { path: "/api/v3/ticker".to_owned(), method: Method::Get, @@ -93,7 +112,6 @@ mod tests { #[test] fn market_rolling_window_price_change_statistics_convert_to_request_test() { let request: Request = RollingWindowPriceChangeStatistics::new() - .symbol("BNBUSDT") .symbols(vec!["BTCUSDT", "BNBBTC"]) .into(); @@ -103,10 +121,7 @@ mod tests { path: "/api/v3/ticker".to_owned(), credentials: None, method: Method::Get, - params: vec![ - ("symbol".to_owned(), "BNBUSDT".to_string()), - ("symbols".to_owned(), "[\"BTCUSDT\",\"BNBBTC\"]".to_string()), - ], + params: vec![("symbols".to_owned(), "[\"BTCUSDT\",\"BNBBTC\"]".to_string())], sign: false } ); diff --git a/src/market/ticker_price.rs b/src/market/ticker_price.rs index 58ccbc2..60a2bbb 100644 --- a/src/market/ticker_price.rs +++ b/src/market/ticker_price.rs @@ -7,8 +7,8 @@ use crate::http::{request::Request, Method}; /// * If the symbol is not sent, prices for all symbols will be returned in an array. /// /// Weight(IP): -/// * `1` for a single symbol; -/// * `2` when the symbol parameter is omitted; +/// * `2` for a single symbol; +/// * `4` when the symbol parameter is omitted; /// /// # Example /// @@ -79,10 +79,7 @@ mod tests { #[test] fn market_ticker_price_convert_to_request_test() { - let request: Request = TickerPrice::new() - .symbol("BNBUSDT") - .symbols(vec!["BTCUSDT", "BNBBTC"]) - .into(); + let request: Request = TickerPrice::new().symbol("BNBUSDT").into(); assert_eq!( request, @@ -90,10 +87,7 @@ mod tests { path: "/api/v3/ticker/price".to_owned(), credentials: None, method: Method::Get, - params: vec![ - ("symbol".to_owned(), "BNBUSDT".to_string()), - ("symbols".to_owned(), "[\"BTCUSDT\",\"BNBBTC\"]".to_string()), - ], + params: vec![("symbol".to_owned(), "BNBUSDT".to_string())], sign: false } ); diff --git a/src/market/ticker_trading_day.rs b/src/market/ticker_trading_day.rs new file mode 100644 index 0000000..2ba39f5 --- /dev/null +++ b/src/market/ticker_trading_day.rs @@ -0,0 +1,120 @@ +use crate::http::{request::Request, Method}; +use crate::market::rolling_window_price_change_statistics::TickerType; + +/// `GET /api/v3/ticker/tradingDay` +/// +/// Price change statistics for a trading day. +/// +/// * Supported values for timeZone: +/// * Hours and minutes (e.g. -1:00, 05:45) +/// * Only hours (e.g. 0, 8, 4) +/// +/// Weight(IP): 4 for each requested `symbol` +/// +/// The weight for this request will cap at 200 once the number of symbols in the request is more than 50. +/// +/// # Example +/// +/// ``` +/// use binance_spot_connector_rust::market; +/// +/// let request = market::ticker_trading_day().symbol("BNBUSDT"); +/// ``` +pub struct TickerTradingDay { + symbol: Option, + symbols: Option>, + time_zone: Option, + ticker_type: Option, +} + +impl TickerTradingDay { + pub fn new() -> Self { + Self { + symbol: None, + symbols: None, + time_zone: None, + ticker_type: None, + } + } + + pub fn symbol(mut self, symbol: &str) -> Self { + self.symbol = Some(symbol.to_owned()); + self + } + + pub fn symbols(mut self, symbols: Vec<&str>) -> Self { + self.symbols = Some(symbols.iter().map(|s| s.to_string()).collect()); + self + } + + pub fn time_zone(mut self, time_zone: &str) -> Self { + self.time_zone = Some(time_zone.to_owned()); + self + } + + pub fn ticker_type(mut self, ticker_type: TickerType) -> Self { + self.ticker_type = Some(ticker_type); + self + } +} + +impl Default for TickerTradingDay { + fn default() -> Self { + Self::new() + } +} + +impl From for Request { + fn from(request: TickerTradingDay) -> Request { + let mut params = vec![]; + + if let Some(symbol) = request.symbol { + params.push(("symbol".to_owned(), symbol)); + } + + if let Some(symbols) = request.symbols { + params.push(( + "symbols".to_owned(), + format!("[\"{}\"]", symbols.join("\",\"")), + )); + } + + if let Some(time_zone) = request.time_zone { + params.push(("timeZone".to_owned(), time_zone)); + } + + if let Some(ticker_type) = request.ticker_type { + params.push(("type".to_owned(), ticker_type.to_string())); + } + + Request { + path: "/api/v3/ticker/tradingDay".to_owned(), + method: Method::Get, + params, + credentials: None, + sign: false, + } + } +} + +#[cfg(test)] +mod tests { + use super::TickerTradingDay; + use crate::http::{request::Request, Method}; + + #[test] + fn market_ticker_trading_day_convert_to_request_test() { + let request: Request = TickerTradingDay::new().symbol("BNBUSDT").into(); + + assert_eq!( + request, + Request { + path: "/api/v3/ticker/tradingDay".to_owned(), + credentials: None, + method: Method::Get, + params: vec![("symbol".to_owned(), "BNBUSDT".to_string())], + sign: false + } + ); + } +} diff --git a/src/market/ticker_twenty_four_hr.rs b/src/market/ticker_twenty_four_hr.rs index bd3e813..acbda8f 100644 --- a/src/market/ticker_twenty_four_hr.rs +++ b/src/market/ticker_twenty_four_hr.rs @@ -1,4 +1,5 @@ use crate::http::{request::Request, Method}; +use crate::market::rolling_window_price_change_statistics::TickerType; /// `GET /api/v3/ticker/24hr` /// @@ -7,19 +8,24 @@ use crate::http::{request::Request, Method}; /// * If the symbol is not sent, tickers for all symbols will be returned in an array. /// /// Weight(IP): -/// * `1` for a single symbol; -/// * `40` when the symbol parameter is omitted; +/// * `2` for a single symbol; +/// * `2` for 1-20 `symbols` sent; +/// * `40` for 21-100 `symbols` sent; +/// * `80` for 101 or more `symbols` sent; +/// * `80` when the symbol parameter is omitted; +/// * `80` when the symbols parameter is omitted; /// /// # Example /// /// ``` /// use binance_spot_connector_rust::market; /// -/// let request = market::ticker_twenty_four_hr().symbol("BNBUSDT").symbols(vec!["BTCUSDT","BNBBTC"]); +/// let request = market::ticker_twenty_four_hr().symbols(vec!["BTCUSDT","BNBBTC"]); /// ``` pub struct Ticker24hr { symbol: Option, symbols: Option>, + ticker_type: Option, } impl Ticker24hr { @@ -27,6 +33,7 @@ impl Ticker24hr { Self { symbol: None, symbols: None, + ticker_type: None, } } @@ -39,6 +46,11 @@ impl Ticker24hr { self.symbols = Some(symbols.iter().map(|s| s.to_string()).collect()); self } + + pub fn ticker_type(mut self, ticker_type: TickerType) -> Self { + self.ticker_type = Some(ticker_type); + self + } } impl From for Request { @@ -56,6 +68,10 @@ impl From for Request { )); } + if let Some(ticker_type) = request.ticker_type { + params.push(("type".to_owned(), ticker_type.to_string())); + } + Request { path: "/api/v3/ticker/24hr".to_owned(), method: Method::Get, @@ -79,10 +95,7 @@ mod tests { #[test] fn market_ticker_twenty_four_hr_convert_to_request_test() { - let request: Request = Ticker24hr::new() - .symbol("BNBUSDT") - .symbols(vec!["BTCUSDT", "BNBBTC"]) - .into(); + let request: Request = Ticker24hr::new().symbols(vec!["BTCUSDT", "BNBBTC"]).into(); assert_eq!( request, @@ -90,10 +103,7 @@ mod tests { path: "/api/v3/ticker/24hr".to_owned(), credentials: None, method: Method::Get, - params: vec![ - ("symbol".to_owned(), "BNBUSDT".to_string()), - ("symbols".to_owned(), "[\"BTCUSDT\",\"BNBBTC\"]".to_string()), - ], + params: vec![("symbols".to_owned(), "[\"BTCUSDT\",\"BNBBTC\"]".to_string())], sign: false } ); diff --git a/src/market/trades.rs b/src/market/trades.rs index 1b176c6..73d7adb 100644 --- a/src/market/trades.rs +++ b/src/market/trades.rs @@ -4,7 +4,7 @@ use crate::http::{request::Request, Method}; /// /// Get recent trades. /// -/// Weight(IP): 1 +/// Weight(IP): 25 /// /// # Example /// diff --git a/src/market/ui_klines.rs b/src/market/ui_klines.rs new file mode 100644 index 0000000..99c50f4 --- /dev/null +++ b/src/market/ui_klines.rs @@ -0,0 +1,133 @@ +use crate::http::{request::Request, Method}; +use crate::market::klines::KlineInterval; + +/// `GET /api/v3/uiKlines` +/// +/// The request is similar to klines having the same parameters and response. +/// `uiKlines` return modified kline data, optimized for presentation of candlestick charts. +/// +/// * If `startTime` and `endTime` are not sent, the most recent Klines are returned. +/// * Supported values for timeZone: +/// * Hours and minutes (e.g. `-1:00`, `05:45`) +/// * Only hours (e.g. `0`, `8`, `4`) +/// * Accepted range is strictly [-12:00 to +14:00] inclusive +/// * If `timeZone` provided, kline intervals are interpreted in that timezone instead of UTC. +/// * Note that `startTime` and `endTime` are always interpreted in UTC, regardless of `timeZone`. +/// +/// Weight(IP): 2 +/// +/// # Example +/// +/// ``` +/// use binance_spot_connector_rust::market::{self, klines::KlineInterval}; +/// +/// let request = market::ui_klines("BTCUSDT", KlineInterval::Minutes1) +/// .start_time(1654079109000) +/// .end_time(1654079209000); +/// ``` +pub struct UIKlines { + symbol: String, + interval: KlineInterval, + start_time: Option, + end_time: Option, + time_zone: Option, + limit: Option, +} + +impl UIKlines { + pub fn new(symbol: &str, interval: KlineInterval) -> Self { + Self { + symbol: symbol.to_owned(), + interval, + start_time: None, + end_time: None, + time_zone: None, + limit: None, + } + } + + pub fn start_time(mut self, start_time: u64) -> Self { + self.start_time = Some(start_time); + self + } + + pub fn end_time(mut self, end_time: u64) -> Self { + self.end_time = Some(end_time); + self + } + + pub fn time_zone(mut self, time_zone: &str) -> Self { + self.time_zone = Some(time_zone.to_owned()); + self + } + + pub fn limit(mut self, limit: u32) -> Self { + self.limit = Some(limit); + self + } +} + +impl From for Request { + fn from(request: UIKlines) -> Request { + let mut params = vec![ + ("symbol".to_owned(), request.symbol), + ("interval".to_owned(), request.interval.to_string()), + ]; + + if let Some(start_time) = request.start_time { + params.push(("startTime".to_owned(), start_time.to_string())); + } + + if let Some(end_time) = request.end_time { + params.push(("endTime".to_owned(), end_time.to_string())); + } + + if let Some(time_zone) = request.time_zone { + params.push(("timeZone".to_owned(), time_zone)); + } + + if let Some(limit) = request.limit { + params.push(("limit".to_owned(), limit.to_string())); + } + + Request { + path: "/api/v3/uiKlines".to_owned(), + method: Method::Get, + params, + credentials: None, + sign: false, + } + } +} + +#[cfg(test)] +mod tests { + use super::{KlineInterval, UIKlines}; + use crate::http::{request::Request, Method}; + + #[test] + fn market_ui_klines_convert_to_request_test() { + let request: Request = UIKlines::new("BTCUSDT", KlineInterval::Minutes1) + .start_time(1654079109000) + .end_time(1654079209000) + .limit(100) + .into(); + + assert_eq!( + request, + Request { + path: "/api/v3/uiKlines".to_owned(), + credentials: None, + method: Method::Get, + params: vec![ + ("symbol".to_owned(), "BTCUSDT".to_string()), + ("interval".to_owned(), "1m".to_string()), + ("startTime".to_owned(), "1654079109000".to_string()), + ("endTime".to_owned(), "1654079209000".to_string()), + ("limit".to_owned(), "100".to_string()) + ], + sign: false + } + ) + } +} diff --git a/src/market_stream/avg_price.rs b/src/market_stream/avg_price.rs new file mode 100644 index 0000000..7235e58 --- /dev/null +++ b/src/market_stream/avg_price.rs @@ -0,0 +1,35 @@ +use crate::websocket::Stream; + +/// Average Price +/// +/// Average price streams push changes in the average price over a fixed time interval. +/// +/// Update Speed: Real-time. +/// +/// [API Documentation](https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#average-price) +/// +/// # Example +/// +/// ``` +/// use binance_spot_connector_rust::market_stream::avg_price::AvgPriceStream; +/// +/// let stream = AvgPriceStream::new("BNBUSDT"); +/// ``` +pub struct AvgPriceStream { + symbol: String, +} + +impl AvgPriceStream { + pub fn new(symbol: &str) -> Self { + Self { + symbol: symbol.to_lowercase(), + } + } +} + +impl From for Stream { + /// Returns stream name as `@avgPrice` + fn from(stream: AvgPriceStream) -> Stream { + Stream::new(&format!("{}@avgPrice", stream.symbol)) + } +} diff --git a/src/market_stream/mod.rs b/src/market_stream/mod.rs index 35c101c..037c92d 100644 --- a/src/market_stream/mod.rs +++ b/src/market_stream/mod.rs @@ -2,6 +2,7 @@ //! //! A collection of SPOT Market Websocket streams. pub mod agg_trade; +pub mod avg_price; pub mod book_ticker; pub mod diff_depth; pub mod kline; @@ -14,6 +15,7 @@ pub mod trade; use crate::market::klines::KlineInterval; use agg_trade::AggTradeStream; +use avg_price::AvgPriceStream; use book_ticker::BookTickerStream; use diff_depth::DiffDepthStream; use kline::KlineStream; @@ -27,6 +29,10 @@ pub fn agg_trades(symbol: &str) -> AggTradeStream { AggTradeStream::new(symbol) } +pub fn avg_price(symbol: &str) -> AvgPriceStream { + AvgPriceStream::new(symbol) +} + pub fn individual_symbol_book_ticker(symbol: &str) -> BookTickerStream { BookTickerStream::from_symbol(symbol) } diff --git a/src/stream/close_listen_key.rs b/src/stream/close_listen_key.rs index 754dc85..f55b860 100644 --- a/src/stream/close_listen_key.rs +++ b/src/stream/close_listen_key.rs @@ -4,7 +4,7 @@ use crate::http::{request::Request, Credentials, Method}; /// /// Close out a user data stream. /// -/// Weight: 1 +/// Weight: 2 /// /// # Example /// diff --git a/src/stream/new_listen_key.rs b/src/stream/new_listen_key.rs index ebc3479..f126efd 100644 --- a/src/stream/new_listen_key.rs +++ b/src/stream/new_listen_key.rs @@ -5,7 +5,7 @@ use crate::http::{request::Request, Credentials, Method}; /// Start a new user data stream. /// The stream will close after 60 minutes unless a keepalive is sent. If the account has an active `listenKey`, that `listenKey` will be returned and its validity will be extended for 60 minutes. /// -/// Weight: 1 +/// Weight: 2 /// /// # Example /// diff --git a/src/stream/renew_listen_key.rs b/src/stream/renew_listen_key.rs index 98b2cc8..5ef2c15 100644 --- a/src/stream/renew_listen_key.rs +++ b/src/stream/renew_listen_key.rs @@ -4,7 +4,7 @@ use crate::http::{request::Request, Credentials, Method}; /// /// Keepalive a user data stream to prevent a time out. User data streams will close after 60 minutes. It's recommended to send a ping about every 30 minutes. /// -/// Weight: 1 +/// Weight: 2 /// /// # Example /// diff --git a/src/trade/account.rs b/src/trade/account.rs index 01599cf..bdfc4d0 100644 --- a/src/trade/account.rs +++ b/src/trade/account.rs @@ -4,7 +4,7 @@ use crate::http::{request::Request, Credentials, Method}; /// /// Get current account information. /// -/// Weight(IP): 10 +/// Weight(IP): 20 /// /// # Example /// @@ -14,6 +14,7 @@ use crate::http::{request::Request, Credentials, Method}; /// let request = trade::account(); /// ``` pub struct Account { + omit_zero_balances: Option, recv_window: Option, credentials: Option, } @@ -21,11 +22,17 @@ pub struct Account { impl Account { pub fn new() -> Self { Self { + omit_zero_balances: None, recv_window: None, credentials: None, } } + pub fn omit_zero_balances(mut self, omit_zero_balances: bool) -> Self { + self.omit_zero_balances = Some(omit_zero_balances); + self + } + pub fn recv_window(mut self, recv_window: i64) -> Self { self.recv_window = Some(recv_window); self @@ -41,6 +48,13 @@ impl From for Request { fn from(request: Account) -> Request { let mut params = vec![]; + if let Some(omit_zero_balances) = request.omit_zero_balances { + params.push(( + "omitZeroBalances".to_owned(), + omit_zero_balances.to_string(), + )); + } + if let Some(recv_window) = request.recv_window { params.push(("recvWindow".to_owned(), recv_window.to_string())); } diff --git a/src/trade/all_orders.rs b/src/trade/all_orders.rs index b5ebb46..346ba0e 100644 --- a/src/trade/all_orders.rs +++ b/src/trade/all_orders.rs @@ -2,13 +2,14 @@ use crate::http::{request::Request, Credentials, Method}; /// `GET /api/v3/allOrders` /// -/// Get all account orders; active, canceled, or filled.. +/// Get all account orders; active, canceled, or filled. /// -/// * If `orderId` is set, it will get orders >= that `orderId`. Otherwise most recent orders are returned. -/// * For some historical orders `cummulativeQuoteQty` will be < 0, meaning the data is not available at this time. +/// * If `orderId` is set, it will get orders >= that `orderId`. Otherwise most recent orders are returned. +/// * For some historical orders `cummulativeQuoteQty` will be < 0, meaning the data is not available at this time. /// * If `startTime` and/or `endTime` provided, `orderId` is not required +/// * The time between startTime and endTime can't be longer than 24 hours. /// -/// Weight(IP): 10 +/// Weight(IP): 20 /// /// # Example /// diff --git a/src/trade/get_allocations.rs b/src/trade/get_allocations.rs new file mode 100644 index 0000000..9b2c47b --- /dev/null +++ b/src/trade/get_allocations.rs @@ -0,0 +1,153 @@ +use crate::http::{request::Request, Credentials, Method}; + +/// `GET /api/v3/myAllocations` +/// +/// Retrieves allocations resulting from SOR order placement. +/// +/// * The time between startTime and endTime can't be longer than 24 hours. +/// +/// Weight(IP): 20 +/// +/// # Example +/// +/// ``` +/// use binance_spot_connector_rust::trade; +/// +/// let request = trade::get_allocations("BNBUSDT").limit(500); +/// ``` +pub struct GetAllocations { + symbol: String, + start_time: Option, + end_time: Option, + from_allocation_id: Option, + limit: Option, + order_id: Option, + recv_window: Option, + credentials: Option, +} + +impl GetAllocations { + pub fn new(symbol: &str) -> Self { + Self { + symbol: symbol.to_owned(), + start_time: None, + end_time: None, + from_allocation_id: None, + limit: None, + order_id: None, + recv_window: None, + credentials: None, + } + } + + pub fn start_time(mut self, start_time: u64) -> Self { + self.start_time = Some(start_time); + self + } + + pub fn end_time(mut self, end_time: u64) -> Self { + self.end_time = Some(end_time); + self + } + + pub fn from_allocation_id(mut self, from_allocation_id: u64) -> Self { + self.from_allocation_id = Some(from_allocation_id); + self + } + + pub fn limit(mut self, limit: u32) -> Self { + self.limit = Some(limit); + self + } + + pub fn order_id(mut self, order_id: u64) -> Self { + self.order_id = Some(order_id); + self + } + + pub fn recv_window(mut self, recv_window: u64) -> Self { + self.recv_window = Some(recv_window); + self + } + + pub fn credentials(mut self, credentials: &Credentials) -> Self { + self.credentials = Some(credentials.clone()); + self + } +} + +impl From for Request { + fn from(request: GetAllocations) -> Request { + let mut params = vec![("symbol".to_owned(), request.symbol.to_string())]; + + if let Some(start_time) = request.start_time { + params.push(("startTime".to_owned(), start_time.to_string())); + } + + if let Some(end_time) = request.end_time { + params.push(("endTime".to_owned(), end_time.to_string())); + } + + if let Some(from_allocation_id) = request.from_allocation_id { + params.push(( + "fromAllocationId".to_owned(), + from_allocation_id.to_string(), + )); + } + + if let Some(limit) = request.limit { + params.push(("limit".to_owned(), limit.to_string())); + } + + if let Some(order_id) = request.order_id { + params.push(("orderId".to_owned(), order_id.to_string())); + } + + if let Some(recv_window) = request.recv_window { + params.push(("recvWindow".to_owned(), recv_window.to_string())); + } + + Request { + path: "/api/v3/myAllocations".to_owned(), + method: Method::Get, + params, + credentials: request.credentials, + sign: true, + } + } +} + +#[cfg(test)] +mod tests { + use super::GetAllocations; + use crate::http::{request::Request, Credentials, Method}; + + static API_KEY: &str = "api-key"; + static API_SECRET: &str = "api-secret"; + + #[test] + fn trade_get_allocations_convert_to_request_test() { + let credentials = Credentials::from_hmac(API_KEY.to_owned(), API_SECRET.to_owned()); + + let request: Request = GetAllocations::new("BNBUSDT") + .limit(500) + .recv_window(5000) + .credentials(&credentials) + .into(); + + assert_eq!( + request, + Request { + path: "/api/v3/myAllocations".to_owned(), + credentials: Some(credentials), + method: Method::Get, + params: vec![ + ("symbol".to_owned(), "BNBUSDT".to_string()), + ("limit".to_owned(), "500".to_string()), + ("recvWindow".to_owned(), "5000".to_string()), + ], + sign: true + } + ); + } +} diff --git a/src/trade/get_commission_rates.rs b/src/trade/get_commission_rates.rs new file mode 100644 index 0000000..1133a48 --- /dev/null +++ b/src/trade/get_commission_rates.rs @@ -0,0 +1,76 @@ +use crate::http::{request::Request, Credentials, Method}; + +/// `GET /api/v3/account/commission` +/// +/// Get current account commission rates. +/// +/// Weight(IP): 20 +/// +/// # Example +/// +/// ``` +/// use binance_spot_connector_rust::trade; +/// +/// let request = trade::get_commission_rates("BNBUSDT"); +/// ``` +pub struct GetCommissionRates { + symbol: String, + credentials: Option, +} + +impl GetCommissionRates { + pub fn new(symbol: &str) -> Self { + Self { + symbol: symbol.to_owned(), + credentials: None, + } + } + + pub fn credentials(mut self, credentials: &Credentials) -> Self { + self.credentials = Some(credentials.clone()); + self + } +} + +impl From for Request { + fn from(request: GetCommissionRates) -> Request { + let params = vec![("symbol".to_owned(), request.symbol.to_string())]; + + Request { + path: "/api/v3/account/commission".to_owned(), + method: Method::Get, + params, + credentials: request.credentials, + sign: true, + } + } +} + +#[cfg(test)] +mod tests { + use super::GetCommissionRates; + use crate::http::{request::Request, Credentials, Method}; + + static API_KEY: &str = "api-key"; + static API_SECRET: &str = "api-secret"; + + #[test] + fn trade_get_commission_rates_convert_to_request_test() { + let credentials = Credentials::from_hmac(API_KEY.to_owned(), API_SECRET.to_owned()); + + let request: Request = GetCommissionRates::new("BNBUSDT") + .credentials(&credentials) + .into(); + + assert_eq!( + request, + Request { + path: "/api/v3/account/commission".to_owned(), + credentials: Some(credentials), + method: Method::Get, + params: vec![("symbol".to_owned(), "BNBUSDT".to_string())], + sign: true + } + ); + } +} diff --git a/src/trade/get_oco_order.rs b/src/trade/get_oco_order.rs index d5ba82a..22a9e2b 100644 --- a/src/trade/get_oco_order.rs +++ b/src/trade/get_oco_order.rs @@ -4,7 +4,7 @@ use crate::http::{request::Request, Credentials, Method}; /// /// Retrieves a specific OCO based on provided optional parameters /// -/// Weight(IP): 2 +/// Weight(IP): 4 /// /// # Example /// diff --git a/src/trade/get_oco_orders.rs b/src/trade/get_oco_orders.rs index 5bd2c1e..6dbdac0 100644 --- a/src/trade/get_oco_orders.rs +++ b/src/trade/get_oco_orders.rs @@ -4,7 +4,7 @@ use crate::http::{request::Request, Credentials, Method}; /// /// Retrieves all OCO based on provided optional parameters /// -/// Weight(IP): 10 +/// Weight(IP): 20 /// /// # Example /// diff --git a/src/trade/get_open_oco_orders.rs b/src/trade/get_open_oco_orders.rs index f90d143..5e177d6 100644 --- a/src/trade/get_open_oco_orders.rs +++ b/src/trade/get_open_oco_orders.rs @@ -2,7 +2,7 @@ use crate::http::{request::Request, Credentials, Method}; /// `GET /api/v3/openOrderList` /// -/// Weight(IP): 3 +/// Weight(IP): 6 /// /// # Example /// diff --git a/src/trade/get_order.rs b/src/trade/get_order.rs index 2dd8e23..c056b6a 100644 --- a/src/trade/get_order.rs +++ b/src/trade/get_order.rs @@ -7,7 +7,7 @@ use crate::http::{request::Request, Credentials, Method}; /// * Either `orderId` or `origClientOrderId` must be sent. /// * For some historical orders `cummulativeQuoteQty` will be < 0, meaning the data is not available at this time. /// -/// Weight(IP): 2 +/// Weight(IP): 4 /// /// # Example /// diff --git a/src/trade/get_prevented_matches.rs b/src/trade/get_prevented_matches.rs new file mode 100644 index 0000000..87a81ba --- /dev/null +++ b/src/trade/get_prevented_matches.rs @@ -0,0 +1,150 @@ +use crate::http::{request::Request, Credentials, Method}; + +/// `GET /api/v3/myPreventedMatches` +/// +/// Displays the list of orders that were expired due to STP. +/// +/// These are the combinations supported: +/// +/// Weight(IP): +/// * If `symbol` is invalid: `2`; +/// * Querying by `preventedMatchId`: `2`; +/// * Querying by `orderId`: `20`; +/// +/// # Example +/// +/// ``` +/// use binance_spot_connector_rust::trade; +/// +/// let request = trade::get_prevented_matches("BNBUSDT"); +/// ``` +pub struct GetPreventedMatches { + symbol: String, + prevented_match_id: Option, + order_id: Option, + from_prevented_match_id: Option, + limit: Option, + recv_window: Option, + credentials: Option, +} + +impl GetPreventedMatches { + pub fn new(symbol: &str) -> Self { + Self { + symbol: symbol.to_owned(), + prevented_match_id: None, + order_id: None, + from_prevented_match_id: None, + limit: None, + recv_window: None, + credentials: None, + } + } + + pub fn prevented_match_id(mut self, prevented_match_id: u64) -> Self { + self.prevented_match_id = Some(prevented_match_id); + self + } + + pub fn order_id(mut self, order_id: u64) -> Self { + self.order_id = Some(order_id); + self + } + + pub fn from_prevented_match_id(mut self, from_prevented_match_id: u64) -> Self { + self.from_prevented_match_id = Some(from_prevented_match_id); + self + } + + pub fn limit(mut self, limit: u32) -> Self { + self.limit = Some(limit); + self + } + + pub fn recv_window(mut self, recv_window: u64) -> Self { + self.recv_window = Some(recv_window); + self + } + + pub fn credentials(mut self, credentials: &Credentials) -> Self { + self.credentials = Some(credentials.clone()); + self + } +} + +impl From for Request { + fn from(request: GetPreventedMatches) -> Request { + let mut params = vec![]; + + params.push(("symbol".to_owned(), request.symbol)); + + if let Some(prevented_match_id) = request.prevented_match_id { + params.push(( + "preventedMatchId".to_owned(), + prevented_match_id.to_string(), + )); + } + + if let Some(order_id) = request.order_id { + params.push(("orderId".to_owned(), order_id.to_string())); + } + + if let Some(from_prevented_match_id) = request.from_prevented_match_id { + params.push(( + "fromPreventedMatchId".to_owned(), + from_prevented_match_id.to_string(), + )); + } + + if let Some(limit) = request.limit { + params.push(("limit".to_owned(), limit.to_string())); + } + + if let Some(recv_window) = request.recv_window { + params.push(("recvWindow".to_owned(), recv_window.to_string())); + } + + Request { + path: "/api/v3/myPreventedMatches".to_owned(), + method: Method::Get, + params, + credentials: request.credentials, + sign: true, + } + } +} + +#[cfg(test)] +mod tests { + use super::GetPreventedMatches; + use crate::http::{request::Request, Credentials, Method}; + + static API_KEY: &str = "api-key"; + static API_SECRET: &str = "api-secret"; + + #[test] + fn trade_get_prevented_matches_convert_to_request_test() { + let credentials = Credentials::from_hmac(API_KEY.to_owned(), API_SECRET.to_owned()); + + let request: Request = GetPreventedMatches::new("BNBUSDT") + .order_id(11) + .recv_window(5000) + .credentials(&credentials) + .into(); + + assert_eq!( + request, + Request { + path: "/api/v3/myPreventedMatches".to_owned(), + credentials: Some(credentials), + method: Method::Get, + params: vec![ + ("symbol".to_owned(), "BNBUSDT".to_string()), + ("orderId".to_owned(), "11".to_string()), + ("recvWindow".to_owned(), "5000".to_string()), + ], + sign: true + } + ); + } +} diff --git a/src/trade/mod.rs b/src/trade/mod.rs index 986f142..098915c 100644 --- a/src/trade/mod.rs +++ b/src/trade/mod.rs @@ -7,14 +7,19 @@ pub mod cancel_an_existing_order_and_send_a_new_order; pub mod cancel_oco_order; pub mod cancel_open_orders; pub mod cancel_order; +pub mod get_allocations; +pub mod get_commission_rates; pub mod get_oco_order; pub mod get_oco_orders; pub mod get_open_oco_orders; pub mod get_order; +pub mod get_prevented_matches; pub mod my_trades; pub mod new_oco_order; pub mod new_order; pub mod new_order_test; +pub mod new_oto_order; +pub mod new_otoco_order; pub mod open_orders; pub mod order; pub mod order_limit_usage; @@ -27,16 +32,21 @@ use cancel_an_existing_order_and_send_a_new_order::CancelAnExistingOrderAndSendA use cancel_oco_order::CancelOCOOrder; use cancel_open_orders::CancelOpenOrders; use cancel_order::CancelOrder; +use get_allocations::GetAllocations; +use get_commission_rates::GetCommissionRates; use get_oco_order::GetOCOOrder; use get_oco_orders::GetOCOOrders; use get_open_oco_orders::GetOpenOCOOrders; use get_order::GetOrder; +use get_prevented_matches::GetPreventedMatches; use my_trades::MyTrades; use new_oco_order::NewOCOOrder; use new_order::NewOrder; use new_order_test::NewOrderTest; +use new_oto_order::NewOTOOrder; +use new_otoco_order::NewOTOCOOrder; use open_orders::OpenOrders; -use order::{CancelReplaceMode, Side}; +use order::{CancelReplaceMode, Side, WorkingMandatoryParams}; use order_limit_usage::OrderLimitUsage; pub fn new_order_test(symbol: &str, side: Side, r#type: &str) -> NewOrderTest { @@ -80,10 +90,50 @@ pub fn new_oco_order( symbol: &str, side: Side, quantity: Decimal, - price: Decimal, - stop_price: Decimal, + above_type: &str, + below_type: &str, ) -> NewOCOOrder { - NewOCOOrder::new(symbol, side, quantity, price, stop_price) + NewOCOOrder::new(symbol, side, quantity, above_type, below_type) +} + +pub fn new_oto_order( + symbol: &str, + working_mandatory_params: WorkingMandatoryParams, + pending_type: &str, + pending_side: Side, + pending_quantity: Decimal, +) -> NewOTOOrder { + NewOTOOrder::new( + symbol, + working_mandatory_params, + pending_type, + pending_side, + pending_quantity, + ) +} + +pub fn new_otoco_order( + symbol: &str, + working_mandatory_params: WorkingMandatoryParams, + pending_side: Side, + pending_quantity: Decimal, + pending_above_type: &str, +) -> NewOTOCOOrder { + NewOTOCOOrder::new( + symbol, + working_mandatory_params, + pending_side, + pending_quantity, + pending_above_type, + ) +} + +pub fn get_allocations(symbol: &str) -> GetAllocations { + GetAllocations::new(symbol) +} + +pub fn get_commission_rates(symbol: &str) -> GetCommissionRates { + GetCommissionRates::new(symbol) } pub fn get_oco_order() -> GetOCOOrder { @@ -102,6 +152,10 @@ pub fn get_open_oco_orders() -> GetOpenOCOOrders { GetOpenOCOOrders::new() } +pub fn get_prevented_matches(symbol: &str) -> GetPreventedMatches { + GetPreventedMatches::new(symbol) +} + pub fn account() -> Account { Account::new() } diff --git a/src/trade/my_trades.rs b/src/trade/my_trades.rs index b6663ec..2ba3adf 100644 --- a/src/trade/my_trades.rs +++ b/src/trade/my_trades.rs @@ -4,9 +4,19 @@ use crate::http::{request::Request, Credentials, Method}; /// /// Get trades for a specific account and symbol. /// -/// If `fromId` is set, it will get id >= that `fromId`. Otherwise most recent orders are returned. +/// * If `fromId` is set, it will get id >= that `fromId`. Otherwise most recent orders are returned. +/// * The time between `startTime` and `endTime` can't be longer than 24 hours. +/// * These are the supported combinations of all parameters: +/// * `symbol` +/// * `symbol` + `orderId` +/// * `symbol` + `startTime` +/// * `symbol` + `endTime` +/// * `symbol` + `fromId` +/// * `symbol` + `startTime` + `endTime` +/// * `symbol` + `orderId` + `fromId` /// -/// Weight(IP): 10 +/// +/// Weight(IP): 20 /// /// # Example /// diff --git a/src/trade/new_oco_order.rs b/src/trade/new_oco_order.rs index 565d03e..86dc25f 100644 --- a/src/trade/new_oco_order.rs +++ b/src/trade/new_oco_order.rs @@ -2,18 +2,16 @@ use crate::http::{request::Request, Credentials, Method}; use crate::trade::order::{NewOrderResponseType, Side, TimeInForce}; use rust_decimal::Decimal; -/// `POST /api/v3/order/oco` +/// `POST /api/v3/orderList/oco` /// -/// Send in a new OCO +/// Send in an one-cancels-the-other (OCO) pair, where activation of one order immediately cancels the other. /// -/// * Price Restrictions: -/// - `SELL`: Limit Price > Last Price > Stop Price -/// - `BUY`: Limit Price < Last Price < Stop Price -/// * Quantity Restrictions: -/// - Both legs must have the same quantity -/// - `ICEBERG` quantities however do not have to be the same -/// * Order Rate Limit -/// - `OCO` counts as 2 orders against the order rate limit. +/// * An OCO has 2 orders called the above order and below order. +/// * One of the orders must be a `LIMIT_MAKER` order and the other must be `STOP_LOSS` or `STOP_LOSS_LIMIT` order. +/// * Price restrictions: +/// - If the OCO is on the `SELL` side: `LIMIT_MAKER`, `price` > Last Traded Price > `stopPrice` +/// - If the OCO is on the `BUY` side: `LIMIT_MAKER`, `price` < Last Traded Price < `stopPrice` +/// * OCOs add 2 orders to the unfilled order count, `EXCHANGE_MAX_ORDERS` filter, and the `MAX_NUM_ORDERS` filter. /// /// Weight(IP): 1 /// @@ -23,23 +21,33 @@ use rust_decimal::Decimal; /// use binance_spot_connector_rust::trade::{self, order::{Side, TimeInForce}}; /// use rust_decimal_macros::dec; /// -/// let request = trade::new_oco_order("BNBUSDT", Side::Sell, dec!(0.1), dec!(400.15), dec!(390.3)).stop_limit_price(dec!(380.3)).stop_limit_time_in_force(TimeInForce::Gtc); +/// let request = trade::new_oco_order("BNBUSDT", Side::Sell, dec!(1.0), "LIMIT_MAKER", "STOP_LOSS_LIMIT").above_price(dec!(595.1)).below_price(dec!(585.3)).below_stop_price(dec!(583.2)).below_trailing_delta(dec!(60.0)).below_time_in_force(TimeInForce::Gtc); /// ``` pub struct NewOCOOrder { symbol: String, side: Side, quantity: Decimal, - price: Decimal, - stop_price: Decimal, + above_type: String, + below_type: String, list_client_order_id: Option, - limit_client_order_id: Option, - limit_iceberg_qty: Option, - trailing_delta: Option, - stop_client_order_id: Option, - stop_limit_price: Option, - stop_iceberg_qty: Option, - stop_limit_time_in_force: Option, + above_client_order_id: Option, + above_iceberg_qty: Option, + above_price: Option, + above_stop_price: Option, + above_trailing_delta: Option, + above_time_in_force: Option, + above_strategy_id: Option, + above_strategy_type: Option, + below_client_order_id: Option, + below_iceberg_qty: Option, + below_price: Option, + below_stop_price: Option, + below_trailing_delta: Option, + below_time_in_force: Option, + below_strategy_id: Option, + below_strategy_type: Option, new_order_resp_type: Option, + self_trade_prevention: Option, recv_window: Option, credentials: Option, } @@ -49,24 +57,34 @@ impl NewOCOOrder { symbol: &str, side: Side, quantity: Decimal, - price: Decimal, - stop_price: Decimal, + above_type: &str, + below_type: &str, ) -> Self { Self { symbol: symbol.to_owned(), side, quantity, - price, - stop_price, + above_type: above_type.to_owned(), + below_type: below_type.to_owned(), list_client_order_id: None, - limit_client_order_id: None, - limit_iceberg_qty: None, - trailing_delta: None, - stop_client_order_id: None, - stop_limit_price: None, - stop_iceberg_qty: None, - stop_limit_time_in_force: None, + above_client_order_id: None, + above_iceberg_qty: None, + above_price: None, + above_stop_price: None, + above_trailing_delta: None, + above_time_in_force: None, + above_strategy_id: None, + above_strategy_type: None, + below_client_order_id: None, + below_iceberg_qty: None, + below_price: None, + below_stop_price: None, + below_trailing_delta: None, + below_time_in_force: None, + below_strategy_id: None, + below_strategy_type: None, new_order_resp_type: None, + self_trade_prevention: None, recv_window: None, credentials: None, } @@ -77,38 +95,83 @@ impl NewOCOOrder { self } - pub fn limit_client_order_id(mut self, limit_client_order_id: &str) -> Self { - self.limit_client_order_id = Some(limit_client_order_id.to_owned()); + pub fn above_client_order_id(mut self, above_client_order_id: &str) -> Self { + self.above_client_order_id = Some(above_client_order_id.to_owned()); self } - pub fn limit_iceberg_qty(mut self, limit_iceberg_qty: Decimal) -> Self { - self.limit_iceberg_qty = Some(limit_iceberg_qty); + pub fn above_iceberg_qty(mut self, above_iceberg_qty: Decimal) -> Self { + self.above_iceberg_qty = Some(above_iceberg_qty); self } - pub fn trailing_delta(mut self, trailing_delta: Decimal) -> Self { - self.trailing_delta = Some(trailing_delta); + pub fn above_price(mut self, above_price: Decimal) -> Self { + self.above_price = Some(above_price); self } - pub fn stop_client_order_id(mut self, stop_client_order_id: &str) -> Self { - self.stop_client_order_id = Some(stop_client_order_id.to_owned()); + pub fn above_stop_price(mut self, above_stop_price: Decimal) -> Self { + self.above_stop_price = Some(above_stop_price); self } - pub fn stop_limit_price(mut self, stop_limit_price: Decimal) -> Self { - self.stop_limit_price = Some(stop_limit_price); + pub fn above_trailing_delta(mut self, above_trailing_delta: Decimal) -> Self { + self.above_trailing_delta = Some(above_trailing_delta); self } - pub fn stop_iceberg_qty(mut self, stop_iceberg_qty: Decimal) -> Self { - self.stop_iceberg_qty = Some(stop_iceberg_qty); + pub fn above_time_in_force(mut self, above_time_in_force: TimeInForce) -> Self { + self.above_time_in_force = Some(above_time_in_force); self } - pub fn stop_limit_time_in_force(mut self, stop_limit_time_in_force: TimeInForce) -> Self { - self.stop_limit_time_in_force = Some(stop_limit_time_in_force); + pub fn above_strategy_id(mut self, above_strategy_id: u64) -> Self { + self.above_strategy_id = Some(above_strategy_id); + self + } + + pub fn above_strategy_type(mut self, above_strategy_type: u64) -> Self { + self.above_strategy_type = Some(above_strategy_type); + self + } + + pub fn below_client_order_id(mut self, below_client_order_id: &str) -> Self { + self.below_client_order_id = Some(below_client_order_id.to_owned()); + self + } + + pub fn below_iceberg_qty(mut self, below_iceberg_qty: Decimal) -> Self { + self.below_iceberg_qty = Some(below_iceberg_qty); + self + } + + pub fn below_price(mut self, below_price: Decimal) -> Self { + self.below_price = Some(below_price); + self + } + + pub fn below_stop_price(mut self, below_stop_price: Decimal) -> Self { + self.below_stop_price = Some(below_stop_price); + self + } + + pub fn below_trailing_delta(mut self, below_trailing_delta: Decimal) -> Self { + self.below_trailing_delta = Some(below_trailing_delta); + self + } + + pub fn below_time_in_force(mut self, below_time_in_force: TimeInForce) -> Self { + self.below_time_in_force = Some(below_time_in_force); + self + } + + pub fn below_strategy_id(mut self, below_strategy_id: u64) -> Self { + self.below_strategy_id = Some(below_strategy_id); + self + } + + pub fn below_strategy_type(mut self, below_strategy_type: u64) -> Self { + self.below_strategy_type = Some(below_strategy_type); self } @@ -117,6 +180,11 @@ impl NewOCOOrder { self } + pub fn self_trade_prevention(mut self, self_trade_prevention: &str) -> Self { + self.self_trade_prevention = Some(self_trade_prevention.to_owned()); + self + } + pub fn recv_window(mut self, recv_window: u64) -> Self { self.recv_window = Some(recv_window); self @@ -134,42 +202,93 @@ impl From for Request { ("symbol".to_owned(), request.symbol.to_string()), ("side".to_owned(), request.side.to_string()), ("quantity".to_owned(), request.quantity.to_string()), - ("price".to_owned(), request.price.to_string()), - ("stopPrice".to_owned(), request.stop_price.to_string()), + ("aboveType".to_owned(), request.above_type.to_string()), + ("belowType".to_owned(), request.below_type.to_string()), ]; if let Some(list_client_order_id) = request.list_client_order_id { params.push(("listClientOrderId".to_owned(), list_client_order_id)); } - if let Some(limit_client_order_id) = request.limit_client_order_id { - params.push(("limitClientOrderId".to_owned(), limit_client_order_id)); + if let Some(above_client_order_id) = request.above_client_order_id { + params.push(("aboveClientOrderId".to_owned(), above_client_order_id)); } - if let Some(limit_iceberg_qty) = request.limit_iceberg_qty { - params.push(("limitIcebergQty".to_owned(), limit_iceberg_qty.to_string())); + if let Some(above_iceberg_qty) = request.above_iceberg_qty { + params.push(("aboveIcebergQty".to_owned(), above_iceberg_qty.to_string())); } - if let Some(trailing_delta) = request.trailing_delta { - params.push(("trailingDelta".to_owned(), trailing_delta.to_string())); + if let Some(above_price) = request.above_price { + params.push(("abovePrice".to_owned(), above_price.to_string())); } - if let Some(stop_client_order_id) = request.stop_client_order_id { - params.push(("stopClientOrderId".to_owned(), stop_client_order_id)); + if let Some(above_stop_price) = request.above_stop_price { + params.push(("aboveStopPrice".to_owned(), above_stop_price.to_string())); } - if let Some(stop_limit_price) = request.stop_limit_price { - params.push(("stopLimitPrice".to_owned(), stop_limit_price.to_string())); + if let Some(above_trailing_delta) = request.above_trailing_delta { + params.push(( + "aboveTrailingDelta".to_owned(), + above_trailing_delta.to_string(), + )); + } + + if let Some(above_time_in_force) = request.above_time_in_force { + params.push(( + "aboveTimeInForce".to_owned(), + above_time_in_force.to_string(), + )); + } + + if let Some(above_strategy_id) = request.above_strategy_id { + params.push(("aboveStrategyId".to_owned(), above_strategy_id.to_string())); + } + + if let Some(above_strategy_type) = request.above_strategy_type { + params.push(( + "aboveStrategyType".to_owned(), + above_strategy_type.to_string(), + )); + } + + if let Some(below_client_order_id) = request.below_client_order_id { + params.push(("belowClientOrderId".to_owned(), below_client_order_id)); + } + + if let Some(below_iceberg_qty) = request.below_iceberg_qty { + params.push(("belowIcebergQty".to_owned(), below_iceberg_qty.to_string())); + } + + if let Some(below_price) = request.below_price { + params.push(("belowPrice".to_owned(), below_price.to_string())); + } + + if let Some(below_stop_price) = request.below_stop_price { + params.push(("belowStopPrice".to_owned(), below_stop_price.to_string())); } - if let Some(stop_iceberg_qty) = request.stop_iceberg_qty { - params.push(("stopIcebergQty".to_owned(), stop_iceberg_qty.to_string())); + if let Some(below_trailing_delta) = request.below_trailing_delta { + params.push(( + "belowTrailingDelta".to_owned(), + below_trailing_delta.to_string(), + )); } - if let Some(stop_limit_time_in_force) = request.stop_limit_time_in_force { + if let Some(below_time_in_force) = request.below_time_in_force { params.push(( - "stopLimitTimeInForce".to_owned(), - stop_limit_time_in_force.to_string(), + "belowTimeInForce".to_owned(), + below_time_in_force.to_string(), + )); + } + + if let Some(below_strategy_id) = request.below_strategy_id { + params.push(("belowStrategyId".to_owned(), below_strategy_id.to_string())); + } + + if let Some(below_strategy_type) = request.below_strategy_type { + params.push(( + "belowStrategyType".to_owned(), + below_strategy_type.to_string(), )); } @@ -180,12 +299,16 @@ impl From for Request { )); } + if let Some(self_trade_prevention) = request.self_trade_prevention { + params.push(("selfTradePrevention".to_owned(), self_trade_prevention)); + } + if let Some(recv_window) = request.recv_window { params.push(("recvWindow".to_owned(), recv_window.to_string())); } Request { - path: "/api/v3/order/oco".to_owned(), + path: "/api/v3/orderList/oco".to_owned(), method: Method::Post, params, credentials: request.credentials, @@ -208,30 +331,38 @@ mod tests { fn trade_new_oco_order_convert_to_request_test() { let credentials = Credentials::from_hmac(API_KEY.to_owned(), API_SECRET.to_owned()); - let request: Request = - NewOCOOrder::new("BNBUSDT", Side::Sell, dec!(0.1), dec!(400.15), dec!(390.3)) - .stop_limit_price(dec!(380.3)) - .stop_limit_time_in_force(TimeInForce::Gtc) - .credentials(&credentials) - .recv_window(5000) - .credentials(&credentials) - .into(); + let request: Request = NewOCOOrder::new( + "BNBUSDT", + Side::Sell, + dec!(1.0), + "LIMIT_MAKER", + "STOP_LOSS_LIMIT", + ) + .above_price(dec!(595.1)) + .below_price(dec!(585.3)) + .below_stop_price(dec!(583.2)) + .below_trailing_delta(dec!(60.0)) + .below_time_in_force(TimeInForce::Gtc) + .credentials(&credentials) + .into(); assert_eq!( request, Request { - path: "/api/v3/order/oco".to_owned(), + path: "/api/v3/orderList/oco".to_owned(), credentials: Some(credentials), method: Method::Post, params: vec![ ("symbol".to_owned(), "BNBUSDT".to_string()), ("side".to_owned(), "SELL".to_string()), - ("quantity".to_owned(), "0.1".to_string()), - ("price".to_owned(), "400.15".to_string()), - ("stopPrice".to_owned(), "390.3".to_string()), - ("stopLimitPrice".to_owned(), "380.3".to_string()), - ("stopLimitTimeInForce".to_owned(), "GTC".to_string()), - ("recvWindow".to_owned(), "5000".to_string()), + ("quantity".to_owned(), "1.0".to_string()), + ("aboveType".to_owned(), "LIMIT_MAKER".to_string()), + ("belowType".to_owned(), "STOP_LOSS_LIMIT".to_string()), + ("abovePrice".to_owned(), "595.1".to_string()), + ("belowPrice".to_owned(), "585.3".to_string()), + ("belowStopPrice".to_owned(), "583.2".to_string()), + ("belowTrailingDelta".to_owned(), "60.0".to_string()), + ("belowTimeInForce".to_owned(), "GTC".to_string()), ], sign: true } diff --git a/src/trade/new_order_test.rs b/src/trade/new_order_test.rs index 683adc4..f0d95a4 100644 --- a/src/trade/new_order_test.rs +++ b/src/trade/new_order_test.rs @@ -31,6 +31,7 @@ pub struct NewOrderTest { iceberg_qty: Option, new_order_resp_type: Option, recv_window: Option, + compute_commission_rates: Option, credentials: Option, } @@ -50,6 +51,7 @@ impl NewOrderTest { iceberg_qty: None, new_order_resp_type: None, recv_window: None, + compute_commission_rates: None, credentials: None, } } @@ -104,6 +106,11 @@ impl NewOrderTest { self } + pub fn compute_commission_rates(mut self, compute_commission_rates: bool) -> Self { + self.compute_commission_rates = Some(compute_commission_rates); + self + } + pub fn credentials(mut self, credentials: &Credentials) -> Self { self.credentials = Some(credentials.clone()); self @@ -161,6 +168,13 @@ impl From for Request { params.push(("recvWindow".to_owned(), recv_window.to_string())); } + if let Some(compute_commission_rates) = request.compute_commission_rates { + params.push(( + "computeCommissionRates".to_owned(), + compute_commission_rates.to_string(), + )); + } + Request { path: "/api/v3/order/test".to_owned(), method: Method::Post, diff --git a/src/trade/new_oto_order.rs b/src/trade/new_oto_order.rs new file mode 100644 index 0000000..a0c5361 --- /dev/null +++ b/src/trade/new_oto_order.rs @@ -0,0 +1,368 @@ +use crate::http::{request::Request, Credentials, Method}; +use crate::trade::order::{NewOrderResponseType, Side, TimeInForce, WorkingMandatoryParams}; +use rust_decimal::Decimal; + +/// `POST /api/v3/orderList/oto` +/// +/// Places an OTO. +/// +/// * An OTO (One-Triggers-the-Other) is an order list comprised of 2 orders. +/// * The first order is called the working order and must be `LIMIT` or `LIMIT_MAKER`. Initially, only the working order goes on the order book. +/// * The second order is called the pending order. It can be any order type except for `MARKET` orders using parameter `quoteOrderQty`. The pending order is only placed on the order book when the working order gets fully filled. +/// * If either the working order or the pending order is cancelled individually, the other order in the order list will also be canceled or expired. +/// * When the order list is placed, if the working order gets immediately fully filled, the placement response will show the working order as `FILLED` but the pending order will still appear as `PENDING_NEW`. You need to query the status of the pending order again to see its updated status. +/// * OTOs add 2 orders to the unfilled order count, `EXCHANGE_MAX_NUM_ORDERS` filter and `MAX_NUM_ORDERS` filter. +/// +/// Weight(IP): 1 +/// +/// # Example +/// +/// ``` +/// use binance_spot_connector_rust::trade::{self, order::{Side, TimeInForce, WorkingMandatoryParams}}; +/// use rust_decimal_macros::dec; +/// +/// let request = trade::new_oto_order("BNBUSDT", WorkingMandatoryParams::new("LIMIT", Side::Buy, dec!(596.0), dec!(1.0)), "LIMIT_MAKER", Side::Buy, dec!(1.0)).working_time_in_force(TimeInForce::Gtc).pending_price(dec!(598.1)); +/// ``` +pub struct NewOTOOrder { + symbol: String, + working_mandatory_params: WorkingMandatoryParams, + pending_type: String, + pending_side: Side, + pending_quantity: Decimal, + list_client_order_id: Option, + new_order_resp_type: Option, + self_trade_prevention: Option, + working_client_order_id: Option, + working_iceberg_qty: Option, + working_time_in_force: Option, + working_strategy_id: Option, + working_strategy_type: Option, + pending_client_order_id: Option, + pending_price: Option, + pending_stop_price: Option, + pending_trailing_delta: Option, + pending_iceberg_qty: Option, + pending_time_in_force: Option, + pending_strategy_id: Option, + pending_strategy_type: Option, + recv_window: Option, + credentials: Option, +} + +impl NewOTOOrder { + pub fn new( + symbol: &str, + working_mandatory_params: WorkingMandatoryParams, + pending_type: &str, + pending_side: Side, + pending_quantity: Decimal, + ) -> Self { + Self { + symbol: symbol.to_owned(), + working_mandatory_params, + pending_type: pending_type.to_owned(), + pending_side, + pending_quantity, + list_client_order_id: None, + new_order_resp_type: None, + self_trade_prevention: None, + working_client_order_id: None, + working_iceberg_qty: None, + working_time_in_force: None, + working_strategy_id: None, + working_strategy_type: None, + pending_client_order_id: None, + pending_price: None, + pending_stop_price: None, + pending_trailing_delta: None, + pending_iceberg_qty: None, + pending_time_in_force: None, + pending_strategy_id: None, + pending_strategy_type: None, + recv_window: None, + credentials: None, + } + } + + pub fn list_client_order_id(mut self, list_client_order_id: &str) -> Self { + self.list_client_order_id = Some(list_client_order_id.to_owned()); + self + } + + pub fn new_order_resp_type(mut self, new_order_resp_type: NewOrderResponseType) -> Self { + self.new_order_resp_type = Some(new_order_resp_type); + self + } + + pub fn self_trade_prevention(mut self, self_trade_prevention: &str) -> Self { + self.self_trade_prevention = Some(self_trade_prevention.to_owned()); + self + } + + pub fn working_client_order_id(mut self, working_client_order_id: &str) -> Self { + self.working_client_order_id = Some(working_client_order_id.to_owned()); + self + } + + pub fn working_iceberg_qty(mut self, working_iceberg_qty: Decimal) -> Self { + self.working_iceberg_qty = Some(working_iceberg_qty); + self + } + + pub fn working_time_in_force(mut self, working_time_in_force: TimeInForce) -> Self { + self.working_time_in_force = Some(working_time_in_force); + self + } + + pub fn working_strategy_id(mut self, working_strategy_id: u64) -> Self { + self.working_strategy_id = Some(working_strategy_id); + self + } + + pub fn working_strategy_type(mut self, working_strategy_type: u64) -> Self { + self.working_strategy_type = Some(working_strategy_type); + self + } + + pub fn pending_client_order_id(mut self, pending_client_order_id: &str) -> Self { + self.pending_client_order_id = Some(pending_client_order_id.to_owned()); + self + } + + pub fn pending_price(mut self, pending_price: Decimal) -> Self { + self.pending_price = Some(pending_price); + self + } + + pub fn pending_stop_price(mut self, pending_stop_price: Decimal) -> Self { + self.pending_stop_price = Some(pending_stop_price); + self + } + + pub fn pending_trailing_delta(mut self, pending_trailing_delta: Decimal) -> Self { + self.pending_trailing_delta = Some(pending_trailing_delta); + self + } + + pub fn pending_iceberg_qty(mut self, pending_iceberg_qty: Decimal) -> Self { + self.pending_iceberg_qty = Some(pending_iceberg_qty); + self + } + + pub fn pending_time_in_force(mut self, pending_time_in_force: TimeInForce) -> Self { + self.pending_time_in_force = Some(pending_time_in_force); + self + } + + pub fn pending_strategy_id(mut self, pending_strategy_id: u64) -> Self { + self.pending_strategy_id = Some(pending_strategy_id); + self + } + + pub fn pending_strategy_type(mut self, pending_strategy_type: u64) -> Self { + self.pending_strategy_type = Some(pending_strategy_type); + self + } + + pub fn recv_window(mut self, recv_window: u64) -> Self { + self.recv_window = Some(recv_window); + self + } + + pub fn credentials(mut self, credentials: &Credentials) -> Self { + self.credentials = Some(credentials.clone()); + self + } +} + +impl From for Request { + fn from(request: NewOTOOrder) -> Request { + let mut params = vec![ + ("symbol".to_owned(), request.symbol.to_string()), + ( + "workingType".to_owned(), + request.working_mandatory_params.working_type.to_string(), + ), + ( + "workingSide".to_owned(), + request.working_mandatory_params.working_side.to_string(), + ), + ( + "workingPrice".to_owned(), + request.working_mandatory_params.working_price.to_string(), + ), + ( + "workingQuantity".to_owned(), + request + .working_mandatory_params + .working_quantity + .to_string(), + ), + ("pendingType".to_owned(), request.pending_type.to_string()), + ("pendingSide".to_owned(), request.pending_side.to_string()), + ( + "pendingQuantity".to_owned(), + request.pending_quantity.to_string(), + ), + ]; + + if let Some(list_client_order_id) = request.list_client_order_id { + params.push(("listClientOrderId".to_owned(), list_client_order_id)); + } + + if let Some(new_order_resp_type) = request.new_order_resp_type { + params.push(( + "newOrderRespType".to_owned(), + new_order_resp_type.to_string(), + )); + } + + if let Some(self_trade_prevention) = request.self_trade_prevention { + params.push(("selfTradePrevention".to_owned(), self_trade_prevention)); + } + + if let Some(working_client_order_id) = request.working_client_order_id { + params.push(("workingClientOrderId".to_owned(), working_client_order_id)); + } + + if let Some(working_iceberg_qty) = request.working_iceberg_qty { + params.push(( + "workingIcebergQty".to_owned(), + working_iceberg_qty.to_string(), + )); + } + + if let Some(working_time_in_force) = request.working_time_in_force { + params.push(( + "workingTimeInForce".to_owned(), + working_time_in_force.to_string(), + )); + } + + if let Some(working_strategy_id) = request.working_strategy_id { + params.push(( + "workingStrategyId".to_owned(), + working_strategy_id.to_string(), + )); + } + + if let Some(working_strategy_type) = request.working_strategy_type { + params.push(( + "workingStrategyType".to_owned(), + working_strategy_type.to_string(), + )); + } + + if let Some(pending_client_order_id) = request.pending_client_order_id { + params.push(("pendingClientOrderId".to_owned(), pending_client_order_id)); + } + + if let Some(pending_price) = request.pending_price { + params.push(("pendingPrice".to_owned(), pending_price.to_string())); + } + + if let Some(pending_stop_price) = request.pending_stop_price { + params.push(( + "pendingStopPrice".to_owned(), + pending_stop_price.to_string(), + )); + } + + if let Some(pending_trailing_delta) = request.pending_trailing_delta { + params.push(( + "pendingTrailingDelta".to_owned(), + pending_trailing_delta.to_string(), + )); + } + + if let Some(pending_iceberg_qty) = request.pending_iceberg_qty { + params.push(( + "pendingIcebergQty".to_owned(), + pending_iceberg_qty.to_string(), + )); + } + + if let Some(pending_time_in_force) = request.pending_time_in_force { + params.push(( + "pendingTimeInForce".to_owned(), + pending_time_in_force.to_string(), + )); + } + + if let Some(pending_strategy_id) = request.pending_strategy_id { + params.push(( + "pendingStrategyId".to_owned(), + pending_strategy_id.to_string(), + )); + } + + if let Some(pending_strategy_type) = request.pending_strategy_type { + params.push(( + "pendingStrategyType".to_owned(), + pending_strategy_type.to_string(), + )); + } + + if let Some(recv_window) = request.recv_window { + params.push(("recvWindow".to_owned(), recv_window.to_string())); + } + + Request { + path: "/api/v3/orderList/oto".to_owned(), + method: Method::Post, + params, + credentials: request.credentials, + sign: true, + } + } +} + +#[cfg(test)] +mod tests { + use super::NewOTOOrder; + use crate::http::{request::Request, Credentials, Method}; + use crate::trade::order::{Side, TimeInForce, WorkingMandatoryParams}; + use rust_decimal_macros::dec; + + static API_KEY: &str = "api-key"; + static API_SECRET: &str = "api-secret"; + + #[test] + fn trade_new_oto_order_convert_to_request_test() { + let credentials = Credentials::from_hmac(API_KEY.to_owned(), API_SECRET.to_owned()); + + let request: Request = NewOTOOrder::new( + "BNBUSDT", + WorkingMandatoryParams::new("LIMIT", Side::Buy, dec!(596.0), dec!(1.0)), + "LIMIT_MAKER", + Side::Buy, + dec!(1.0), + ) + .working_time_in_force(TimeInForce::Gtc) + .pending_price(dec!(598.1)) + .credentials(&credentials) + .into(); + + assert_eq!( + request, + Request { + path: "/api/v3/orderList/oto".to_owned(), + credentials: Some(credentials), + method: Method::Post, + params: vec![ + ("symbol".to_owned(), "BNBUSDT".to_string()), + ("workingType".to_owned(), "LIMIT".to_string()), + ("workingSide".to_owned(), "BUY".to_string()), + ("workingPrice".to_owned(), "596.0".to_string()), + ("workingQuantity".to_owned(), "1.0".to_string()), + ("pendingType".to_owned(), "LIMIT_MAKER".to_string()), + ("pendingSide".to_owned(), "BUY".to_string()), + ("pendingQuantity".to_owned(), "1.0".to_string()), + ("workingTimeInForce".to_owned(), "GTC".to_string()), + ("pendingPrice".to_owned(), "598.1".to_string()), + ], + sign: true + } + ); + } +} diff --git a/src/trade/new_otoco_order.rs b/src/trade/new_otoco_order.rs new file mode 100644 index 0000000..41b4211 --- /dev/null +++ b/src/trade/new_otoco_order.rs @@ -0,0 +1,510 @@ +use crate::http::{request::Request, Credentials, Method}; +use crate::trade::order::{NewOrderResponseType, Side, TimeInForce, WorkingMandatoryParams}; +use rust_decimal::Decimal; + +/// `POST /api/v3/orderList/otoco` +/// +/// Place an OTOCO. +/// +/// * An OTOCO (One-Triggers-One-Cancels-the-Other) is an order list comprised of 3 orders. +/// * The first order is called the working order and must be `LIMIT` or `LIMIT_MAKER`. Initially, only the working order goes on the order book. +/// - The behavior of the working order is the same as the OTO. +/// * OTOCO has 2 pending orders (pending above and pending below), forming an OCO pair. The pending orders are only placed on the order book when the working order gets fully filled. +/// - The rules of the pending above and pending below follow the same rules as the Order List OCO. +/// * OTOCOs add 3 orders against the unfilled order count, EXCHANGE_MAX_NUM_ORDERS filter, and MAX_NUM_ORDERS filter. +/// +/// Weight(IP): 1 +/// +/// # Example +/// +/// ``` +/// use binance_spot_connector_rust::trade::{self, order::{Side, TimeInForce, WorkingMandatoryParams}}; +/// use rust_decimal_macros::dec; +/// +/// let request = trade::new_otoco_order("BNBUSDT", WorkingMandatoryParams::new("LIMIT", Side::Sell, dec!(305), dec!(0.5)), Side::Sell, dec!(0.5), "LIMIT_MAKER").working_time_in_force(TimeInForce::Gtc).pending_above_price(dec!(308)).pending_below_type("STOP_LOSS_LIMIT").pending_below_stop_price(dec!(300.5)).pending_below_trailing_delta(dec!(30)).pending_below_time_in_force(TimeInForce::Gtc).pending_below_price(dec!(301)); +/// ``` +pub struct NewOTOCOOrder { + symbol: String, + working_mandatory_params: WorkingMandatoryParams, + pending_side: Side, + pending_quantity: Decimal, + pending_above_type: String, + list_client_order_id: Option, + new_order_resp_type: Option, + self_trade_prevention: Option, + working_client_order_id: Option, + working_iceberg_qty: Option, + working_time_in_force: Option, + working_strategy_id: Option, + working_strategy_type: Option, + pending_above_client_order_id: Option, + pending_above_price: Option, + pending_above_stop_price: Option, + pending_above_trailing_delta: Option, + pending_above_iceberg_qty: Option, + pending_above_time_in_force: Option, + pending_above_strategy_id: Option, + pending_above_strategy_type: Option, + pending_below_type: Option, + pending_below_client_order_id: Option, + pending_below_price: Option, + pending_below_stop_price: Option, + pending_below_trailing_delta: Option, + pending_below_iceberg_qty: Option, + pending_below_time_in_force: Option, + pending_below_strategy_id: Option, + pending_below_strategy_type: Option, + recv_window: Option, + credentials: Option, +} + +impl NewOTOCOOrder { + pub fn new( + symbol: &str, + working_mandatory_params: WorkingMandatoryParams, + pending_side: Side, + pending_quantity: Decimal, + pending_above_type: &str, + ) -> Self { + Self { + symbol: symbol.to_owned(), + working_mandatory_params, + pending_side, + pending_quantity, + pending_above_type: pending_above_type.to_owned(), + list_client_order_id: None, + new_order_resp_type: None, + self_trade_prevention: None, + working_client_order_id: None, + working_iceberg_qty: None, + working_time_in_force: None, + working_strategy_id: None, + working_strategy_type: None, + pending_above_client_order_id: None, + pending_above_price: None, + pending_above_stop_price: None, + pending_above_trailing_delta: None, + pending_above_iceberg_qty: None, + pending_above_time_in_force: None, + pending_above_strategy_id: None, + pending_above_strategy_type: None, + pending_below_type: None, + pending_below_client_order_id: None, + pending_below_price: None, + pending_below_stop_price: None, + pending_below_trailing_delta: None, + pending_below_iceberg_qty: None, + pending_below_time_in_force: None, + pending_below_strategy_id: None, + pending_below_strategy_type: None, + recv_window: None, + credentials: None, + } + } + + pub fn list_client_order_id(mut self, list_client_order_id: &str) -> Self { + self.list_client_order_id = Some(list_client_order_id.to_owned()); + self + } + + pub fn new_order_resp_type(mut self, new_order_resp_type: NewOrderResponseType) -> Self { + self.new_order_resp_type = Some(new_order_resp_type); + self + } + + pub fn self_trade_prevention(mut self, self_trade_prevention: &str) -> Self { + self.self_trade_prevention = Some(self_trade_prevention.to_owned()); + self + } + + pub fn working_client_order_id(mut self, working_client_order_id: &str) -> Self { + self.working_client_order_id = Some(working_client_order_id.to_owned()); + self + } + + pub fn working_iceberg_qty(mut self, working_iceberg_qty: Decimal) -> Self { + self.working_iceberg_qty = Some(working_iceberg_qty); + self + } + + pub fn working_time_in_force(mut self, working_time_in_force: TimeInForce) -> Self { + self.working_time_in_force = Some(working_time_in_force); + self + } + + pub fn working_strategy_id(mut self, working_strategy_id: u64) -> Self { + self.working_strategy_id = Some(working_strategy_id); + self + } + + pub fn working_strategy_type(mut self, working_strategy_type: u64) -> Self { + self.working_strategy_type = Some(working_strategy_type); + self + } + + pub fn pending_above_client_order_id(mut self, pending_above_client_order_id: &str) -> Self { + self.pending_above_client_order_id = Some(pending_above_client_order_id.to_owned()); + self + } + + pub fn pending_above_price(mut self, pending_above_price: Decimal) -> Self { + self.pending_above_price = Some(pending_above_price); + self + } + + pub fn pending_above_stop_price(mut self, pending_above_stop_price: Decimal) -> Self { + self.pending_above_stop_price = Some(pending_above_stop_price); + self + } + + pub fn pending_above_trailing_delta(mut self, pending_above_trailing_delta: Decimal) -> Self { + self.pending_above_trailing_delta = Some(pending_above_trailing_delta); + self + } + + pub fn pending_above_iceberg_qty(mut self, pending_above_iceberg_qty: Decimal) -> Self { + self.pending_above_iceberg_qty = Some(pending_above_iceberg_qty); + self + } + + pub fn pending_above_time_in_force(mut self, pending_above_time_in_force: TimeInForce) -> Self { + self.pending_above_time_in_force = Some(pending_above_time_in_force); + self + } + + pub fn pending_above_strategy_id(mut self, pending_above_strategy_id: u64) -> Self { + self.pending_above_strategy_id = Some(pending_above_strategy_id); + self + } + + pub fn pending_above_strategy_type(mut self, pending_above_strategy_type: u64) -> Self { + self.pending_above_strategy_type = Some(pending_above_strategy_type); + self + } + + pub fn pending_below_type(mut self, pending_below_type: &str) -> Self { + self.pending_below_type = Some(pending_below_type.to_owned()); + self + } + + pub fn pending_below_client_order_id(mut self, pending_below_client_order_id: &str) -> Self { + self.pending_below_client_order_id = Some(pending_below_client_order_id.to_owned()); + self + } + + pub fn pending_below_price(mut self, pending_below_price: Decimal) -> Self { + self.pending_below_price = Some(pending_below_price); + self + } + + pub fn pending_below_stop_price(mut self, pending_below_stop_price: Decimal) -> Self { + self.pending_below_stop_price = Some(pending_below_stop_price); + self + } + + pub fn pending_below_trailing_delta(mut self, pending_below_trailing_delta: Decimal) -> Self { + self.pending_below_trailing_delta = Some(pending_below_trailing_delta); + self + } + + pub fn pending_below_iceberg_qty(mut self, pending_below_iceberg_qty: Decimal) -> Self { + self.pending_below_iceberg_qty = Some(pending_below_iceberg_qty); + self + } + + pub fn pending_below_time_in_force(mut self, pending_below_time_in_force: TimeInForce) -> Self { + self.pending_below_time_in_force = Some(pending_below_time_in_force); + self + } + + pub fn pending_below_strategy_id(mut self, pending_below_strategy_id: u64) -> Self { + self.pending_below_strategy_id = Some(pending_below_strategy_id); + self + } + + pub fn pending_below_strategy_type(mut self, pending_below_strategy_type: u64) -> Self { + self.pending_below_strategy_type = Some(pending_below_strategy_type); + self + } + + pub fn recv_window(mut self, recv_window: u64) -> Self { + self.recv_window = Some(recv_window); + self + } + + pub fn credentials(mut self, credentials: &Credentials) -> Self { + self.credentials = Some(credentials.clone()); + self + } +} + +impl From for Request { + fn from(request: NewOTOCOOrder) -> Request { + let mut params = vec![ + ("symbol".to_owned(), request.symbol.to_string()), + ( + "workingType".to_owned(), + request.working_mandatory_params.working_type.to_string(), + ), + ( + "workingSide".to_owned(), + request.working_mandatory_params.working_side.to_string(), + ), + ( + "workingPrice".to_owned(), + request.working_mandatory_params.working_price.to_string(), + ), + ( + "workingQuantity".to_owned(), + request + .working_mandatory_params + .working_quantity + .to_string(), + ), + ("pendingSide".to_owned(), request.pending_side.to_string()), + ( + "pendingQuantity".to_owned(), + request.pending_quantity.to_string(), + ), + ( + "pendingAboveType".to_owned(), + request.pending_above_type.to_string(), + ), + ]; + + if let Some(list_client_order_id) = request.list_client_order_id { + params.push(("listClientOrderId".to_owned(), list_client_order_id)); + } + + if let Some(new_order_resp_type) = request.new_order_resp_type { + params.push(( + "newOrderRespType".to_owned(), + new_order_resp_type.to_string(), + )); + } + + if let Some(self_trade_prevention) = request.self_trade_prevention { + params.push(("selfTradePrevention".to_owned(), self_trade_prevention)); + } + + if let Some(working_client_order_id) = request.working_client_order_id { + params.push(("workingClientOrderId".to_owned(), working_client_order_id)); + } + + if let Some(working_iceberg_qty) = request.working_iceberg_qty { + params.push(( + "workingIcebergQty".to_owned(), + working_iceberg_qty.to_string(), + )); + } + + if let Some(working_time_in_force) = request.working_time_in_force { + params.push(( + "workingTimeInForce".to_owned(), + working_time_in_force.to_string(), + )); + } + + if let Some(working_strategy_id) = request.working_strategy_id { + params.push(( + "workingStrategyId".to_owned(), + working_strategy_id.to_string(), + )); + } + + if let Some(working_strategy_type) = request.working_strategy_type { + params.push(( + "workingStrategyType".to_owned(), + working_strategy_type.to_string(), + )); + } + + if let Some(pending_above_client_order_id) = request.pending_above_client_order_id { + params.push(( + "pendingAboveClientOrderId".to_owned(), + pending_above_client_order_id, + )); + } + + if let Some(pending_above_price) = request.pending_above_price { + params.push(( + "pendingAbovePrice".to_owned(), + pending_above_price.to_string(), + )); + } + + if let Some(pending_above_stop_price) = request.pending_above_stop_price { + params.push(( + "pendingAboveStopPrice".to_owned(), + pending_above_stop_price.to_string(), + )); + } + + if let Some(pending_above_trailing_delta) = request.pending_above_trailing_delta { + params.push(( + "pendingAboveTrailingDelta".to_owned(), + pending_above_trailing_delta.to_string(), + )); + } + + if let Some(pending_above_iceberg_qty) = request.pending_above_iceberg_qty { + params.push(( + "pendingAboveIcebergQty".to_owned(), + pending_above_iceberg_qty.to_string(), + )); + } + + if let Some(pending_above_time_in_force) = request.pending_above_time_in_force { + params.push(( + "pendingAboveTimeInForce".to_owned(), + pending_above_time_in_force.to_string(), + )); + } + + if let Some(pending_above_strategy_id) = request.pending_above_strategy_id { + params.push(( + "pendingAboveStrategyId".to_owned(), + pending_above_strategy_id.to_string(), + )); + } + + if let Some(pending_above_strategy_type) = request.pending_above_strategy_type { + params.push(( + "pendingAboveStrategyType".to_owned(), + pending_above_strategy_type.to_string(), + )); + } + + if let Some(pending_below_type) = request.pending_below_type { + params.push(("pendingBelowType".to_owned(), pending_below_type)); + } + + if let Some(pending_below_client_order_id) = request.pending_below_client_order_id { + params.push(( + "pendingBelowClientOrderId".to_owned(), + pending_below_client_order_id, + )); + } + + if let Some(pending_below_price) = request.pending_below_price { + params.push(( + "pendingBelowPrice".to_owned(), + pending_below_price.to_string(), + )); + } + + if let Some(pending_below_stop_price) = request.pending_below_stop_price { + params.push(( + "pendingBelowStopPrice".to_owned(), + pending_below_stop_price.to_string(), + )); + } + + if let Some(pending_below_trailing_delta) = request.pending_below_trailing_delta { + params.push(( + "pendingBelowTrailingDelta".to_owned(), + pending_below_trailing_delta.to_string(), + )); + } + + if let Some(pending_below_iceberg_qty) = request.pending_below_iceberg_qty { + params.push(( + "pendingBelowIcebergQty".to_owned(), + pending_below_iceberg_qty.to_string(), + )); + } + + if let Some(pending_below_time_in_force) = request.pending_below_time_in_force { + params.push(( + "pendingBelowTimeInForce".to_owned(), + pending_below_time_in_force.to_string(), + )); + } + + if let Some(pending_below_strategy_id) = request.pending_below_strategy_id { + params.push(( + "pendingBelowStrategyId".to_owned(), + pending_below_strategy_id.to_string(), + )); + } + + if let Some(pending_below_strategy_type) = request.pending_below_strategy_type { + params.push(( + "pendingBelowStrategyType".to_owned(), + pending_below_strategy_type.to_string(), + )); + } + + if let Some(recv_window) = request.recv_window { + params.push(("recvWindow".to_owned(), recv_window.to_string())); + } + + Request { + path: "/api/v3/orderList/otoco".to_owned(), + method: Method::Post, + params, + credentials: request.credentials, + sign: true, + } + } +} + +#[cfg(test)] +mod tests { + use super::NewOTOCOOrder; + use crate::http::{request::Request, Credentials, Method}; + use crate::trade::order::{Side, TimeInForce, WorkingMandatoryParams}; + use rust_decimal_macros::dec; + + static API_KEY: &str = "api-key"; + static API_SECRET: &str = "api-secret"; + + #[test] + fn trade_new_otoco_order_convert_to_request_test() { + let credentials = Credentials::from_hmac(API_KEY.to_owned(), API_SECRET.to_owned()); + + let request: Request = NewOTOCOOrder::new( + "BNBUSDT", + WorkingMandatoryParams::new("LIMIT", Side::Sell, dec!(305), dec!(0.5)), + Side::Sell, + dec!(0.5), + "LIMIT_MAKER", + ) + .working_time_in_force(TimeInForce::Gtc) + .pending_above_price(dec!(308)) + .pending_below_type("STOP_LOSS_LIMIT") + .pending_below_stop_price(dec!(300.5)) + .pending_below_trailing_delta(dec!(30)) + .pending_below_time_in_force(TimeInForce::Gtc) + .pending_below_price(dec!(301)) + .credentials(&credentials) + .into(); + + assert_eq!( + request, + Request { + path: "/api/v3/orderList/otoco".to_owned(), + credentials: Some(credentials), + method: Method::Post, + params: vec![ + ("symbol".to_owned(), "BNBUSDT".to_string()), + ("workingType".to_owned(), "LIMIT".to_string()), + ("workingSide".to_owned(), "SELL".to_string()), + ("workingPrice".to_owned(), "305".to_string()), + ("workingQuantity".to_owned(), "0.5".to_string()), + ("pendingSide".to_owned(), "SELL".to_string()), + ("pendingQuantity".to_owned(), "0.5".to_string()), + ("pendingAboveType".to_owned(), "LIMIT_MAKER".to_string()), + ("workingTimeInForce".to_owned(), "GTC".to_string()), + ("pendingAbovePrice".to_owned(), "308".to_string()), + ("pendingBelowType".to_owned(), "STOP_LOSS_LIMIT".to_string()), + ("pendingBelowPrice".to_owned(), "301".to_string()), + ("pendingBelowStopPrice".to_owned(), "300.5".to_string()), + ("pendingBelowTrailingDelta".to_owned(), "30".to_string()), + ("pendingBelowTimeInForce".to_owned(), "GTC".to_string()), + ], + sign: true + } + ); + } +} diff --git a/src/trade/open_orders.rs b/src/trade/open_orders.rs index b2c6689..9929fc5 100644 --- a/src/trade/open_orders.rs +++ b/src/trade/open_orders.rs @@ -5,8 +5,8 @@ use crate::http::{request::Request, Credentials, Method}; /// Get all open orders on a symbol. Careful when accessing this with no symbol. /// /// Weight(IP): -/// * `3` for a single symbol; -/// * `40` when the symbol parameter is omitted; +/// * `6` for a single symbol; +/// * `80` when the symbol parameter is omitted; /// /// # Example /// diff --git a/src/trade/order.rs b/src/trade/order.rs index f09aa58..71e946b 100644 --- a/src/trade/order.rs +++ b/src/trade/order.rs @@ -1,3 +1,4 @@ +use rust_decimal::Decimal; use strum::Display; #[derive(Copy, Clone, Display)] @@ -30,3 +31,26 @@ pub enum CancelReplaceMode { #[strum(serialize = "ALLOW_FAILURE")] AllowFailure, } + +pub struct WorkingMandatoryParams { + pub working_type: String, + pub working_side: Side, + pub working_price: Decimal, + pub working_quantity: Decimal, +} + +impl WorkingMandatoryParams { + pub fn new( + working_type: &str, + working_side: Side, + working_price: Decimal, + working_quantity: Decimal, + ) -> Self { + Self { + working_type: working_type.to_owned(), + working_side, + working_price, + working_quantity, + } + } +} diff --git a/src/trade/order_limit_usage.rs b/src/trade/order_limit_usage.rs index d736ccd..f767d44 100644 --- a/src/trade/order_limit_usage.rs +++ b/src/trade/order_limit_usage.rs @@ -4,7 +4,7 @@ use crate::http::{request::Request, Credentials, Method}; /// /// Displays the user's current order count usage for all intervals. /// -/// Weight(IP): 20 +/// Weight(IP): 40 /// /// # Example /// diff --git a/src/utils.rs b/src/utils.rs index 3bf1b59..a852892 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,19 +1,20 @@ use crate::http::Signature; -use base64::encode; +use base64::{engine::general_purpose, Engine as _}; use ed25519_dalek::pkcs8::DecodePrivateKey; use ed25519_dalek::SigningKey; use ed25519_dalek::{Signature as Ed25519Signature, Signer}; use hmac::{Hmac, Mac}; -use sha2::{digest::InvalidLength, Sha256}; +use sha2::Sha256; +use std::error::Error; -pub fn sign(payload: &str, signature: &Signature) -> Result { +pub fn sign(payload: &str, signature: &Signature) -> Result> { match signature { Signature::Hmac(signature) => sign_hmac(payload, &signature.api_secret), Signature::Ed25519(signature) => sign_ed25519(payload, &signature.key), } } -fn sign_hmac(payload: &str, key: &str) -> Result { +fn sign_hmac(payload: &str, key: &str) -> Result> { let mut mac = Hmac::::new_from_slice(key.to_string().as_bytes())?; mac.update(payload.to_string().as_bytes()); @@ -21,12 +22,11 @@ fn sign_hmac(payload: &str, key: &str) -> Result { Ok(format!("{:x}", result.into_bytes())) } -fn sign_ed25519(payload: &str, key: &str) -> Result { - let private = SigningKey::from_pkcs8_pem(key); +fn sign_ed25519(payload: &str, key: &str) -> Result> { + let private_key = SigningKey::from_pkcs8_pem(key)?; - let signing_key: SigningKey = SigningKey::from_bytes(&private.unwrap().to_bytes()); - let signature: Ed25519Signature = signing_key.sign(&payload.to_string().into_bytes()); - Ok(encode(signature.to_bytes())) + let signature: Ed25519Signature = private_key.sign(payload.as_bytes()); + Ok(general_purpose::STANDARD.encode(signature.to_bytes())) } #[cfg(test)] diff --git a/src/version.rs b/src/version.rs index d1bc1fa..9397951 100644 --- a/src/version.rs +++ b/src/version.rs @@ -1 +1 @@ -pub const VERSION: &str = "1.1.0"; +pub const VERSION: &str = "1.3.0"; diff --git a/src/wallet/account_info.rs b/src/wallet/account_info.rs new file mode 100644 index 0000000..98b6268 --- /dev/null +++ b/src/wallet/account_info.rs @@ -0,0 +1,92 @@ +use crate::http::{request::Request, Credentials, Method}; + +/// `GET /sapi/v1/account/info` +/// +/// Fetch account info detail. +/// +/// Weight(IP): 1 +/// +/// # Example +/// +/// ``` +/// use binance_spot_connector_rust::wallet; +/// +/// let request = wallet::account_info().recv_window(5000); +/// ``` +pub struct AccountInfo { + recv_window: Option, + credentials: Option, +} + +impl AccountInfo { + pub fn new() -> Self { + Self { + recv_window: None, + credentials: None, + } + } + + pub fn recv_window(mut self, recv_window: u64) -> Self { + self.recv_window = Some(recv_window); + self + } + + pub fn credentials(mut self, credentials: &Credentials) -> Self { + self.credentials = Some(credentials.clone()); + self + } +} + +impl From for Request { + fn from(request: AccountInfo) -> Request { + let mut params = vec![]; + + if let Some(recv_window) = request.recv_window { + params.push(("recvWindow".to_owned(), recv_window.to_string())); + } + + Request { + path: "/sapi/v1/account/info".to_owned(), + method: Method::Get, + params, + credentials: request.credentials, + sign: true, + } + } +} + +impl Default for AccountInfo { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::AccountInfo; + use crate::http::{request::Request, Credentials, Method}; + + static API_KEY: &str = "api-key"; + static API_SECRET: &str = "api-secret"; + + #[test] + fn wallet_delist_schedule_convert_to_request_test() { + let credentials = Credentials::from_hmac(API_KEY.to_owned(), API_SECRET.to_owned()); + + let request: Request = AccountInfo::new() + .recv_window(5000) + .credentials(&credentials) + .into(); + + assert_eq!( + request, + Request { + path: "/sapi/v1/account/info".to_owned(), + credentials: Some(credentials), + method: Method::Get, + params: vec![("recvWindow".to_owned(), "5000".to_string())], + sign: true + } + ); + } +} diff --git a/src/wallet/balance.rs b/src/wallet/balance.rs new file mode 100644 index 0000000..8edf74f --- /dev/null +++ b/src/wallet/balance.rs @@ -0,0 +1,94 @@ +use crate::http::{request::Request, Credentials, Method}; + +/// `GET /sapi/v1/asset/wallet/balance` +/// +/// Query User Wallet Balance +/// +/// Weight(IP): 60 +/// +/// * You need to open Permits Universal Transfer permission for the API Key which requests this endpoint. +/// +/// # Example +/// +/// ``` +/// use binance_spot_connector_rust::wallet; +/// +/// let request = wallet::balance().recv_window(5000); +/// ``` +pub struct Balance { + recv_window: Option, + credentials: Option, +} + +impl Balance { + pub fn new() -> Self { + Self { + recv_window: None, + credentials: None, + } + } + + pub fn recv_window(mut self, recv_window: u64) -> Self { + self.recv_window = Some(recv_window); + self + } + + pub fn credentials(mut self, credentials: &Credentials) -> Self { + self.credentials = Some(credentials.clone()); + self + } +} + +impl From for Request { + fn from(request: Balance) -> Request { + let mut params = vec![]; + + if let Some(recv_window) = request.recv_window { + params.push(("recvWindow".to_owned(), recv_window.to_string())); + } + + Request { + path: "/sapi/v1/asset/wallet/balance".to_owned(), + method: Method::Get, + params, + credentials: request.credentials, + sign: true, + } + } +} + +impl Default for Balance { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::Balance; + use crate::http::{request::Request, Credentials, Method}; + + static API_KEY: &str = "api-key"; + static API_SECRET: &str = "api-secret"; + + #[test] + fn wallet_balance_convert_to_request_test() { + let credentials = Credentials::from_hmac(API_KEY.to_owned(), API_SECRET.to_owned()); + + let request: Request = Balance::new() + .recv_window(5000) + .credentials(&credentials) + .into(); + + assert_eq!( + request, + Request { + path: "/sapi/v1/asset/wallet/balance".to_owned(), + credentials: Some(credentials), + method: Method::Get, + params: vec![("recvWindow".to_owned(), "5000".to_string())], + sign: true + } + ); + } +} diff --git a/src/wallet/delist_schedule.rs b/src/wallet/delist_schedule.rs new file mode 100644 index 0000000..11e8df2 --- /dev/null +++ b/src/wallet/delist_schedule.rs @@ -0,0 +1,92 @@ +use crate::http::{request::Request, Credentials, Method}; + +/// `GET /sapi/v1/spot/delist-schedule` +/// +/// Get symbols delist schedule for spot +/// +/// Weight(IP): 100 +/// +/// # Example +/// +/// ``` +/// use binance_spot_connector_rust::wallet; +/// +/// let request = wallet::delist_schedule().recv_window(5000); +/// ``` +pub struct DelistSchedule { + recv_window: Option, + credentials: Option, +} + +impl DelistSchedule { + pub fn new() -> Self { + Self { + recv_window: None, + credentials: None, + } + } + + pub fn recv_window(mut self, recv_window: u64) -> Self { + self.recv_window = Some(recv_window); + self + } + + pub fn credentials(mut self, credentials: &Credentials) -> Self { + self.credentials = Some(credentials.clone()); + self + } +} + +impl From for Request { + fn from(request: DelistSchedule) -> Request { + let mut params = vec![]; + + if let Some(recv_window) = request.recv_window { + params.push(("recvWindow".to_owned(), recv_window.to_string())); + } + + Request { + path: "/sapi/v1/spot/delist-schedule".to_owned(), + method: Method::Get, + params, + credentials: request.credentials, + sign: true, + } + } +} + +impl Default for DelistSchedule { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::DelistSchedule; + use crate::http::{request::Request, Credentials, Method}; + + static API_KEY: &str = "api-key"; + static API_SECRET: &str = "api-secret"; + + #[test] + fn wallet_delist_schedule_convert_to_request_test() { + let credentials = Credentials::from_hmac(API_KEY.to_owned(), API_SECRET.to_owned()); + + let request: Request = DelistSchedule::new() + .recv_window(5000) + .credentials(&credentials) + .into(); + + assert_eq!( + request, + Request { + path: "/sapi/v1/spot/delist-schedule".to_owned(), + credentials: Some(credentials), + method: Method::Get, + params: vec![("recvWindow".to_owned(), "5000".to_string())], + sign: true + } + ); + } +} diff --git a/src/wallet/deposit_address_list.rs b/src/wallet/deposit_address_list.rs new file mode 100644 index 0000000..9a6d242 --- /dev/null +++ b/src/wallet/deposit_address_list.rs @@ -0,0 +1,94 @@ +use crate::http::{request::Request, Credentials, Method}; + +/// `GET /sapi/v1/capital/deposit/address/list` +/// +/// Fetch deposit address list with network. +/// +/// * If network is not send, return with default network of the coin. +/// * You can get network and isDefault in networkList in the response of `Get /sapi/v1/capital/config/getall`. +/// +/// Weight(IP): 10 +/// +/// # Example +/// +/// ``` +/// use binance_spot_connector_rust::wallet; +/// +/// let request = wallet::deposit_address_list("BNB").network("ETH"); +/// ``` +pub struct DepositAddressList { + coin: String, + network: Option, + credentials: Option, +} + +impl DepositAddressList { + pub fn new(coin: &str) -> Self { + Self { + coin: coin.to_owned(), + network: None, + credentials: None, + } + } + + pub fn network(mut self, network: &str) -> Self { + self.network = Some(network.to_owned()); + self + } + + pub fn credentials(mut self, credentials: &Credentials) -> Self { + self.credentials = Some(credentials.clone()); + self + } +} + +impl From for Request { + fn from(request: DepositAddressList) -> Request { + let mut params = vec![("coin".to_owned(), request.coin.to_string())]; + + if let Some(network) = request.network { + params.push(("network".to_owned(), network)); + } + + Request { + path: "/sapi/v1/capital/deposit/address/list".to_owned(), + method: Method::Get, + params, + credentials: request.credentials, + sign: true, + } + } +} + +#[cfg(test)] +mod tests { + use super::DepositAddressList; + use crate::http::{request::Request, Credentials, Method}; + + static API_KEY: &str = "api-key"; + static API_SECRET: &str = "api-secret"; + + #[test] + fn wallet_deposit_address_list_convert_to_request_test() { + let credentials = Credentials::from_hmac(API_KEY.to_owned(), API_SECRET.to_owned()); + + let request: Request = DepositAddressList::new("BNB") + .network("ETH") + .credentials(&credentials) + .into(); + + assert_eq!( + request, + Request { + path: "/sapi/v1/capital/deposit/address/list".to_owned(), + credentials: Some(credentials), + method: Method::Get, + params: vec![ + ("coin".to_owned(), "BNB".to_string()), + ("network".to_owned(), "ETH".to_string()), + ], + sign: true + } + ); + } +} diff --git a/src/wallet/deposit_credit_apply.rs b/src/wallet/deposit_credit_apply.rs new file mode 100644 index 0000000..8a9fe29 --- /dev/null +++ b/src/wallet/deposit_credit_apply.rs @@ -0,0 +1,125 @@ +use crate::http::{request::Request, Credentials, Method}; + +/// `POST /sapi/v1/capital/deposit/credit-apply` +/// +/// Apply deposit credit for expired address (One click arrival) +/// +/// Weight(IP): 1 +/// +/// # Example +/// +/// ``` +/// use binance_spot_connector_rust::wallet; +/// +/// let request = wallet::deposit_credit_apply().deposit_id(1); +/// ``` +pub struct DepositCreditApply { + deposit_id: Option, + tx_id: Option, + sub_account_id: Option, + sub_user_id: Option, + credentials: Option, +} + +impl DepositCreditApply { + pub fn new() -> Self { + Self { + deposit_id: None, + tx_id: None, + sub_account_id: None, + sub_user_id: None, + credentials: None, + } + } + + pub fn deposit_id(mut self, deposit_id: u64) -> Self { + self.deposit_id = Some(deposit_id); + self + } + + pub fn tx_id(mut self, tx_id: &str) -> Self { + self.tx_id = Some(tx_id.to_owned()); + self + } + + pub fn sub_account_id(mut self, sub_account_id: u64) -> Self { + self.sub_account_id = Some(sub_account_id); + self + } + + pub fn sub_user_id(mut self, sub_user_id: u64) -> Self { + self.sub_user_id = Some(sub_user_id); + self + } + + pub fn credentials(mut self, credentials: &Credentials) -> Self { + self.credentials = Some(credentials.clone()); + self + } +} + +impl From for Request { + fn from(request: DepositCreditApply) -> Request { + let mut params = vec![]; + + if let Some(deposit_id) = request.deposit_id { + params.push(("depositId".to_owned(), deposit_id.to_string())); + } + + if let Some(tx_id) = request.tx_id { + params.push(("txId".to_owned(), tx_id)); + } + + if let Some(sub_account_id) = request.sub_account_id { + params.push(("subAccountId".to_owned(), sub_account_id.to_string())); + } + + if let Some(sub_user_id) = request.sub_user_id { + params.push(("subUserId".to_owned(), sub_user_id.to_string())); + } + + Request { + path: "/sapi/v1/capital/deposit/credit-apply".to_owned(), + method: Method::Post, + params, + credentials: request.credentials, + sign: true, + } + } +} + +impl Default for DepositCreditApply { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::DepositCreditApply; + use crate::http::{request::Request, Credentials, Method}; + + static API_KEY: &str = "api-key"; + static API_SECRET: &str = "api-secret"; + + #[test] + fn wallet_deposit_credit_apply_convert_to_request_test() { + let credentials = Credentials::from_hmac(API_KEY.to_owned(), API_SECRET.to_owned()); + + let request: Request = DepositCreditApply::new() + .deposit_id(1) + .credentials(&credentials) + .into(); + + assert_eq!( + request, + Request { + path: "/sapi/v1/capital/deposit/credit-apply".to_owned(), + credentials: Some(credentials), + method: Method::Post, + params: vec![("depositId".to_owned(), "1".to_owned())], + sign: true + } + ); + } +} diff --git a/src/wallet/dustable_assets.rs b/src/wallet/dustable_assets.rs index 5b02be8..95daf66 100644 --- a/src/wallet/dustable_assets.rs +++ b/src/wallet/dustable_assets.rs @@ -12,6 +12,7 @@ use crate::http::{request::Request, Credentials, Method}; /// let request = wallet::dustable_assets(); /// ``` pub struct DustableAssets { + account_type: Option, recv_window: Option, credentials: Option, } @@ -19,11 +20,17 @@ pub struct DustableAssets { impl DustableAssets { pub fn new() -> Self { Self { + account_type: None, recv_window: None, credentials: None, } } + pub fn account_type(mut self, account_type: &str) -> Self { + self.account_type = Some(account_type.to_owned()); + self + } + pub fn recv_window(mut self, recv_window: u64) -> Self { self.recv_window = Some(recv_window); self @@ -39,6 +46,10 @@ impl From for Request { fn from(request: DustableAssets) -> Request { let mut params = vec![]; + if let Some(account_type) = request.account_type { + params.push(("accountType".to_owned(), account_type)); + } + if let Some(recv_window) = request.recv_window { params.push(("recvWindow".to_owned(), recv_window.to_string())); } diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index dfcd9a0..6a45207 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -1,13 +1,18 @@ //! Market Data +pub mod account_info; pub mod account_snapshot; pub mod account_status; pub mod api_key_permission; pub mod api_trading_status; pub mod asset_detail; pub mod asset_dividend_record; +pub mod balance; pub mod coin_info; +pub mod delist_schedule; pub mod deposit_address; +pub mod deposit_address_list; +pub mod deposit_credit_apply; pub mod deposit_history; pub mod disable_fast_withdraw; pub mod dust_log; @@ -17,6 +22,7 @@ pub mod enable_fast_withdraw; pub mod funding_wallet; pub mod system_status; pub mod trade_fee; +pub mod transfer_history; pub mod universal_transfer; pub mod universal_transfer_history; pub mod user_asset; @@ -25,14 +31,19 @@ pub mod withdraw_history; use rust_decimal::Decimal; +use account_info::AccountInfo; use account_snapshot::AccountSnapshot; use account_status::AccountStatus; use api_key_permission::APIKeyPermission; use api_trading_status::APITradingStatus; use asset_detail::AssetDetail; use asset_dividend_record::AssetDividendRecord; +use balance::Balance; use coin_info::CoinInfo; +use delist_schedule::DelistSchedule; use deposit_address::DepositAddress; +use deposit_address_list::DepositAddressList; +use deposit_credit_apply::DepositCreditApply; use deposit_history::DepositHistory; use disable_fast_withdraw::DisableFastWithdraw; use dust_log::DustLog; @@ -42,6 +53,7 @@ use enable_fast_withdraw::EnableFastWithdraw; use funding_wallet::FundingWallet; use system_status::SystemStatus; use trade_fee::TradeFee; +use transfer_history::TransferHistory; use universal_transfer::UniversalTransfer; use universal_transfer_history::UniversalTransferHistory; use user_asset::UserAsset; @@ -135,3 +147,27 @@ pub fn user_asset() -> UserAsset { pub fn api_key_permission() -> APIKeyPermission { APIKeyPermission::new() } + +pub fn account_info() -> AccountInfo { + AccountInfo::new() +} + +pub fn balance() -> Balance { + Balance::new() +} + +pub fn delist_schedule() -> DelistSchedule { + DelistSchedule::new() +} + +pub fn deposit_address_list(coin: &str) -> DepositAddressList { + DepositAddressList::new(coin) +} + +pub fn deposit_credit_apply() -> DepositCreditApply { + DepositCreditApply::new() +} + +pub fn transfer_history(email: &str, start_time: u64, end_time: u64) -> TransferHistory { + TransferHistory::new(email, start_time, end_time) +} diff --git a/src/wallet/transfer_history.rs b/src/wallet/transfer_history.rs new file mode 100644 index 0000000..268c2e9 --- /dev/null +++ b/src/wallet/transfer_history.rs @@ -0,0 +1,147 @@ +use crate::http::{request::Request, Credentials, Method}; + +/// `GET /sapi/v1/asset/custody/transfer-history` +/// +/// Query User Delegation History +/// +/// * You need to open Enable Spot & Margin Trading permission for the API Key which requests this endpoint +/// +/// Weight(IP): 60 +/// +/// # Example +/// +/// ``` +/// use binance_spot_connector_rust::wallet; +/// +/// let request = wallet::transfer_history("a@a", 1695205406000, 1695208406000); +/// ``` +pub struct TransferHistory { + email: String, + start_time: u64, + end_time: u64, + transfer_type: Option, + asset: Option, + current: Option, + size: Option, + recv_window: Option, + credentials: Option, +} + +impl TransferHistory { + pub fn new(email: &str, start_time: u64, end_time: u64) -> Self { + Self { + email: email.to_owned(), + start_time, + end_time, + transfer_type: None, + asset: None, + current: None, + size: None, + recv_window: None, + credentials: None, + } + } + + pub fn transfer_type(mut self, transfer_type: &str) -> Self { + self.transfer_type = Some(transfer_type.to_owned()); + self + } + + pub fn asset(mut self, asset: &str) -> Self { + self.asset = Some(asset.to_owned()); + self + } + + pub fn current(mut self, current: u32) -> Self { + self.current = Some(current); + self + } + + pub fn size(mut self, size: u32) -> Self { + self.size = Some(size); + self + } + + pub fn recv_window(mut self, recv_window: u64) -> Self { + self.recv_window = Some(recv_window); + self + } + + pub fn credentials(mut self, credentials: &Credentials) -> Self { + self.credentials = Some(credentials.clone()); + self + } +} + +impl From for Request { + fn from(request: TransferHistory) -> Request { + let mut params = vec![ + ("email".to_owned(), request.email), + ("startTime".to_owned(), request.start_time.to_string()), + ("endTime".to_owned(), request.end_time.to_string()), + ]; + + if let Some(transfer_type) = request.transfer_type { + params.push(("transferType".to_owned(), transfer_type)); + } + + if let Some(asset) = request.asset { + params.push(("asset".to_owned(), asset)); + } + + if let Some(current) = request.current { + params.push(("current".to_owned(), current.to_string())); + } + + if let Some(size) = request.size { + params.push(("size".to_owned(), size.to_string())); + } + + if let Some(recv_window) = request.recv_window { + params.push(("recvWindow".to_owned(), recv_window.to_string())); + } + + Request { + path: "/sapi/v1/asset/custody/transfer-history".to_owned(), + method: Method::Get, + params, + credentials: request.credentials, + sign: true, + } + } +} + +#[cfg(test)] +mod tests { + use super::TransferHistory; + use crate::http::{request::Request, Credentials, Method}; + + static API_KEY: &str = "api-key"; + static API_SECRET: &str = "api-secret"; + + #[test] + fn wallet_transfert_history_convert_to_request_test() { + let credentials = Credentials::from_hmac(API_KEY.to_owned(), API_SECRET.to_owned()); + + let request: Request = TransferHistory::new("a@a", 1695205406000, 1695208406000) + .recv_window(5000) + .credentials(&credentials) + .into(); + + assert_eq!( + request, + Request { + path: "/sapi/v1/asset/custody/transfer-history".to_owned(), + credentials: Some(credentials), + method: Method::Get, + params: vec![ + ("email".to_owned(), "a@a".to_string()), + ("startTime".to_owned(), "1695205406000".to_string()), + ("endTime".to_owned(), "1695208406000".to_string()), + ("recvWindow".to_owned(), "5000".to_string()), + ], + sign: true + } + ); + } +} diff --git a/src/wallet/withdraw_history.rs b/src/wallet/withdraw_history.rs index 3d443c3..e850d6e 100644 --- a/src/wallet/withdraw_history.rs +++ b/src/wallet/withdraw_history.rs @@ -8,7 +8,9 @@ use crate::http::{request::Request, Credentials, Method}; /// * Please notice the default `startTime` and `endTime` to make sure that time interval is within 0-90 days. /// * If both `startTime` and `endTime` are sent, time between `startTime` and `endTime` must be less than 90 days /// -/// Weight(IP): 1 +/// Weight(IP): 18000 +/// * Request limit: 10 requests per second +/// * This endpoint specifically uses per second IP rate limit, user's total second level IP rate limit is 180000/second. Response from the endpoint contains header key `X-SAPI-USED-IP-WEIGHT-1S`, which defines weight used by the current IP. /// /// # Example ///