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