deribit_base/model/
instrument.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 21/7/25
5******************************************************************************/
6use pretty_simple_display::{DebugPretty, DisplaySimple};
7use serde::{Deserialize, Serialize};
8
9/// Instrument kind enumeration
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "lowercase")]
12pub enum InstrumentKind {
13    /// Future contract
14    Future,
15    /// Option contract
16    Option,
17    /// Spot trading
18    Spot,
19    /// Future combo
20    #[serde(rename = "future_combo")]
21    FutureCombo,
22    /// Option combo
23    #[serde(rename = "option_combo")]
24    OptionCombo,
25}
26
27/// Option type enumeration
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "lowercase")]
30pub enum OptionType {
31    /// Call option
32    Call,
33    /// Put option
34    Put,
35}
36
37/// Instrument type enumeration
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(rename_all = "lowercase")]
40pub enum InstrumentType {
41    /// Linear instrument
42    Linear,
43    /// Reversed instrument
44    Reversed,
45}
46
47/// Instrument information
48#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
49pub struct Instrument {
50    /// Instrument name (e.g., "BTC-PERPETUAL", "ETH-25JUL25-3000-C")
51    pub instrument_name: String,
52    /// Price index used for mark price calculation
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub price_index: Option<String>,
55    /// Instrument kind
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub kind: Option<InstrumentKind>,
58    /// Base currency
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub currency: Option<String>,
61    /// Whether the instrument is active for trading
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub is_active: Option<bool>,
64    /// Expiration timestamp (None for perpetuals)
65    pub expiration_timestamp: Option<i64>,
66    /// Strike price (for options)
67    pub strike: Option<f64>,
68    /// Option type (call/put, for options only)
69    pub option_type: Option<OptionType>,
70    /// Minimum price movement
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub tick_size: Option<f64>,
73    /// Minimum trade amount
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub min_trade_amount: Option<f64>,
76    /// Contract size
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub contract_size: Option<f64>,
79    /// Settlement period
80    pub settlement_period: Option<String>,
81    /// Instrument type (linear/reversed)
82    pub instrument_type: Option<InstrumentType>,
83    /// Quote currency
84    pub quote_currency: Option<String>,
85    /// Settlement currency
86    pub settlement_currency: Option<String>,
87    /// Creation timestamp
88    pub creation_timestamp: Option<i64>,
89    /// Maximum leverage
90    pub max_leverage: Option<f64>,
91    /// Maker commission rate
92    pub maker_commission: Option<f64>,
93    /// Taker commission rate
94    pub taker_commission: Option<f64>,
95    /// Unique instrument identifier
96    pub instrument_id: Option<u32>,
97    /// Base currency for the instrument
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub base_currency: Option<String>,
100    /// Counter currency for the instrument
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub counter_currency: Option<String>,
103}
104
105impl Instrument {
106    /// Check if the instrument is a perpetual contract
107    pub fn is_perpetual(&self) -> bool {
108        self.expiration_timestamp.is_none()
109            && self
110                .kind
111                .as_ref()
112                .is_some_and(|k| matches!(k, InstrumentKind::Future))
113    }
114
115    /// Check if the instrument is an option
116    pub fn is_option(&self) -> bool {
117        self.kind
118            .as_ref()
119            .is_some_and(|k| matches!(k, InstrumentKind::Option | InstrumentKind::OptionCombo))
120    }
121
122    /// Check if the instrument is a future
123    pub fn is_future(&self) -> bool {
124        self.kind
125            .as_ref()
126            .is_some_and(|k| matches!(k, InstrumentKind::Future | InstrumentKind::FutureCombo))
127    }
128
129    /// Check if the instrument is a spot
130    pub fn is_spot(&self) -> bool {
131        self.kind
132            .as_ref()
133            .is_some_and(|k| matches!(k, InstrumentKind::Spot))
134    }
135}
136
137/// Index data
138#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
139pub struct IndexData {
140    /// BTC component (optional)
141    pub btc: Option<f64>,
142    /// ETH component (optional)
143    pub eth: Option<f64>,
144    /// USDC component (optional)
145    pub usdc: Option<f64>,
146    /// USDT component (optional)
147    pub usdt: Option<f64>,
148    /// EURR component (optional)
149    pub eurr: Option<f64>,
150    /// EDP (Estimated Delivery Price)
151    pub edp: f64,
152}
153
154/// Index price data
155#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
156pub struct IndexPriceData {
157    /// Current index price
158    pub index_price: f64,
159    /// Estimated delivery price
160    pub estimated_delivery_price: f64,
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    fn create_test_instrument() -> Instrument {
168        Instrument {
169            instrument_name: "BTC-PERPETUAL".to_string(),
170            price_index: Some("btc_usd".to_string()),
171            kind: Some(InstrumentKind::Future),
172            currency: Some("BTC".to_string()),
173            is_active: Some(true),
174            expiration_timestamp: None, // Perpetual
175            strike: None,
176            option_type: None,
177            tick_size: Some(0.5),
178            min_trade_amount: Some(10.0),
179            contract_size: Some(1.0),
180            settlement_period: Some("perpetual".to_string()),
181            instrument_type: Some(InstrumentType::Linear),
182            quote_currency: Some("USD".to_string()),
183            settlement_currency: Some("BTC".to_string()),
184            creation_timestamp: Some(1640995200000),
185            max_leverage: Some(100.0),
186            maker_commission: Some(0.0001),
187            taker_commission: Some(0.0005),
188            instrument_id: Some(12345),
189            base_currency: Some("BTC".to_string()),
190            counter_currency: Some("USD".to_string()),
191        }
192    }
193
194    fn create_test_option() -> Instrument {
195        Instrument {
196            instrument_name: "BTC-25DEC25-50000-C".to_string(),
197            price_index: Some("btc_usd".to_string()),
198            kind: Some(InstrumentKind::Option),
199            currency: Some("BTC".to_string()),
200            is_active: Some(true),
201            expiration_timestamp: Some(1735084800000),
202            strike: Some(50000.0),
203            option_type: Some(OptionType::Call),
204            tick_size: Some(0.0005),
205            min_trade_amount: Some(0.1),
206            contract_size: Some(1.0),
207            settlement_period: Some("week".to_string()),
208            instrument_type: Some(InstrumentType::Linear),
209            quote_currency: Some("USD".to_string()),
210            settlement_currency: Some("BTC".to_string()),
211            creation_timestamp: Some(1640995200000),
212            max_leverage: Some(10.0),
213            maker_commission: Some(0.0003),
214            taker_commission: Some(0.0003),
215            instrument_id: Some(67890),
216            base_currency: Some("BTC".to_string()),
217            counter_currency: Some("USD".to_string()),
218        }
219    }
220
221    #[test]
222    fn test_instrument_is_perpetual() {
223        let perpetual = create_test_instrument();
224        assert!(perpetual.is_perpetual());
225
226        let option = create_test_option();
227        assert!(!option.is_perpetual());
228
229        let mut future_with_expiry = create_test_instrument();
230        future_with_expiry.expiration_timestamp = Some(1735084800000);
231        assert!(!future_with_expiry.is_perpetual());
232    }
233
234    #[test]
235    fn test_instrument_is_option() {
236        let option = create_test_option();
237        assert!(option.is_option());
238
239        let perpetual = create_test_instrument();
240        assert!(!perpetual.is_option());
241
242        let mut option_combo = create_test_option();
243        option_combo.kind = Some(InstrumentKind::OptionCombo);
244        assert!(option_combo.is_option());
245    }
246
247    #[test]
248    fn test_instrument_is_future() {
249        let future = create_test_instrument();
250        assert!(future.is_future());
251
252        let option = create_test_option();
253        assert!(!option.is_future());
254
255        let mut future_combo = create_test_instrument();
256        future_combo.kind = Some(InstrumentKind::FutureCombo);
257        assert!(future_combo.is_future());
258    }
259
260    #[test]
261    fn test_instrument_is_spot() {
262        let mut spot = create_test_instrument();
263        spot.kind = Some(InstrumentKind::Spot);
264        assert!(spot.is_spot());
265
266        let future = create_test_instrument();
267        assert!(!future.is_spot());
268
269        let option = create_test_option();
270        assert!(!option.is_spot());
271    }
272
273    #[test]
274    fn test_instrument_kind_serialization() {
275        assert_eq!(
276            serde_json::to_string(&InstrumentKind::Future).unwrap(),
277            "\"future\""
278        );
279        assert_eq!(
280            serde_json::to_string(&InstrumentKind::Option).unwrap(),
281            "\"option\""
282        );
283        assert_eq!(
284            serde_json::to_string(&InstrumentKind::Spot).unwrap(),
285            "\"spot\""
286        );
287        assert_eq!(
288            serde_json::to_string(&InstrumentKind::FutureCombo).unwrap(),
289            "\"future_combo\""
290        );
291        assert_eq!(
292            serde_json::to_string(&InstrumentKind::OptionCombo).unwrap(),
293            "\"option_combo\""
294        );
295    }
296
297    #[test]
298    fn test_option_type_serialization() {
299        assert_eq!(
300            serde_json::to_string(&OptionType::Call).unwrap(),
301            "\"call\""
302        );
303        assert_eq!(serde_json::to_string(&OptionType::Put).unwrap(), "\"put\"");
304    }
305
306    #[test]
307    fn test_instrument_type_serialization() {
308        assert_eq!(
309            serde_json::to_string(&InstrumentType::Linear).unwrap(),
310            "\"linear\""
311        );
312        assert_eq!(
313            serde_json::to_string(&InstrumentType::Reversed).unwrap(),
314            "\"reversed\""
315        );
316    }
317
318    #[test]
319    fn test_instrument_serialization() {
320        let instrument = create_test_instrument();
321        let json = serde_json::to_string(&instrument).unwrap();
322        let deserialized: Instrument = serde_json::from_str(&json).unwrap();
323        assert_eq!(instrument.instrument_name, deserialized.instrument_name);
324        assert_eq!(instrument.kind, deserialized.kind);
325    }
326
327    #[test]
328    fn test_index_data_creation() {
329        let index_data = IndexData {
330            btc: Some(0.5),
331            eth: Some(0.3),
332            usdc: Some(0.1),
333            usdt: Some(0.05),
334            eurr: Some(0.05),
335            edp: 50000.0,
336        };
337
338        assert_eq!(index_data.btc, Some(0.5));
339        assert_eq!(index_data.edp, 50000.0);
340    }
341
342    #[test]
343    fn test_index_price_data_creation() {
344        let index_price_data = IndexPriceData {
345            index_price: 50000.0,
346            estimated_delivery_price: 50100.0,
347        };
348
349        assert_eq!(index_price_data.index_price, 50000.0);
350        assert_eq!(index_price_data.estimated_delivery_price, 50100.0);
351    }
352
353    #[test]
354    fn test_debug_and_display_implementations() {
355        let instrument = create_test_instrument();
356        let debug_str = format!("{:?}", instrument);
357        let display_str = format!("{}", instrument);
358
359        assert!(debug_str.contains("BTC-PERPETUAL"));
360        assert!(display_str.contains("BTC-PERPETUAL"));
361    }
362}