Skip to main content

exc_binance/http/response/
instrument.rs

1#![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/// Exchange info.
14#[derive(Debug, Deserialize)]
15#[serde(untagged)]
16pub enum ExchangeInfo {
17    /// Usd-margin futures.
18    UsdMarginFutures(UFExchangeInfo),
19    /// Spot.
20    Spot(SpotExchangeInfo),
21    /// European options.
22    EuropeanOptions(EuropeanExchangeInfo),
23}
24
25/// Usd-margin futures exchange info.
26#[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/// Spot exchange info.
37#[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/// European options exchange info.
49#[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.
194    Symbol(SymbolFilter),
195    /// Unknwon.
196    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        /// Max price.
205        #[serde(rename = "maxPrice")]
206        max_price: Decimal,
207        /// Min price.
208        #[serde(rename = "minPrice")]
209        min_price: Decimal,
210        /// Tick size.
211        #[serde(rename = "tickSize")]
212        tick_size: Decimal,
213    },
214    #[serde(rename = "LOT_SIZE")]
215    LotSize {
216        /// Max quantity.
217        #[serde(rename = "maxQty")]
218        max_qty: Decimal,
219        /// Min quantity.
220        #[serde(rename = "minQty")]
221        min_qty: Decimal,
222        /// step size.
223        #[serde(rename = "stepSize")]
224        step_size: Decimal,
225    },
226    #[serde(rename = "MARKET_LOT_SIZE")]
227    MarketLotSize {
228        /// Max quantity.
229        #[serde(rename = "maxQty")]
230        max_qty: Decimal,
231        /// Min quantity.
232        #[serde(rename = "minQty")]
233        min_qty: Decimal,
234        /// step size.
235        #[serde(rename = "stepSize")]
236        step_size: Decimal,
237    },
238    #[serde(rename = "MAX_NUM_ORDERS")]
239    MaxNumOrders {
240        /// Limit.
241        limit: u64,
242    },
243    #[serde(rename = "MAX_NUM_ALGO_ORDERS")]
244    MaxNumAlgoOrders {
245        /// Limit.
246        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        // Expiry date is in milliseconds.
308        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        // FIXME: Check if the option is live by comparing the expiry date with the current date.
334        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}