v_exchanges 0.17.3

Implementations of HTTP/HTTPS/WebSocket API methods for some crypto exchanges, using [crypto-botters](<https://github.com/negi-grass/crypto-botters>) framework
Documentation
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},
};

// balance {{{
/// Query income history
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", &params, 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),
	})
}

// Order Placement {{{

//,}}}

// Income History {{{

//,}}}

#[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,
}
//,}}}

// Response Types {{{
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ApiRestrictionsResponse {
	/// Millisecond timestamp; absent when no expiry is set
	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,
	/// Absent for keys that don't have margin lending permissions configured
	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
	}
}
//,}}}

// Request/Enum Types {{{

//,}}}