brokapi_bybit/map/
instrument.rs1use 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
11pub 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
25pub 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
45pub 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
66pub 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}