use std::collections::BTreeMap;
use eyre::{Result, eyre};
use jiff::Timestamp;
use serde::Deserialize;
use serde_with::{DisplayFromStr, serde_as};
use v_exchanges_adapters::binance::{BinanceAuth, BinanceHttpUrl, BinanceOption};
use v_utils::{
macros::ScreamIt,
trades::{Asset, Pair, Side, Usd},
};
use crate::{
ExchangeResult,
core::{ApiKeyInfo, AssetBalance, Balances, KeyPermission, PersonalInfo},
};
pub async fn income_history(client: &v_exchanges_adapters::Client, request: IncomeRequest, recv_window: Option<std::time::Duration>) -> ExchangeResult<Vec<IncomeRecord>> {
assert!(client.is_authenticated::<BinanceOption>());
let mut options = vec![BinanceOption::HttpUrl(BinanceHttpUrl::FuturesUsdM), BinanceOption::HttpAuth(BinanceAuth::Sign)];
if let Some(rw) = recv_window {
options.push(BinanceOption::RecvWindow(rw));
}
let mut params = vec![];
if let Some(symbol) = &request.symbol {
params.push(("symbol", symbol.clone()));
}
if let Some(income_type) = &request.income_type {
params.push(("incomeType", income_type.to_string()));
}
if let Some(start_time) = request.start_time {
params.push(("startTime", start_time.to_string()));
}
if let Some(end_time) = request.end_time {
params.push(("endTime", end_time.to_string()));
}
if let Some(limit) = request.limit {
params.push(("limit", limit.to_string()));
}
if let Some(page) = request.page {
params.push(("page", page.to_string()));
}
let response: Vec<IncomeRecord> = client.get("/fapi/v1/income", ¶ms, options).await?;
Ok(response)
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderResponse {
pub client_order_id: String,
pub cum_qty: String,
pub cum_quote: String,
#[serde_as(as = "DisplayFromStr")]
pub executed_qty: f64,
pub order_id: u64,
#[serde_as(as = "Option<DisplayFromStr>")]
pub avg_price: Option<f64>,
#[serde_as(as = "Option<DisplayFromStr>")]
pub orig_qty: Option<f64>,
#[serde_as(as = "Option<DisplayFromStr>")]
pub price: Option<f64>,
pub reduce_only: bool,
pub side: String,
pub position_side: String,
pub status: String,
#[serde_as(as = "Option<DisplayFromStr>")]
pub stop_price: Option<f64>,
pub close_position: bool,
pub symbol: String,
pub time_in_force: String,
#[serde(rename = "type")]
pub order_type: String,
#[serde_as(as = "Option<DisplayFromStr>")]
pub activation_price: Option<f64>,
#[serde_as(as = "Option<DisplayFromStr>")]
pub price_rate: Option<f64>,
pub update_time: u64,
pub working_type: String,
pub price_protect: bool,
#[serde(rename = "priceMatch")]
pub price_match: Option<String>,
pub self_trade_prevention_mode: Option<String>,
pub good_till_date: Option<u64>,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IncomeRecord {
pub symbol: String,
pub income_type: String,
#[serde_as(as = "DisplayFromStr")]
pub income: f64,
pub asset: String,
pub info: String,
pub time: u64,
pub tran_id: String,
pub trade_id: String,
}
#[derive(Clone, Debug)]
pub struct OrderRequest {
pub symbol: String,
pub side: Side,
pub order_type: OrderType,
pub position_side: Option<PositionSide>,
pub time_in_force: Option<TimeInForce>,
pub qty: Option<f64>,
pub price: Option<f64>,
pub stop_price: Option<f64>,
pub reduce_only: Option<bool>,
pub close_position: Option<bool>,
pub activation_price: Option<f64>,
pub callback_rate: Option<f64>,
pub working_type: Option<WorkingType>,
pub price_protect: Option<bool>,
pub new_client_order_id: Option<String>,
}
#[derive(Clone, Debug)]
pub struct IncomeRequest {
pub symbol: Option<String>,
pub income_type: Option<IncomeType>,
pub start_time: Option<u64>,
pub end_time: Option<u64>,
pub limit: Option<u32>,
pub page: Option<u32>,
}
#[derive(Clone, Debug, ScreamIt)]
pub enum ContractType {
Perpetual,
CurrentMonth,
NextMonth,
CurrentQuarter,
NextQuarter,
}
#[derive(Clone, Debug, ScreamIt)]
pub enum PositionSide {
Both,
Long,
Short,
}
#[derive(Clone, Debug, ScreamIt)]
pub enum OrderType {
Limit,
Market,
Stop,
StopMarket,
TakeProfit,
TakeProfitMarket,
TrailingStopMarket,
}
#[derive(Clone, Debug, ScreamIt)]
pub enum WorkingType {
MarkPrice,
ContractPrice,
}
#[derive(Clone, Debug, ScreamIt)]
pub enum TimeInForce {
Gtc,
Ioc,
Fok,
Gtx,
}
#[derive(Clone, Debug, ScreamIt)]
pub enum IncomeType {
Transfer,
WelcomeBonus,
RealizedPnl,
FundingFee,
Commission,
InsuranceClear,
ReferralKickback,
CommissionRebate,
ApiRebate,
ContestReward,
CrossCollateralTransfer,
OptionsPremiumFee,
OptionsSettleProfit,
InternalTransfer,
AutoExchange,
DeliveredSettlement,
CoinSwapDeposit,
CoinSwapWithdraw,
PositionLimitIncreaseFee,
}
pub(in crate::binance) async fn personal_info(client: &v_exchanges_adapters::Client, recv_window: Option<std::time::Duration>, prices: &BTreeMap<Pair, f64>) -> ExchangeResult<PersonalInfo> {
assert!(client.is_authenticated::<BinanceOption>());
let mut balance_options = vec![BinanceOption::HttpUrl(BinanceHttpUrl::FuturesUsdM), BinanceOption::HttpAuth(BinanceAuth::Sign)];
let mut api_options = vec![BinanceOption::HttpUrl(BinanceHttpUrl::Spot), BinanceOption::HttpAuth(BinanceAuth::Sign)];
if let Some(rw) = recv_window {
balance_options.push(BinanceOption::RecvWindow(rw));
api_options.push(BinanceOption::RecvWindow(rw));
}
let (balance_result, api_result) = tokio::join!(
client.get_no_query::<Vec<AssetBalanceResponse>, _>("/fapi/v3/balance", balance_options),
client.get_no_query::<ApiRestrictionsResponse, _>("/sapi/v1/account/apiRestrictions", api_options),
);
let rs = balance_result?;
let api_response = api_result?;
fn usd_value(underlying: f64, asset: Asset, prices: &BTreeMap<Pair, f64>) -> Result<Usd> {
if underlying == 0. {
return Ok(Usd(0.));
}
if asset == "USDT" {
return Ok(Usd(underlying));
}
let usdt_pair = Pair::new(asset, "USDT".into());
let usdt_price = prices.get(&usdt_pair).ok_or_else(|| eyre!("No usdt price found for {asset}, which has non-zero balance."))?;
Ok((underlying * usdt_price).into())
}
let mut asset_balances: Vec<AssetBalance> = Vec::with_capacity(rs.len());
for r in rs {
let asset = r.asset.into();
let underlying = r.balance;
asset_balances.push(AssetBalance {
asset,
underlying,
usd: Some(usd_value(underlying, asset, prices)?),
});
}
let non_zero: Vec<AssetBalance> = asset_balances.iter().filter(|b| b.underlying != 0.).cloned().collect();
let total = non_zero.iter().fold(Usd(0.), |acc, b| acc + b.usd.unwrap_or(Usd(0.)));
let expire_time = api_response
.expire_time
.map(|ms| Timestamp::from_millisecond(ms).expect("Binance expireTime is valid ms timestamp"));
Ok(PersonalInfo {
api: ApiKeyInfo {
expire_time,
permissions: api_response.into(),
},
balances: Balances::new(non_zero, total),
})
}
#[allow(unused)]
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct AssetBalanceResponse {
account_alias: String,
pub asset: String,
#[serde_as(as = "DisplayFromStr")]
pub balance: f64,
#[serde_as(as = "DisplayFromStr")]
cross_wallet_balance: f64,
#[serde(rename = "crossUnPnl")]
#[serde_as(as = "DisplayFromStr")]
cross_unrealized_pnl: f64,
#[serde_as(as = "DisplayFromStr")]
available_balance: f64,
#[serde_as(as = "DisplayFromStr")]
max_withdraw_amount: f64,
margin_available: bool,
update_time: u64,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ApiRestrictionsResponse {
expire_time: Option<i64>,
#[allow(unused)]
create_time: i64,
#[allow(unused)]
ip_restrict: bool,
enable_reading: bool,
enable_futures: bool,
enable_spot_and_margin_trading: bool,
enable_withdrawals: bool,
enable_internal_transfer: bool,
enable_margin_loan: Option<bool>,
enable_vanilla_options: bool,
permits_universal_transfer: bool,
enable_portfolio_margin_trading: bool,
enable_fix_api_trade: bool,
enable_fix_read_only: bool,
enable_margin: bool,
}
impl From<ApiRestrictionsResponse> for Vec<KeyPermission> {
fn from(r: ApiRestrictionsResponse) -> Self {
let mut out = Vec::new();
if r.enable_reading {
out.push(KeyPermission::Read);
}
if r.enable_futures {
out.push(KeyPermission::Futures);
}
if r.enable_spot_and_margin_trading {
out.push(KeyPermission::SpotTrade);
}
if r.enable_withdrawals {
out.push(KeyPermission::Withdraw);
}
if r.enable_internal_transfer || r.permits_universal_transfer {
out.push(KeyPermission::Transfer);
}
if r.enable_margin_loan.unwrap_or(false) || r.enable_margin {
out.push(KeyPermission::Margin);
}
if r.enable_vanilla_options {
out.push(KeyPermission::Options);
}
if r.enable_portfolio_margin_trading {
out.push(KeyPermission::Other("PortfolioMarginTrading".to_owned()));
}
if r.enable_fix_api_trade {
out.push(KeyPermission::Other("FixApiTrade".to_owned()));
}
if r.enable_fix_read_only {
out.push(KeyPermission::Other("FixReadOnly".to_owned()));
}
out
}
}