Skip to main content

brokapi_bybit/map/
instrument.rs

1//! Instruments: the directory entry, the trading rules and the status.
2
3use brokapi_bybit_protocol::http::models::InstrumentInfo;
4use brokapi_core::{
5    Instrument, InstrumentId, InstrumentKind, Money, Price, Qty, TradingAvailability,
6    TradingParams, TradingStatus, UnixNanos,
7};
8
9use crate::util::instrument_id;
10
11/// The kind of an instrument by the contract type of the exchange: a perpetual is a [`InstrumentKind::Perpetual`],
12/// a dated contract a [`InstrumentKind::Future`].
13pub fn kind_of(contract_type: &str) -> InstrumentKind {
14    match contract_type {
15        "LinearPerpetual" | "InversePerpetual" => InstrumentKind::Perpetual,
16        "LinearFutures" | "InverseFutures" => InstrumentKind::Future,
17        _ => InstrumentKind::Other,
18    }
19}
20
21fn id_of(info: &InstrumentInfo, category: &str) -> InstrumentId {
22    instrument_id(&info.symbol, category)
23}
24
25/// The directory entry.
26pub fn instrument(info: &InstrumentInfo, category: &str) -> Instrument {
27    Instrument {
28        id: id_of(info, category),
29        name: if info.display_name.is_empty() {
30            info.symbol.clone()
31        } else {
32            info.display_name.clone()
33        },
34        isin: None,
35        kind: kind_of(&info.contract_type),
36        currency: if info.settle_coin.is_empty() {
37            info.quote_coin.clone()
38        } else {
39            info.settle_coin.clone()
40        },
41        exchange: "BYBIT".to_string(),
42    }
43}
44
45/// The trading rules.
46///
47/// The quantity is in units of the base coin (`0.001` BTC). `lot_size` is the quantity step and `min_qty` the smallest
48/// order, so a quantity is valid when it is a whole number of steps and not below the minimum. The value of one price
49/// step is the step itself: the contract is linear (one unit of the base coin pays one unit of the settle coin per unit
50/// of price), so a step of `0.10` moves one unit by `0.10` USDT.
51pub fn trading_params(info: &InstrumentInfo, category: &str) -> TradingParams {
52    TradingParams {
53        instrument: id_of(info, category),
54        lot_size: Qty::new(info.lot_size_filter.qty_step),
55        min_qty: Qty::new(info.lot_size_filter.min_order_qty),
56        price_step: Price::new(info.price_filter.tick_size),
57        price_step_value: Money::new(info.price_filter.tick_size),
58        currency: if info.settle_coin.is_empty() {
59            info.quote_coin.clone()
60        } else {
61            info.settle_coin.clone()
62        },
63    }
64}
65
66/// The status: `Trading` is open, the contracts that wait to open or are delisted are closed.
67pub fn trading_status(
68    info: &InstrumentInfo,
69    category: &str,
70    now_ns: Option<UnixNanos>,
71) -> TradingStatus {
72    let (availability, reason) = match info.status.as_str() {
73        "Trading" => (TradingAvailability::Open, None),
74        "PreLaunch" | "PendingOpen" | "Delivering" | "Closed" => {
75            (TradingAvailability::Closed, Some(info.status.clone()))
76        }
77        "" => (TradingAvailability::Unknown, None),
78        other => (TradingAvailability::Unknown, Some(other.to_string())),
79    };
80    TradingStatus {
81        instrument: id_of(info, category),
82        availability,
83        reason,
84        ts: now_ns,
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    fn btc() -> InstrumentInfo {
93        serde_json::from_str(include_str!("../../tests/fixtures/instrument_btcusdt.json"))
94            .map(|v: serde_json::Value| {
95                serde_json::from_value(v["result"]["list"][0].clone()).unwrap()
96            })
97            .unwrap()
98    }
99
100    #[test]
101    fn a_perpetual_is_a_perpetual_and_the_rules_come_from_the_filters() {
102        let info = btc();
103        let i = instrument(&info, "linear");
104        assert_eq!(i.kind, InstrumentKind::Perpetual);
105        assert_eq!(i.id.key.display(), "BTCUSDT@linear");
106        assert_eq!(
107            (i.currency.as_str(), i.exchange.as_str()),
108            ("USDT", "BYBIT")
109        );
110        let p = trading_params(&info, "linear");
111        assert_eq!(p.lot_size.to_string(), "0.001");
112        assert_eq!(p.min_qty.to_string(), "0.001");
113        assert_eq!(p.price_step.to_string(), "0.10");
114        assert_eq!(
115            trading_status(&info, "linear", None).availability,
116            TradingAvailability::Open
117        );
118    }
119
120    #[test]
121    fn contract_types_and_statuses() {
122        assert_eq!(kind_of("LinearFutures"), InstrumentKind::Future);
123        assert_eq!(kind_of("Strange"), InstrumentKind::Other);
124        let mut info = btc();
125        info.status = "PreLaunch".into();
126        let s = trading_status(&info, "linear", None);
127        assert_eq!(
128            (s.availability, s.reason.as_deref()),
129            (TradingAvailability::Closed, Some("PreLaunch"))
130        );
131        info.status = "Odd".into();
132        assert_eq!(
133            trading_status(&info, "linear", None).availability,
134            TradingAvailability::Unknown
135        );
136    }
137}