exc_binance/http/response/
instrument.rs1#![allow(dead_code)]
2
3use exc_core::{symbol::ExcSymbol, Asset, Str};
4use rust_decimal::Decimal;
5use serde::Deserialize;
6
7use crate::http::error::RestError;
8
9use super::Data;
10
11const TRADING: &str = "TRADING";
12
13#[derive(Debug, Deserialize)]
15#[serde(untagged)]
16pub enum ExchangeInfo {
17 UsdMarginFutures(UFExchangeInfo),
19 Spot(SpotExchangeInfo),
21 EuropeanOptions(EuropeanExchangeInfo),
23}
24
25#[derive(Debug, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub struct UFExchangeInfo {
29 pub(crate) exchange_filters: Vec<serde_json::Value>,
30 pub(crate) rate_limits: Vec<RateLimit>,
31 pub(crate) assets: Vec<serde_json::Value>,
32 pub(crate) symbols: Vec<UFSymbol>,
33 pub(crate) timezone: String,
34}
35
36#[derive(Debug, Deserialize)]
38#[serde(rename_all = "camelCase")]
39pub struct SpotExchangeInfo {
40 pub(crate) exchange_filters: Vec<serde_json::Value>,
41 pub(crate) rate_limits: Vec<RateLimit>,
42 #[serde(default)]
43 pub(crate) assets: Vec<serde_json::Value>,
44 pub(crate) symbols: Vec<SpotSymbol>,
45 pub(crate) timezone: String,
46}
47
48#[derive(Debug, Deserialize)]
50#[serde(rename_all = "camelCase")]
51pub struct EuropeanExchangeInfo {
52 pub(crate) option_contracts: Vec<serde_json::Value>,
53 pub(crate) option_assets: Vec<serde_json::Value>,
54 pub(crate) option_symbols: Vec<OptionSymbol>,
55 pub(crate) rate_limits: Vec<RateLimit>,
56 pub(crate) timezone: String,
57}
58
59#[derive(Debug, Deserialize)]
60#[serde(rename_all = "camelCase")]
61pub(crate) struct RateLimit {
62 interval: String,
63 interval_num: u64,
64 limit: u64,
65 rate_limit_type: String,
66}
67
68#[derive(Debug, Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub(crate) struct SpotSymbol {
71 pub(crate) symbol: String,
72 pub(crate) status: String,
73 pub(crate) base_asset: Asset,
74 pub(crate) base_asset_precision: u32,
75 pub(crate) quote_asset: Asset,
76 pub(crate) quote_precision: u32,
77 pub(crate) order_types: Vec<String>,
78 pub(crate) iceberg_allowed: bool,
79 pub(crate) oco_allowed: bool,
80 pub(crate) quote_order_qty_market_allowed: bool,
81 pub(crate) allow_trailing_stop: bool,
82 pub(crate) cancel_replace_allowed: bool,
83 pub(crate) is_spot_trading_allowed: bool,
84 pub(crate) is_margin_trading_allowed: bool,
85 pub(crate) filters: Vec<Filter>,
86 pub(crate) permissions: Vec<String>,
87}
88
89impl SpotSymbol {
90 pub(crate) fn to_exc_symbol(&self) -> Result<ExcSymbol, RestError> {
91 Ok(ExcSymbol::spot(&self.base_asset, &self.quote_asset))
92 }
93
94 pub(crate) fn is_live(&self) -> bool {
95 self.status == TRADING
96 }
97}
98
99#[derive(Debug, Deserialize)]
100#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
101pub(crate) enum KnownContractType {
102 Perpetual,
103 CurrentQuarter,
104 NextQuarter,
105}
106
107#[derive(Debug, Deserialize)]
108#[serde(untagged)]
109pub(crate) enum ContractType {
110 Known(KnownContractType),
111 Unknwon(String),
112}
113
114impl ContractType {
115 const U_EMPTY: Str = Str::new_inline("U-EMPTY");
116 const U_PERP: Str = Str::new_inline("U-PERP");
117 const U_CURRENT_QUARTER: Str = Str::new_inline("U-QUARTER");
118 const U_NEXT_QUARTER: Str = Str::new_inline("U-NEXT-QUARTER");
119
120 fn to_prefix(&self) -> Str {
121 match self {
122 Self::Known(c) => match c {
123 KnownContractType::Perpetual => Self::U_PERP,
124 KnownContractType::CurrentQuarter => Self::U_CURRENT_QUARTER,
125 KnownContractType::NextQuarter => Self::U_NEXT_QUARTER,
126 },
127 Self::Unknwon(s) => {
128 if s.is_empty() {
129 Self::U_EMPTY
130 } else {
131 Str::new(format!("U-{s}"))
132 }
133 }
134 }
135 }
136}
137
138#[derive(Debug, Deserialize)]
139#[serde(rename_all = "camelCase")]
140pub(crate) struct UFSymbol {
141 pub(crate) symbol: String,
142 pub(crate) pair: String,
143 pub(crate) contract_type: ContractType,
144 pub(crate) delivery_date: i64,
145 pub(crate) onboard_date: i64,
146 pub(crate) status: String,
147 pub(crate) base_asset: Asset,
148 pub(crate) quote_asset: Asset,
149 pub(crate) margin_asset: Asset,
150 pub(crate) price_precision: u32,
151 pub(crate) quantity_precision: u32,
152 pub(crate) base_asset_precision: u32,
153 pub(crate) quote_precision: u32,
154 pub(crate) underlying_type: String,
155 pub(crate) settle_plan: i64,
156 pub(crate) trigger_protect: Decimal,
157 pub(crate) order_types: Vec<String>,
158 pub(crate) time_in_force: Vec<String>,
159 pub(crate) liquidation_fee: Decimal,
160 pub(crate) market_take_bound: Decimal,
161 pub(crate) filters: Vec<Filter>,
162}
163
164impl UFSymbol {
165 pub(crate) fn is_live(&self) -> bool {
166 self.status == TRADING
167 }
168
169 pub(crate) fn to_exc_symbol(&self) -> Result<ExcSymbol, RestError> {
170 match &self.contract_type {
171 ContractType::Known(ty) => match ty {
172 KnownContractType::Perpetual => {
173 Ok(ExcSymbol::perpetual(&self.base_asset, &self.quote_asset))
174 }
175 KnownContractType::NextQuarter | KnownContractType::CurrentQuarter => {
176 let date = self
177 .symbol
178 .split('_')
179 .last()
180 .ok_or(RestError::MissingDateForFutures)?;
181 ExcSymbol::futures_with_str(&self.base_asset, &self.quote_asset, date)
182 .ok_or(RestError::FailedToBuildExcSymbol)
183 }
184 },
185 ContractType::Unknwon(ty) => Err(RestError::UnknownContractType(ty.clone())),
186 }
187 }
188}
189
190#[derive(Debug, Deserialize)]
191#[serde(untagged)]
192pub(crate) enum Filter {
193 Symbol(SymbolFilter),
195 Unknwon(serde_json::Value),
197}
198
199#[derive(Debug, Deserialize)]
200#[serde(tag = "filterType")]
201pub(crate) enum SymbolFilter {
202 #[serde(rename = "PRICE_FILTER")]
203 PriceFilter {
204 #[serde(rename = "maxPrice")]
206 max_price: Decimal,
207 #[serde(rename = "minPrice")]
209 min_price: Decimal,
210 #[serde(rename = "tickSize")]
212 tick_size: Decimal,
213 },
214 #[serde(rename = "LOT_SIZE")]
215 LotSize {
216 #[serde(rename = "maxQty")]
218 max_qty: Decimal,
219 #[serde(rename = "minQty")]
221 min_qty: Decimal,
222 #[serde(rename = "stepSize")]
224 step_size: Decimal,
225 },
226 #[serde(rename = "MARKET_LOT_SIZE")]
227 MarketLotSize {
228 #[serde(rename = "maxQty")]
230 max_qty: Decimal,
231 #[serde(rename = "minQty")]
233 min_qty: Decimal,
234 #[serde(rename = "stepSize")]
236 step_size: Decimal,
237 },
238 #[serde(rename = "MAX_NUM_ORDERS")]
239 MaxNumOrders {
240 limit: u64,
242 },
243 #[serde(rename = "MAX_NUM_ALGO_ORDERS")]
244 MaxNumAlgoOrders {
245 limit: u64,
247 },
248 #[serde(rename = "MIN_NOTIONAL")]
249 MinNotional {
250 #[serde(alias = "minNotional")]
251 notional: Decimal,
252 },
253 #[serde(rename = "NOTIONAL")]
254 Notional {
255 #[serde(alias = "minNotional")]
256 min_notional: Decimal,
257 },
258 #[serde(rename = "PERCENT_PRICE")]
259 PercentPrice {
260 #[serde(rename = "multiplierUp")]
261 multiplier_up: Decimal,
262 #[serde(rename = "multiplierDown")]
263 multiplier_down: Decimal,
264 #[serde(rename = "multiplierDecimal")]
265 multiplier_decimal: Decimal,
266 },
267}
268
269#[derive(Debug, Deserialize)]
270#[serde(rename_all = "camelCase")]
271pub(crate) struct OptionSymbol {
272 pub(crate) contract_id: i64,
273 pub(crate) expiry_date: i64,
274 pub(crate) filters: Vec<Filter>,
275 pub(crate) id: i64,
276 pub(crate) symbol: String,
277 pub(crate) side: OptionSide,
278 pub(crate) strike_price: Decimal,
279 pub(crate) underlying: String,
280 pub(crate) unit: u32,
281 pub(crate) maker_fee_rate: Decimal,
282 pub(crate) taker_fee_rate: Decimal,
283 pub(crate) min_qty: Decimal,
284 pub(crate) max_qty: Decimal,
285 pub(crate) maintenance_margin: Decimal,
286 pub(crate) min_initial_margin: Decimal,
287 pub(crate) min_maintenance_margin: Decimal,
288 pub(crate) price_scale: u32,
289 pub(crate) quantity_scale: i64,
290 pub(crate) quote_asset: Asset,
291}
292
293#[derive(Debug, Deserialize)]
294#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
295pub(crate) enum OptionSide {
296 Call,
297 Put,
298}
299
300impl OptionSymbol {
301 fn base(&self) -> Option<Asset> {
302 let (base, _) = self.symbol.split_once('-')?;
303 base.parse().ok()
304 }
305
306 pub(crate) fn expire_ts(&self) -> Result<time::OffsetDateTime, RestError> {
307 time::OffsetDateTime::from_unix_timestamp_nanos(self.expiry_date as i128 * 1_000_000)
309 .map_err(|_| RestError::InvalidDateForOptions)
310 }
311
312 fn expiry_date(&self) -> Result<time::Date, RestError> {
313 let ts = self.expire_ts()?;
314 Ok(ts.date())
315 }
316
317 pub(crate) fn to_exc_symbol(&self) -> Result<ExcSymbol, RestError> {
318 let base = self
319 .base()
320 .ok_or_else(|| RestError::MissingBaseAssetForOptions)?;
321 let quote = &self.quote_asset;
322 let date = self.expiry_date()?;
323 let price = self.strike_price.normalize();
324 let symbol = match self.side {
325 OptionSide::Call => ExcSymbol::call(&base, quote, date, price),
326 OptionSide::Put => ExcSymbol::put(&base, quote, date, price),
327 }
328 .ok_or_else(|| RestError::InvalidDateForOptions)?;
329 Ok(symbol)
330 }
331
332 pub(crate) fn is_live(&self) -> bool {
333 true
335 }
336}
337
338impl TryFrom<Data> for ExchangeInfo {
339 type Error = RestError;
340
341 fn try_from(value: Data) -> Result<Self, Self::Error> {
342 match value {
343 Data::ExchangeInfo(v) => Ok(v),
344 _ => Err(RestError::UnexpectedResponseType(anyhow::anyhow!(
345 "{value:?}"
346 ))),
347 }
348 }
349}