1use crate::{impl_json_debug_pretty, impl_json_display};
8use serde::{Deserialize, Serialize};
9
10#[derive(Clone, PartialEq, Serialize, Deserialize)]
12pub struct WithdrawalPriority {
13 pub name: String,
15 pub value: f64,
17}
18
19impl WithdrawalPriority {
20 pub fn new(name: String, value: f64) -> Self {
22 Self { name, value }
23 }
24
25 pub fn very_low() -> Self {
27 Self::new("very_low".to_string(), 0.15)
28 }
29
30 pub fn low() -> Self {
32 Self::new("low".to_string(), 0.5)
33 }
34
35 pub fn medium() -> Self {
37 Self::new("medium".to_string(), 1.0)
38 }
39
40 pub fn high() -> Self {
42 Self::new("high".to_string(), 1.2)
43 }
44
45 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#[derive(Clone, PartialEq, Serialize, Deserialize)]
56pub struct CurrencyInfo {
57 pub coin_type: String,
59 pub currency: String,
61 pub currency_long: String,
63 pub fee_precision: i32,
65 pub min_confirmations: i32,
67 pub min_withdrawal_fee: f64,
69 pub withdrawal_fee: f64,
71 pub withdrawal_priorities: Vec<WithdrawalPriority>,
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub disabled: Option<bool>,
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub min_deposit_amount: Option<f64>,
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub max_withdrawal_amount: Option<f64>,
82}
83
84impl CurrencyInfo {
85 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 pub fn add_priority(&mut self, priority: WithdrawalPriority) {
112 self.withdrawal_priorities.push(priority);
113 }
114
115 pub fn with_disabled(mut self, disabled: bool) -> Self {
117 self.disabled = Some(disabled);
118 self
119 }
120
121 pub fn with_deposit_limit(mut self, min_amount: f64) -> Self {
123 self.min_deposit_amount = Some(min_amount);
124 self
125 }
126
127 pub fn with_withdrawal_limit(mut self, max_amount: f64) -> Self {
129 self.max_withdrawal_amount = Some(max_amount);
130 self
131 }
132
133 pub fn is_enabled(&self) -> bool {
135 !self.disabled.unwrap_or(false)
136 }
137
138 pub fn get_priority(&self, name: &str) -> Option<&WithdrawalPriority> {
140 self.withdrawal_priorities.iter().find(|p| p.name == name)
141 }
142
143 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 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#[derive(Clone, PartialEq, Serialize, Deserialize)]
163pub struct IndexPrice {
164 pub estimated_delivery_price: f64,
166 pub index_price: f64,
168 pub timestamp: i64,
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub index_name: Option<String>,
173}
174
175impl IndexPrice {
176 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 pub fn with_name(mut self, name: String) -> Self {
188 self.index_name = Some(name);
189 self
190 }
191
192 pub fn price_difference(&self) -> f64 {
194 self.estimated_delivery_price - self.index_price
195 }
196
197 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#[derive(Clone, PartialEq, Serialize, Deserialize)]
212pub struct FundingRate {
213 pub timestamp: i64,
215 pub index_name: String,
217 pub interest_rate: f64,
219 pub interest_8h: f64,
221 #[serde(skip_serializing_if = "Option::is_none")]
223 pub current_funding: Option<f64>,
224 #[serde(skip_serializing_if = "Option::is_none")]
226 pub next_funding_timestamp: Option<i64>,
227}
228
229impl FundingRate {
230 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 pub fn with_current_funding(mut self, funding: f64) -> Self {
244 self.current_funding = Some(funding);
245 self
246 }
247
248 pub fn with_next_funding(mut self, timestamp: i64) -> Self {
250 self.next_funding_timestamp = Some(timestamp);
251 self
252 }
253
254 pub fn annualized_rate(&self) -> f64 {
256 self.interest_rate * 365.0 * 3.0 }
258
259 pub fn is_positive(&self) -> bool {
261 self.current_funding.unwrap_or(0.0) > 0.0
262 }
263
264 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#[derive(Clone, PartialEq, Serialize, Deserialize)]
275pub struct HistoricalVolatility {
276 pub timestamp: i64,
278 pub volatility: f64,
280 pub period_days: i32,
282 pub underlying: String,
284}
285
286impl HistoricalVolatility {
287 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 pub fn as_decimal(&self) -> f64 {
299 self.volatility / 100.0
300 }
301
302 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#[derive(Clone, PartialEq, Serialize, Deserialize)]
317pub struct MarketStatistics {
318 pub currency: String,
320 pub volume_24h: f64,
322 pub volume_30d: f64,
324 pub volume_usd_24h: f64,
326 pub volume_usd_30d: f64,
328 pub trades_count_24h: i64,
330 pub trades_count_30d: i64,
332 pub open_interest: f64,
334 pub timestamp: i64,
336}
337
338impl MarketStatistics {
339 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 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 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 pub fn with_open_interest(mut self, oi: f64) -> Self {
378 self.open_interest = oi;
379 self
380 }
381
382 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 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 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#[derive(Clone, PartialEq, Serialize, Deserialize)]
416pub struct CurrencyInfoCollection {
417 pub currencies: Vec<CurrencyInfo>,
419}
420
421impl CurrencyInfoCollection {
422 pub fn new() -> Self {
424 Self {
425 currencies: Vec::new(),
426 }
427 }
428
429 pub fn add(&mut self, info: CurrencyInfo) {
431 self.currencies.push(info);
432 }
433
434 pub fn get(&self, currency: String) -> Option<&CurrencyInfo> {
436 self.currencies.iter().find(|c| c.currency == currency)
437 }
438
439 pub fn enabled(&self) -> Vec<&CurrencyInfo> {
441 self.currencies.iter().filter(|c| c.is_enabled()).collect()
442 }
443
444 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); }
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}