deribit_base/model/
extended_market_data.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 21/7/25
5******************************************************************************/
6
7use crate::{impl_json_debug_pretty, impl_json_display};
8use serde::{Deserialize, Serialize};
9
10/// Withdrawal priority information
11#[derive(Clone, PartialEq, Serialize, Deserialize)]
12pub struct WithdrawalPriority {
13    /// Priority name (e.g., "very_low", "low", "medium", "high", "very_high")
14    pub name: String,
15    /// Priority value (fee multiplier)
16    pub value: f64,
17}
18
19impl WithdrawalPriority {
20    /// Create a new withdrawal priority
21    pub fn new(name: String, value: f64) -> Self {
22        Self { name, value }
23    }
24
25    /// Create a very low priority
26    pub fn very_low() -> Self {
27        Self::new("very_low".to_string(), 0.15)
28    }
29
30    /// Create a low priority
31    pub fn low() -> Self {
32        Self::new("low".to_string(), 0.5)
33    }
34
35    /// Create a medium priority
36    pub fn medium() -> Self {
37        Self::new("medium".to_string(), 1.0)
38    }
39
40    /// Create a high priority
41    pub fn high() -> Self {
42        Self::new("high".to_string(), 1.2)
43    }
44
45    /// Create a very high priority
46    pub fn very_high() -> Self {
47        Self::new("very_high".to_string(), 1.5)
48    }
49}
50
51impl_json_display!(WithdrawalPriority);
52impl_json_debug_pretty!(WithdrawalPriority);
53
54/// Currency information and configuration
55#[derive(Clone, PartialEq, Serialize, Deserialize)]
56pub struct CurrencyInfo {
57    /// Coin type identifier (e.g., "BITCOIN", "ETHEREUM")
58    pub coin_type: String,
59    /// Currency code
60    pub currency: String,
61    /// Full currency name
62    pub currency_long: String,
63    /// Fee precision (decimal places)
64    pub fee_precision: i32,
65    /// Minimum confirmations required
66    pub min_confirmations: i32,
67    /// Minimum withdrawal fee
68    pub min_withdrawal_fee: f64,
69    /// Standard withdrawal fee
70    pub withdrawal_fee: f64,
71    /// Available withdrawal priorities
72    pub withdrawal_priorities: Vec<WithdrawalPriority>,
73    /// Whether the currency is disabled
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub disabled: Option<bool>,
76    /// Minimum deposit amount
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub min_deposit_amount: Option<f64>,
79    /// Maximum withdrawal amount
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub max_withdrawal_amount: Option<f64>,
82}
83
84impl CurrencyInfo {
85    /// Create new currency info
86    pub fn new(
87        coin_type: String,
88        currency: String,
89        currency_long: String,
90        fee_precision: i32,
91        min_confirmations: i32,
92        min_withdrawal_fee: f64,
93        withdrawal_fee: f64,
94    ) -> Self {
95        Self {
96            coin_type,
97            currency,
98            currency_long,
99            fee_precision,
100            min_confirmations,
101            min_withdrawal_fee,
102            withdrawal_fee,
103            withdrawal_priorities: Vec::new(),
104            disabled: None,
105            min_deposit_amount: None,
106            max_withdrawal_amount: None,
107        }
108    }
109
110    /// Add withdrawal priority
111    pub fn add_priority(&mut self, priority: WithdrawalPriority) {
112        self.withdrawal_priorities.push(priority);
113    }
114
115    /// Set disabled status
116    pub fn with_disabled(mut self, disabled: bool) -> Self {
117        self.disabled = Some(disabled);
118        self
119    }
120
121    /// Set deposit limits
122    pub fn with_deposit_limit(mut self, min_amount: f64) -> Self {
123        self.min_deposit_amount = Some(min_amount);
124        self
125    }
126
127    /// Set withdrawal limits
128    pub fn with_withdrawal_limit(mut self, max_amount: f64) -> Self {
129        self.max_withdrawal_amount = Some(max_amount);
130        self
131    }
132
133    /// Check if currency is enabled
134    pub fn is_enabled(&self) -> bool {
135        !self.disabled.unwrap_or(false)
136    }
137
138    /// Get priority by name
139    pub fn get_priority(&self, name: &str) -> Option<&WithdrawalPriority> {
140        self.withdrawal_priorities.iter().find(|p| p.name == name)
141    }
142
143    /// Get highest priority
144    pub fn highest_priority(&self) -> Option<&WithdrawalPriority> {
145        self.withdrawal_priorities
146            .iter()
147            .max_by(|a, b| a.value.partial_cmp(&b.value).unwrap())
148    }
149
150    /// Get lowest priority
151    pub fn lowest_priority(&self) -> Option<&WithdrawalPriority> {
152        self.withdrawal_priorities
153            .iter()
154            .min_by(|a, b| a.value.partial_cmp(&b.value).unwrap())
155    }
156}
157
158impl_json_display!(CurrencyInfo);
159impl_json_debug_pretty!(CurrencyInfo);
160
161/// Index price information
162#[derive(Clone, PartialEq, Serialize, Deserialize)]
163pub struct IndexPrice {
164    /// Estimated delivery price
165    pub estimated_delivery_price: f64,
166    /// Current index price
167    pub index_price: f64,
168    /// Timestamp (milliseconds since Unix epoch)
169    pub timestamp: i64,
170    /// Index name
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub index_name: Option<String>,
173}
174
175impl IndexPrice {
176    /// Create new index price
177    pub fn new(estimated_delivery_price: f64, index_price: f64, timestamp: i64) -> Self {
178        Self {
179            estimated_delivery_price,
180            index_price,
181            timestamp,
182            index_name: None,
183        }
184    }
185
186    /// Set index name
187    pub fn with_name(mut self, name: String) -> Self {
188        self.index_name = Some(name);
189        self
190    }
191
192    /// Get price difference
193    pub fn price_difference(&self) -> f64 {
194        self.estimated_delivery_price - self.index_price
195    }
196
197    /// Get price difference percentage
198    pub fn price_difference_percentage(&self) -> f64 {
199        if self.index_price != 0.0 {
200            (self.price_difference() / self.index_price) * 100.0
201        } else {
202            0.0
203        }
204    }
205}
206
207impl_json_display!(IndexPrice);
208impl_json_debug_pretty!(IndexPrice);
209
210/// Funding rate information
211#[derive(Clone, PartialEq, Serialize, Deserialize)]
212pub struct FundingRate {
213    /// Timestamp (milliseconds since Unix epoch)
214    pub timestamp: i64,
215    /// Index name
216    pub index_name: String,
217    /// Interest rate
218    pub interest_rate: f64,
219    /// 8-hour interest rate
220    pub interest_8h: f64,
221    /// Current funding rate
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub current_funding: Option<f64>,
224    /// Next funding timestamp
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub next_funding_timestamp: Option<i64>,
227}
228
229impl FundingRate {
230    /// Create new funding rate
231    pub fn new(timestamp: i64, index_name: String, interest_rate: f64, interest_8h: f64) -> Self {
232        Self {
233            timestamp,
234            index_name,
235            interest_rate,
236            interest_8h,
237            current_funding: None,
238            next_funding_timestamp: None,
239        }
240    }
241
242    /// Set current funding
243    pub fn with_current_funding(mut self, funding: f64) -> Self {
244        self.current_funding = Some(funding);
245        self
246    }
247
248    /// Set next funding timestamp
249    pub fn with_next_funding(mut self, timestamp: i64) -> Self {
250        self.next_funding_timestamp = Some(timestamp);
251        self
252    }
253
254    /// Calculate annualized rate
255    pub fn annualized_rate(&self) -> f64 {
256        self.interest_rate * 365.0 * 3.0 // 3 times per day (8h intervals)
257    }
258
259    /// Check if funding is positive (longs pay shorts)
260    pub fn is_positive(&self) -> bool {
261        self.current_funding.unwrap_or(0.0) > 0.0
262    }
263
264    /// Check if funding is negative (shorts pay longs)
265    pub fn is_negative(&self) -> bool {
266        self.current_funding.unwrap_or(0.0) < 0.0
267    }
268}
269
270impl_json_display!(FundingRate);
271impl_json_debug_pretty!(FundingRate);
272
273/// Historical volatility information
274#[derive(Clone, PartialEq, Serialize, Deserialize)]
275pub struct HistoricalVolatility {
276    /// Timestamp (milliseconds since Unix epoch)
277    pub timestamp: i64,
278    /// Volatility value (percentage)
279    pub volatility: f64,
280    /// Period in days
281    pub period_days: i32,
282    /// Underlying asset
283    pub underlying: String,
284}
285
286impl HistoricalVolatility {
287    /// Create new historical volatility
288    pub fn new(timestamp: i64, volatility: f64, period_days: i32, underlying: String) -> Self {
289        Self {
290            timestamp,
291            volatility,
292            period_days,
293            underlying,
294        }
295    }
296
297    /// Convert to decimal form
298    pub fn as_decimal(&self) -> f64 {
299        self.volatility / 100.0
300    }
301
302    /// Annualize volatility if needed
303    pub fn annualized(&self) -> f64 {
304        if self.period_days == 365 {
305            self.volatility
306        } else {
307            self.volatility * (365.0 / self.period_days as f64).sqrt()
308        }
309    }
310}
311
312impl_json_display!(HistoricalVolatility);
313impl_json_debug_pretty!(HistoricalVolatility);
314
315/// Market statistics
316#[derive(Clone, PartialEq, Serialize, Deserialize)]
317pub struct MarketStatistics {
318    /// Currency
319    pub currency: String,
320    /// Total volume 24h
321    pub volume_24h: f64,
322    /// Total volume 30d
323    pub volume_30d: f64,
324    /// Volume in USD 24h
325    pub volume_usd_24h: f64,
326    /// Volume in USD 30d
327    pub volume_usd_30d: f64,
328    /// Number of trades 24h
329    pub trades_count_24h: i64,
330    /// Number of trades 30d
331    pub trades_count_30d: i64,
332    /// Open interest
333    pub open_interest: f64,
334    /// Timestamp (milliseconds since Unix epoch)
335    pub timestamp: i64,
336}
337
338impl MarketStatistics {
339    /// Create new market statistics
340    pub fn new(currency: String, timestamp: i64) -> Self {
341        Self {
342            currency,
343            volume_24h: 0.0,
344            volume_30d: 0.0,
345            volume_usd_24h: 0.0,
346            volume_usd_30d: 0.0,
347            trades_count_24h: 0,
348            trades_count_30d: 0,
349            open_interest: 0.0,
350            timestamp,
351        }
352    }
353
354    /// Set volume data
355    pub fn with_volume(
356        mut self,
357        vol_24h: f64,
358        vol_30d: f64,
359        vol_usd_24h: f64,
360        vol_usd_30d: f64,
361    ) -> Self {
362        self.volume_24h = vol_24h;
363        self.volume_30d = vol_30d;
364        self.volume_usd_24h = vol_usd_24h;
365        self.volume_usd_30d = vol_usd_30d;
366        self
367    }
368
369    /// Set trade counts
370    pub fn with_trades(mut self, trades_24h: i64, trades_30d: i64) -> Self {
371        self.trades_count_24h = trades_24h;
372        self.trades_count_30d = trades_30d;
373        self
374    }
375
376    /// Set open interest
377    pub fn with_open_interest(mut self, oi: f64) -> Self {
378        self.open_interest = oi;
379        self
380    }
381
382    /// Calculate average trade size 24h
383    pub fn avg_trade_size_24h(&self) -> f64 {
384        if self.trades_count_24h > 0 {
385            self.volume_24h / self.trades_count_24h as f64
386        } else {
387            0.0
388        }
389    }
390
391    /// Calculate average trade size 30d
392    pub fn avg_trade_size_30d(&self) -> f64 {
393        if self.trades_count_30d > 0 {
394            self.volume_30d / self.trades_count_30d as f64
395        } else {
396            0.0
397        }
398    }
399
400    /// Calculate volume growth (30d vs 24h annualized)
401    pub fn volume_growth_rate(&self) -> f64 {
402        let daily_30d = self.volume_30d / 30.0;
403        if daily_30d > 0.0 {
404            ((self.volume_24h / daily_30d) - 1.0) * 100.0
405        } else {
406            0.0
407        }
408    }
409}
410
411impl_json_display!(MarketStatistics);
412impl_json_debug_pretty!(MarketStatistics);
413
414/// Collection of currency information
415#[derive(Clone, PartialEq, Serialize, Deserialize)]
416pub struct CurrencyInfoCollection {
417    /// List of currency information
418    pub currencies: Vec<CurrencyInfo>,
419}
420
421impl CurrencyInfoCollection {
422    /// Create new collection
423    pub fn new() -> Self {
424        Self {
425            currencies: Vec::new(),
426        }
427    }
428
429    /// Add currency info
430    pub fn add(&mut self, info: CurrencyInfo) {
431        self.currencies.push(info);
432    }
433
434    /// Get currency info by currency
435    pub fn get(&self, currency: String) -> Option<&CurrencyInfo> {
436        self.currencies.iter().find(|c| c.currency == currency)
437    }
438
439    /// Get enabled currencies
440    pub fn enabled(&self) -> Vec<&CurrencyInfo> {
441        self.currencies.iter().filter(|c| c.is_enabled()).collect()
442    }
443
444    /// Get currencies with withdrawal support
445    pub fn with_withdrawal(&self) -> Vec<&CurrencyInfo> {
446        self.currencies
447            .iter()
448            .filter(|c| !c.withdrawal_priorities.is_empty())
449            .collect()
450    }
451}
452
453impl Default for CurrencyInfoCollection {
454    fn default() -> Self {
455        Self::new()
456    }
457}
458
459impl_json_display!(CurrencyInfoCollection);
460impl_json_debug_pretty!(CurrencyInfoCollection);
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465
466    #[test]
467    fn test_withdrawal_priority() {
468        let priority = WithdrawalPriority::very_high();
469        assert_eq!(priority.name, "very_high");
470        assert_eq!(priority.value, 1.5);
471    }
472
473    #[test]
474    fn test_currency_info() {
475        let mut info = CurrencyInfo::new(
476            "BITCOIN".to_string(),
477            "BTC".to_string(),
478            "Bitcoin".to_string(),
479            4,
480            1,
481            0.0001,
482            0.0005,
483        );
484
485        info.add_priority(WithdrawalPriority::low());
486        info.add_priority(WithdrawalPriority::high());
487
488        assert!(info.is_enabled());
489        assert_eq!(info.withdrawal_priorities.len(), 2);
490        assert!(info.get_priority("low").is_some());
491        assert_eq!(info.highest_priority().unwrap().name, "high");
492        assert_eq!(info.lowest_priority().unwrap().name, "low");
493    }
494
495    #[test]
496    fn test_index_price() {
497        let index =
498            IndexPrice::new(45000.0, 44950.0, 1640995200000).with_name("BTC-USD".to_string());
499
500        assert_eq!(index.price_difference(), 50.0);
501        assert!((index.price_difference_percentage() - 0.1112).abs() < 0.001);
502    }
503
504    #[test]
505    fn test_funding_rate() {
506        let funding = FundingRate::new(1640995200000, "BTC-PERPETUAL".to_string(), 0.0001, 0.0008)
507            .with_current_funding(0.0002);
508
509        assert!(funding.is_positive());
510        assert!(!funding.is_negative());
511        assert_eq!(funding.annualized_rate(), 0.0001 * 365.0 * 3.0);
512    }
513
514    #[test]
515    fn test_historical_volatility() {
516        let vol = HistoricalVolatility::new(1640995200000, 80.0, 30, "BTC".to_string());
517
518        assert_eq!(vol.as_decimal(), 0.8);
519        let annualized = vol.annualized();
520        assert!((annualized - 80.0 * (365.0f64 / 30.0f64).sqrt()).abs() < 0.001);
521    }
522
523    #[test]
524    fn test_market_statistics() {
525        let stats = MarketStatistics::new("BTC".to_string(), 1640995200000)
526            .with_volume(1000.0, 30000.0, 45000000.0, 1350000000.0)
527            .with_trades(500, 15000)
528            .with_open_interest(5000000.0);
529
530        assert_eq!(stats.avg_trade_size_24h(), 2.0);
531        assert_eq!(stats.avg_trade_size_30d(), 2.0);
532
533        let growth = stats.volume_growth_rate();
534        assert!(growth.abs() < 0.001); // Should be close to 0% since volumes are proportional
535    }
536
537    #[test]
538    fn test_currency_info_collection() {
539        let mut collection = CurrencyInfoCollection::new();
540
541        let btc_info = CurrencyInfo::new(
542            "BITCOIN".to_string(),
543            "BTC".to_string(),
544            "Bitcoin".to_string(),
545            4,
546            1,
547            0.0001,
548            0.0005,
549        );
550
551        collection.add(btc_info);
552
553        assert_eq!(collection.currencies.len(), 1);
554        assert!(collection.get("BTC".to_string()).is_some());
555        assert_eq!(collection.enabled().len(), 1);
556    }
557
558    #[test]
559    fn test_serde() {
560        let info = CurrencyInfo::new(
561            "BITCOIN".to_string(),
562            "BTC".to_string(),
563            "Bitcoin".to_string(),
564            4,
565            1,
566            0.0001,
567            0.0005,
568        );
569
570        let json = serde_json::to_string(&info).unwrap();
571        let deserialized: CurrencyInfo = serde_json::from_str(&json).unwrap();
572        assert_eq!(info, deserialized);
573    }
574}