1use pretty_simple_display::{DebugPretty, DisplaySimple};
7use serde::{Deserialize, Serialize};
8
9#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
11pub struct WithdrawalPriority {
12 pub name: String,
14 pub value: f64,
16}
17
18impl WithdrawalPriority {
19 pub fn new(name: String, value: f64) -> Self {
21 Self { name, value }
22 }
23
24 pub fn very_low() -> Self {
26 Self::new("very_low".to_string(), 0.15)
27 }
28
29 pub fn low() -> Self {
31 Self::new("low".to_string(), 0.5)
32 }
33
34 pub fn medium() -> Self {
36 Self::new("medium".to_string(), 1.0)
37 }
38
39 pub fn high() -> Self {
41 Self::new("high".to_string(), 1.2)
42 }
43
44 pub fn very_high() -> Self {
46 Self::new("very_high".to_string(), 1.5)
47 }
48}
49
50#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
52pub struct CurrencyInfo {
53 pub coin_type: String,
55 pub currency: String,
57 pub currency_long: String,
59 pub fee_precision: i32,
61 pub min_confirmations: i32,
63 pub min_withdrawal_fee: f64,
65 pub withdrawal_fee: f64,
67 pub withdrawal_priorities: Vec<WithdrawalPriority>,
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub disabled: Option<bool>,
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub min_deposit_amount: Option<f64>,
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub max_withdrawal_amount: Option<f64>,
78}
79
80impl CurrencyInfo {
81 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 pub fn add_priority(&mut self, priority: WithdrawalPriority) {
108 self.withdrawal_priorities.push(priority);
109 }
110
111 pub fn with_disabled(mut self, disabled: bool) -> Self {
113 self.disabled = Some(disabled);
114 self
115 }
116
117 pub fn with_deposit_limit(mut self, min_amount: f64) -> Self {
119 self.min_deposit_amount = Some(min_amount);
120 self
121 }
122
123 pub fn with_withdrawal_limit(mut self, max_amount: f64) -> Self {
125 self.max_withdrawal_amount = Some(max_amount);
126 self
127 }
128
129 pub fn is_enabled(&self) -> bool {
131 !self.disabled.unwrap_or(false)
132 }
133
134 pub fn get_priority(&self, name: &str) -> Option<&WithdrawalPriority> {
136 self.withdrawal_priorities.iter().find(|p| p.name == name)
137 }
138
139 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 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#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
156pub struct IndexPrice {
157 pub estimated_delivery_price: f64,
159 pub index_price: f64,
161 pub timestamp: i64,
163 #[serde(skip_serializing_if = "Option::is_none")]
165 pub index_name: Option<String>,
166}
167
168impl IndexPrice {
169 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 pub fn with_name(mut self, name: String) -> Self {
181 self.index_name = Some(name);
182 self
183 }
184
185 pub fn price_difference(&self) -> f64 {
187 self.estimated_delivery_price - self.index_price
188 }
189
190 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#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
202pub struct FundingRate {
203 pub timestamp: i64,
205 pub index_name: String,
207 pub interest_rate: f64,
209 pub interest_8h: f64,
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub current_funding: Option<f64>,
214 #[serde(skip_serializing_if = "Option::is_none")]
216 pub next_funding_timestamp: Option<i64>,
217}
218
219impl FundingRate {
220 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 pub fn with_current_funding(mut self, funding: f64) -> Self {
234 self.current_funding = Some(funding);
235 self
236 }
237
238 pub fn with_next_funding(mut self, timestamp: i64) -> Self {
240 self.next_funding_timestamp = Some(timestamp);
241 self
242 }
243
244 pub fn annualized_rate(&self) -> f64 {
246 self.interest_rate * 365.0 * 3.0 }
248
249 pub fn is_positive(&self) -> bool {
251 self.current_funding.unwrap_or(0.0) > 0.0
252 }
253
254 pub fn is_negative(&self) -> bool {
256 self.current_funding.unwrap_or(0.0) < 0.0
257 }
258}
259
260#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
262pub struct HistoricalVolatility {
263 pub timestamp: i64,
265 pub volatility: f64,
267 pub period_days: i32,
269 pub underlying: String,
271}
272
273impl HistoricalVolatility {
274 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 pub fn as_decimal(&self) -> f64 {
286 self.volatility / 100.0
287 }
288
289 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#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
301pub struct MarketStatistics {
302 pub currency: String,
304 pub volume_24h: f64,
306 pub volume_30d: f64,
308 pub volume_usd_24h: f64,
310 pub volume_usd_30d: f64,
312 pub trades_count_24h: i64,
314 pub trades_count_30d: i64,
316 pub open_interest: f64,
318 pub timestamp: i64,
320}
321
322impl MarketStatistics {
323 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 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 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 pub fn with_open_interest(mut self, oi: f64) -> Self {
362 self.open_interest = oi;
363 self
364 }
365
366 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 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 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#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
397pub struct CurrencyInfoCollection {
398 pub currencies: Vec<CurrencyInfo>,
400}
401
402impl CurrencyInfoCollection {
403 pub fn new() -> Self {
405 Self {
406 currencies: Vec::new(),
407 }
408 }
409
410 pub fn add(&mut self, info: CurrencyInfo) {
412 self.currencies.push(info);
413 }
414
415 pub fn get(&self, currency: String) -> Option<&CurrencyInfo> {
417 self.currencies.iter().find(|c| c.currency == currency)
418 }
419
420 pub fn enabled(&self) -> Vec<&CurrencyInfo> {
422 self.currencies.iter().filter(|c| c.is_enabled()).collect()
423 }
424
425 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); }
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}