Skip to main content

bpx_api_types/
markets.rs

1use rust_decimal::Decimal;
2use serde::{Deserialize, Deserializer, Serialize};
3
4use crate::Blockchain;
5
6/// An asset is most of the time a crypto coin that can have multiple representations
7/// across different blockchains. For example, USDT.
8#[derive(Debug, Serialize, Deserialize)]
9#[serde(rename_all = "camelCase")]
10pub struct Asset {
11    /// CoinGecko ID for price tracking
12    pub coingecko_id: Option<String>,
13    /// Human-readable display name
14    pub display_name: String,
15    /// Identifier
16    pub symbol: String,
17    /// See [`Token`]
18    pub tokens: Vec<Token>,
19}
20
21#[derive(Debug, Serialize, Deserialize)]
22#[serde(rename_all = "camelCase")]
23pub struct Ticker {
24    pub symbol: String,
25    pub first_price: Decimal,
26    pub last_price: Decimal,
27    pub price_change: Decimal,
28    pub price_change_percent: Decimal,
29    pub high: Decimal,
30    pub low: Decimal,
31    pub volume: Decimal,
32    pub trades: String,
33}
34
35/// Sent by an exchange to indicate a change in the order book, such as the execution of a bid or ask.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37#[serde(rename_all = "camelCase")]
38pub struct TickerUpdate {
39    /// Event type
40    #[serde(rename = "e")]
41    pub event_type: String,
42
43    /// Event timestamp in microseconds
44    #[serde(rename = "E")]
45    pub event_time: i64,
46
47    /// Symbol
48    #[serde(rename = "s")]
49    pub symbol: String,
50
51    #[serde(rename = "a")]
52    pub ask_price: Decimal,
53
54    #[serde(rename = "A")]
55    pub ask_quantity: Decimal,
56
57    #[serde(rename = "b")]
58    pub bid_price: Decimal,
59
60    #[serde(rename = "B")]
61    pub bid_quantity: Decimal,
62
63    /// Update ID of event
64    #[serde(rename = "u")]
65    pub update_id: u64,
66
67    /// Engine timestamp in microseconds
68    #[serde(rename = "T")]
69    pub timestamp: u64,
70}
71
72/// A market is where two assets are exchanged. Most notably, in a `BTC/USDC` pair
73/// `BTC` is the base and `USDC` is the quote.
74#[derive(Debug, Serialize, Deserialize)]
75#[serde(rename_all = "camelCase")]
76pub struct Market {
77    /// The `Market` identifier.
78    pub symbol: String,
79    /// The base asset.
80    pub base_symbol: String,
81    /// The quote asset for the market.
82    pub quote_symbol: String,
83    /// The type of the market. Can be `SPOT`, `PERP`, `IPERP`, `DATED`, `PREDICTION`, `RFQ` or `MONAD`.
84    /// New market types may also be added in the future.
85    pub market_type: String,
86    /// See [`MarketFilters`].
87    pub filters: MarketFilters,
88}
89
90impl Market {
91    /// Returns the decimal places this market supports on the price.
92    /// We error if a price with more decimal places is provided.
93    /// `Price decimal too long`
94    pub const fn price_decimal_places(&self) -> u32 {
95        self.filters.price.tick_size.scale()
96    }
97
98    /// Returns the decimal places this market supports on the quantity.
99    /// if you provide a more precise quantity you will get an error
100    /// `Quantity decimal too long`
101    pub const fn quantity_decimal_places(&self) -> u32 {
102        self.filters.quantity.step_size.scale()
103    }
104}
105
106#[derive(Debug, Serialize, Deserialize)]
107#[serde(rename_all = "camelCase")]
108pub struct MarketFilters {
109    pub price: PriceFilter,
110    pub quantity: QuantityFilter,
111    pub leverage: Option<LeverageFilter>,
112}
113
114#[derive(Debug, Serialize, Deserialize)]
115#[serde(rename_all = "camelCase")]
116pub struct PriceFilter {
117    /// Minimum price the order book will allow.
118    pub min_price: Decimal,
119    /// Maximum price the order book will allow.
120    pub max_price: Option<Decimal>,
121    /// Price increment.
122    pub tick_size: Decimal,
123    /// Maximum allowed multiplier move from last active price.
124    pub max_multiplier: Option<Decimal>,
125    /// Minimum allowed multiplier move from last active price.
126    pub min_multiplier: Option<Decimal>,
127    /// Maximum allowed impact multiplier from last active price. This
128    /// determines how far above the best ask a market buy can penetrate.
129    pub max_impact_multiplier: Option<Decimal>,
130    /// Minimum allowed impact multiplier from last active price. This
131    /// determines how far below the best bid a market sell can penetrate.
132    pub min_impact_multiplier: Option<Decimal>,
133    /// Price band for futures markets. Restricts the price moving too far from
134    /// the mean mark price.
135    pub mean_mark_price_band: Option<PriceBandMarkPrice>,
136    /// Price band for futures markets. Restricts the premium moving too far
137    /// from the mean premium.
138    pub mean_premium_band: Option<PriceBandPremium>,
139    /// Maximum allowed multiplier move from last active price without
140    /// incurring an entry fee when borrowing for spot margin.
141    pub borrow_entry_fee_max_multiplier: Option<Decimal>,
142    /// Minimum allowed multiplier move from last active price without
143    /// incurring an entry fee when borrowing for spot margin.
144    pub borrow_entry_fee_min_multiplier: Option<Decimal>,
145}
146
147#[derive(Debug, Serialize, Deserialize)]
148#[serde(rename_all = "camelCase")]
149pub struct PriceBandMarkPrice {
150    /// Maximum allowed multiplier move from mean price.
151    pub max_multiplier: Decimal,
152    /// Minimum allowed multiplier move from mean price.
153    pub min_multiplier: Decimal,
154}
155
156#[derive(Debug, Serialize, Deserialize)]
157#[serde(rename_all = "camelCase")]
158pub struct PriceBandPremium {
159    /// Latest index price.
160    pub index_price: Option<Decimal>,
161    /// Maximum premium the order book will allow. This is constantly updated
162    /// based on the mean premium scaled by the `tolerance_pct`
163    pub max_premium_pct: Option<Decimal>,
164    /// Minimum premium the order book will allow. This is constantly updated
165    /// based on the mean premium scaled by the `tolerance_pct`
166    pub min_premium_pct: Option<Decimal>,
167    /// Maximum allowed deviation from the mean premium. E.g. if
168    /// `tolerance_pct` is 0.05 (5%), and the mean premium is 5%, then
169    /// orders will be prevented from being placed if the premium exceeds 10%.
170    /// User to calculate `min_premium_pct` and `max_premium_pct`.
171    pub tolerance_pct: Decimal,
172}
173
174#[derive(Debug, Serialize, Deserialize)]
175#[serde(rename_all = "camelCase")]
176pub struct QuantityFilter {
177    pub min_quantity: Decimal,
178    pub max_quantity: Option<Decimal>,
179    pub step_size: Decimal,
180}
181
182#[derive(Debug, Serialize, Deserialize)]
183#[serde(rename_all = "camelCase")]
184pub struct LeverageFilter {
185    pub min_leverage: Decimal,
186    pub max_leverage: Decimal,
187    pub step_size: Decimal,
188}
189
190#[derive(Debug, Serialize, Deserialize)]
191#[serde(rename_all = "camelCase")]
192pub struct Token {
193    pub blockchain: Blockchain,
194    pub contract_address: String,
195    pub deposit_enabled: bool,
196    pub display_name: String,
197    pub minimum_deposit: Decimal,
198    pub withdraw_enabled: bool,
199    pub minimum_withdrawal: Decimal,
200    pub maximum_withdrawal: Option<Decimal>,
201    pub withdrawal_fee: Decimal,
202}
203
204#[derive(Debug, Serialize, Deserialize, strum::AsRefStr)]
205pub enum OrderBookDepthLimit {
206    #[serde(rename = "5")]
207    #[strum(serialize = "5")]
208    Five,
209    #[serde(rename = "10")]
210    #[strum(serialize = "10")]
211    Ten,
212    #[serde(rename = "20")]
213    #[strum(serialize = "20")]
214    Twenty,
215    #[serde(rename = "50")]
216    #[strum(serialize = "50")]
217    Fifty,
218    #[serde(rename = "100")]
219    #[strum(serialize = "100")]
220    OneHundred,
221    #[serde(rename = "500")]
222    #[strum(serialize = "500")]
223    FiveHundred,
224    #[serde(rename = "1000")]
225    #[strum(serialize = "1000")]
226    OneThousand,
227}
228
229#[derive(Debug, Serialize, Deserialize)]
230#[serde(rename_all = "camelCase")]
231pub struct OrderBookDepth {
232    /// Resting limit orders on ask side, listed as price-quantity pairs
233    pub asks: Vec<(Decimal, Decimal)>,
234    /// Resting limit orders on bid side, listed as price-quantity pairs
235    pub bids: Vec<(Decimal, Decimal)>,
236    /// The id of the last update applied to this order book state
237    // The API currently returns i64 encoded as a string. This was likely done to work around
238    // JavaScript's inability to handle large integers using the Number type.
239    // We should change the API at some point to return an i64 instead of a string, but it's
240    // a breaking change, so just improving the client for now.
241    #[serde(deserialize_with = "deserialize_str_or_i64")]
242    pub last_update_id: i64,
243    /// Timestamp in microseconds
244    pub timestamp: i64,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
248#[serde(rename_all = "camelCase")]
249pub struct OrderBookDepthUpdate {
250    /// Event type
251    #[serde(rename = "e")]
252    pub event_type: String,
253
254    /// Event timestamp in microseconds
255    #[serde(rename = "E")]
256    pub event_time: i64,
257
258    /// Symbol
259    #[serde(rename = "s")]
260    pub symbol: String,
261
262    /// Engine timestamp in microseconds
263    #[serde(rename = "T")]
264    pub timestamp: i64,
265
266    /// First update ID in event
267    #[serde(rename = "U")]
268    pub first_update_id: i64,
269
270    /// Last update ID in event
271    #[serde(rename = "u")]
272    pub last_update_id: i64,
273
274    /// Asks
275    #[serde(rename = "a")]
276    pub asks: Vec<(Decimal, Decimal)>,
277
278    /// Bids
279    #[serde(rename = "b")]
280    pub bids: Vec<(Decimal, Decimal)>,
281}
282
283#[derive(Debug, Serialize, Deserialize)]
284#[serde(rename_all = "camelCase")]
285pub struct Kline {
286    pub start: String,
287    pub open: Option<Decimal>,
288    pub high: Option<Decimal>,
289    pub low: Option<Decimal>,
290    pub close: Option<Decimal>,
291    pub end: Option<String>,
292    pub volume: Decimal,
293    pub trades: String,
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct KlineUpdate {
298    /// Event type
299    #[serde(rename = "e")]
300    pub event_type: String,
301
302    /// Event timestamp in microseconds
303    #[serde(rename = "E")]
304    pub event_time: i64,
305
306    /// Symbol
307    #[serde(rename = "s")]
308    pub symbol: String,
309
310    /// K-Line start time in seconds
311    #[serde(rename = "t")]
312    pub start_time: i64,
313
314    /// K-Line end time in seconds
315    #[serde(rename = "T")]
316    pub end_time: i64,
317
318    /// Open price
319    #[serde(rename = "o")]
320    pub open_price: Decimal,
321
322    /// Close price
323    #[serde(rename = "c")]
324    pub close_price: Decimal,
325
326    /// High price
327    #[serde(rename = "h")]
328    pub high_price: Decimal,
329
330    /// Low price
331    #[serde(rename = "l")]
332    pub low_price: Decimal,
333
334    /// Base asset volume
335    #[serde(rename = "v")]
336    pub volume: Decimal,
337
338    /// Number of trades
339    #[serde(rename = "n")]
340    pub trades: u64,
341
342    /// Is this k-line closed?
343    #[serde(rename = "X")]
344    pub is_closed: bool,
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize)]
348#[serde(rename_all = "camelCase")]
349pub struct FundingRate {
350    pub symbol: String,
351    pub interval_end_timestamp: String,
352    pub funding_rate: Decimal,
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize)]
356#[serde(rename_all = "camelCase")]
357pub struct MarkPrice {
358    pub symbol: String,
359    pub funding_rate: Decimal,
360    pub index_price: Decimal,
361    pub mark_price: Decimal,
362    pub next_funding_timestamp: u64,
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize)]
366#[serde(rename_all = "camelCase")]
367pub struct MarkPriceUpdate {
368    /// Event type
369    #[serde(rename = "e")]
370    pub event_type: String,
371
372    /// Event timestamp in microseconds
373    #[serde(rename = "E")]
374    pub event_time: i64,
375
376    /// Symbol
377    #[serde(rename = "s")]
378    pub symbol: String,
379
380    /// Mark Price
381    #[serde(rename = "p")]
382    pub mark_price: Decimal,
383
384    /// Estimated funding rate
385    #[serde(rename = "f")]
386    pub funding_rate: Decimal,
387
388    /// Index Price
389    #[serde(rename = "i")]
390    pub index_price: Decimal,
391
392    /// Next funding timestamp in microseconds
393    #[serde(rename = "n")]
394    pub funding_timestamp: u64,
395
396    /// Engine timestamp in microseconds
397    #[serde(rename = "T")]
398    pub engine_timestamp: i64,
399}
400
401impl TryFrom<u32> for OrderBookDepthLimit {
402    type Error = &'static str;
403
404    fn try_from(value: u32) -> Result<Self, Self::Error> {
405        match value {
406            5 => Ok(OrderBookDepthLimit::Five),
407            10 => Ok(OrderBookDepthLimit::Ten),
408            20 => Ok(OrderBookDepthLimit::Twenty),
409            50 => Ok(OrderBookDepthLimit::Fifty),
410            100 => Ok(OrderBookDepthLimit::OneHundred),
411            500 => Ok(OrderBookDepthLimit::FiveHundred),
412            1000 => Ok(OrderBookDepthLimit::OneThousand),
413            _ => Err("Invalid OrderBookDepthLimit value"),
414        }
415    }
416}
417
418/// Deserializes a value that can be either a string or an i64 into an i64.
419fn deserialize_str_or_i64<'de, D>(deserializer: D) -> Result<i64, D::Error>
420where
421    D: Deserializer<'de>,
422{
423    use serde::de::Visitor;
424    use std::fmt;
425
426    struct StringOrI64Visitor;
427
428    impl<'de> Visitor<'de> for StringOrI64Visitor {
429        type Value = i64;
430
431        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
432            formatter.write_str("a string or an integer")
433        }
434
435        fn visit_str<E>(self, value: &str) -> Result<i64, E>
436        where
437            E: serde::de::Error,
438        {
439            value.parse().map_err(serde::de::Error::custom)
440        }
441
442        fn visit_i64<E>(self, value: i64) -> Result<i64, E>
443        where
444            E: serde::de::Error,
445        {
446            Ok(value)
447        }
448
449        fn visit_u64<E>(self, value: u64) -> Result<i64, E>
450        where
451            E: serde::de::Error,
452        {
453            i64::try_from(value).map_err(|_| serde::de::Error::custom("value too large"))
454        }
455    }
456
457    deserializer.deserialize_any(StringOrI64Visitor)
458}
459
460#[cfg(test)]
461mod test {
462    use super::*;
463    use rust_decimal_macros::dec;
464
465    fn get_test_market() -> Market {
466        Market {
467            symbol: "TEST_MARKET".to_string(),
468            base_symbol: "TEST".to_string(),
469            quote_symbol: "MARKET".to_string(),
470            market_type: "SPOT".to_string(),
471            filters: super::MarketFilters {
472                price: PriceFilter {
473                    min_price: dec!(0.0001),
474                    max_price: None,
475                    tick_size: dec!(0.0001),
476                    min_multiplier: Some(dec!(1.25)),
477                    max_multiplier: Some(dec!(0.75)),
478                    max_impact_multiplier: Some(dec!(1.05)),
479                    min_impact_multiplier: Some(dec!(0.95)),
480                    mean_mark_price_band: None,
481                    mean_premium_band: None,
482                    borrow_entry_fee_max_multiplier: None,
483                    borrow_entry_fee_min_multiplier: None,
484                },
485                quantity: QuantityFilter {
486                    min_quantity: dec!(0.01),
487                    max_quantity: None,
488                    step_size: dec!(0.01),
489                },
490                leverage: None,
491            },
492        }
493    }
494
495    #[test]
496    fn test_decimal_places_on_price_filters_4() {
497        let market = get_test_market();
498        assert_eq!(market.price_decimal_places(), 4);
499    }
500
501    #[test]
502    fn test_decimal_places_on_quantity_filters() {
503        let market = get_test_market();
504        assert_eq!(market.quantity_decimal_places(), 2);
505    }
506
507    #[test]
508    fn test_mark_price_update_parse() {
509        let data = r#"
510{
511	"E": 1747291031914525,
512	"T": 1747291031910025,
513	"e": "markPrice",
514	"f": "-0.0000039641039274236048482914",
515	"i": "173.44031179",
516	"n": 1747296000000,
517	"p": "173.35998175",
518	"s": "SOL_USDC_PERP"
519}
520        "#;
521
522        let mark_price_update: MarkPriceUpdate = serde_json::from_str(data).unwrap();
523        assert_eq!(mark_price_update.symbol, "SOL_USDC_PERP".to_string());
524        assert_eq!(
525            mark_price_update.funding_rate,
526            dec!(-0.0000039641039274236048482914)
527        );
528        assert_eq!(mark_price_update.mark_price, dec!(173.35998175));
529    }
530
531    #[test]
532    fn test_kline_update_parse() {
533        let data = r#"
534{
535  "e": "kline",
536  "E": 1694687692980000,
537  "s": "SOL_USD",
538  "t": 123400000,
539  "T": 123460000,
540  "o": "18.75",
541  "c": "19.25",
542  "h": "19.80",
543  "l": "18.50",
544  "v": "32123",
545  "n": 93828,
546  "X": false
547}
548        "#;
549
550        let kline_update: KlineUpdate = serde_json::from_str(data).unwrap();
551        assert_eq!(kline_update.symbol, "SOL_USD".to_string());
552        assert_eq!(kline_update.start_time, 123400000);
553        assert_eq!(kline_update.open_price, dec!(18.75));
554    }
555
556    #[test]
557    fn test_order_book_depth_last_update_id_as_string() {
558        let data = r#"
559{
560  "asks": [["18.70", "0.000"]],
561  "bids": [["18.67", "0.832"]],
562  "lastUpdateId": "94978271",
563  "timestamp": 1694687965941000
564}
565        "#;
566
567        let depth: OrderBookDepth = serde_json::from_str(data).unwrap();
568        assert_eq!(depth.last_update_id, 94978271);
569    }
570
571    #[test]
572    fn test_order_book_depth_last_update_id_as_i64() {
573        let data = r#"
574{
575  "asks": [["18.70", "0.000"]],
576  "bids": [["18.67", "0.832"]],
577  "lastUpdateId": 94978271,
578  "timestamp": 1694687965941000
579}
580        "#;
581
582        let depth: OrderBookDepth = serde_json::from_str(data).unwrap();
583        assert_eq!(depth.last_update_id, 94978271);
584    }
585}