RustQuant_instruments/fx/
exchange.rs

1// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2// RustQuant: A Rust library for quantitative finance tools.
3// Copyright (C) 2023 https://github.com/avhz
4// Dual licensed under Apache 2.0 and MIT.
5// See:
6//      - LICENSE-APACHE.md
7//      - LICENSE-MIT.md
8// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
9
10//! FX exchange module.
11
12use super::CurrencyPair;
13use crate::fx::currency::Currency;
14use crate::fx::money::Money;
15use std::collections::HashMap;
16
17// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
18// STRUCTS, ENUMS, AND TRAITS
19// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
20
21/// Exchange struct to hold exchange rates.
22#[derive(Debug, Clone, Default)]
23pub struct Exchange {
24    /// Exchange rates hashmap.
25    /// The key is a string of the form e.g. "USD_EUR",
26    /// and the value is an ExchangeRate struct.
27    /// The key is generated from the from_currency and to_currency of the ExchangeRate.
28    pub rates: HashMap<CurrencyPair, ExchangeRate>,
29}
30
31/// `ExchangeRate` struct to hold exchange rate information.
32#[allow(clippy::module_name_repetitions)]
33#[derive(Debug, Clone, Copy)]
34pub struct ExchangeRate {
35    /// From currency
36    pub from_currency: Currency,
37
38    /// To currency
39    pub to_currency: Currency,
40
41    /// The actual exchange rate as a ratio from_currency/to_currency
42    pub rate: f64,
43}
44
45// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
46// IMPLEMENTATIONS
47// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
48
49impl Exchange {
50    /// Create a new empty Exchange.
51    ///
52    /// # Example
53    /// ```
54    /// use RustQuant::instruments::fx::exchange::Exchange;
55    ///
56    /// let exchange = Exchange::new();
57    /// ```
58    ///
59    #[must_use]
60    pub fn new() -> Self {
61        Self {
62            rates: HashMap::new(),
63        }
64    }
65
66    /// Adds a new `ExchangeRate` to the Exchange.
67    ///
68    /// # Example
69    /// ```
70    /// use RustQuant::instruments::*;
71    /// use RustQuant::iso::*;
72    ///
73    /// let mut exchange = Exchange::new();
74    ///
75    /// let usd_to_eur = ExchangeRate::new(USD, EUR, 0.85); // USD to EUR
76    /// let eur_to_usd = ExchangeRate::new(EUR, USD, 1.18); // EUR to USD
77    ///
78    /// exchange.add_rate(usd_to_eur);
79    /// exchange.add_rate(eur_to_usd);
80    /// ```
81    ///
82    pub fn add_rate(&mut self, rate: ExchangeRate) {
83        // let key = format!(
84        //     "{}/{}",
85        //     rate.from_currency.code.alphabetic, rate.to_currency.code.alphabetic
86        // );
87        let key = CurrencyPair::new(rate.from_currency, rate.to_currency);
88        self.rates.insert(key, rate);
89    }
90
91    /// Retrieves an `ExchangeRate` from the Exchange.
92    ///
93    /// # Example
94    /// ```
95    /// use RustQuant::instruments::*;
96    ///
97    /// let mut exchange = Exchange::new();
98    ///
99    /// let usd_to_eur = ExchangeRate::new(USD, EUR, 0.85); // USD to EUR
100    /// let eur_to_usd = ExchangeRate::new(EUR, USD, 1.18); // EUR to USD
101    ///
102    /// exchange.add_rate(usd_to_eur);
103    /// exchange.add_rate(eur_to_usd);
104    ///
105    /// let retrieved_usd_to_eur = exchange.get_rate(&USD, &EUR).expect("Rate not found");
106    /// assert_eq!(retrieved_usd_to_eur.rate, 0.85);
107    ///
108    /// let retrieved_eur_to_usd = exchange.get_rate(&EUR, &USD).expect("Rate not found");
109    /// assert_eq!(retrieved_eur_to_usd.rate, 1.18);
110    /// ```
111    ///
112    #[must_use]
113    pub fn get_rate(
114        &self,
115        from_currency: &Currency,
116        to_currency: &Currency,
117    ) -> Option<&ExchangeRate> {
118        // let key = format!(
119        //     "{}/{}",
120        //     from_currency.code.alphabetic, to_currency.code.alphabetic
121        // );
122        let key = CurrencyPair::new(*from_currency, *to_currency);
123        self.rates.get(&key)
124    }
125
126    /// Convert money from one currency to another using the exchange rate in the Exchange.
127    /// It panics if the conversion rate is not found or if the money's currency doesn't match with `from_currency`.
128    ///
129    /// # Example
130    /// ```
131    /// use RustQuant::instruments::*;
132    ///
133    /// let mut exchange = Exchange::new();
134    ///
135    /// let usd_to_eur = ExchangeRate::new(USD, EUR, 0.85); // USD to EUR
136    /// let eur_to_usd = ExchangeRate::new(EUR, USD, 1.18); // EUR to USD
137    ///
138    /// exchange.add_rate(usd_to_eur);
139    /// exchange.add_rate(eur_to_usd);
140    ///
141    /// let usd_100 = Money::new(USD, 100.0); // 100 USD
142    /// let eur_85 = exchange.convert(usd_100, EUR); // Should be 85 EUR
143    ///
144    /// assert_eq!(eur_85.currency, EUR);
145    /// assert_eq!(eur_85.amount, 85.0);
146    /// ```
147    #[must_use]
148    pub fn convert(&self, money: Money, to_currency: Currency) -> Money {
149        let rate = self
150            .get_rate(&money.currency, &to_currency)
151            .unwrap_or_else(|| {
152                panic!(
153                    "Exchange rate for converting {} to {} not found.",
154                    money.currency.code.alphabetic, to_currency.code.alphabetic
155                )
156            });
157        rate.convert(money)
158    }
159}
160
161impl ExchangeRate {
162    /// Create a new exchange rate.
163    #[must_use]
164    pub fn new(from_currency: Currency, to_currency: Currency, rate: f64) -> Self {
165        Self {
166            from_currency,
167            to_currency,
168            rate,
169        }
170    }
171
172    /// Convert money from one currency to another using this exchange rate.
173    /// It panics if the money's currency doesn't match with `from_currency`.
174    ///
175    /// # Example
176    /// ```
177    /// use RustQuant::instruments::*;
178    /// use RustQuant::utils::assert_approx_equal;
179    ///
180    /// // Use USD and EUR currency constants from the money module.
181    /// let usd = Money::new(USD, 100.0);
182    /// let eur_usd = ExchangeRate::new(USD, EUR, 0.9186955);  // 1 USD = 0.9186955 EUR
183    /// let eur = eur_usd.convert(usd);
184    ///
185    /// assert_approx_equal!(eur.amount, 91.86955, 1e-5);
186    /// assert_eq!(eur.currency, EUR);
187    /// ```
188    ///
189    /// It panics if the money's currency doesn't match with `from_currency`.
190    ///
191    /// ```should_panic
192    /// use RustQuant::instruments::*;
193    ///
194    /// let usd = Money::new(EUR, 100.0);                       // Notice the wrong currency
195    /// let eur_usd = ExchangeRate::new(USD, EUR, 0.9186955);   // 1 USD = 0.9186955 EUR
196    ///
197    /// eur_usd.convert(usd);  // This will panic
198    /// ```
199    #[must_use]
200    pub fn convert(&self, money: Money) -> Money {
201        if money.currency == self.from_currency {
202            let new_amount = money.amount * self.rate;
203            Money::new(self.to_currency, new_amount)
204        } else {
205            panic!(
206                "The currency of the money doesn't match with from_currency of the exchange rate."
207            )
208        }
209    }
210}
211
212// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
213// UNIT TESTS
214// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
215
216// You can now add additional unit tests
217#[cfg(test)]
218mod test_exchange_rate {
219    use super::*;
220    use crate::fx::*;
221    use crate::fx::{EUR, USD};
222    use std::f64::EPSILON as EPS;
223    use RustQuant_utils::assert_approx_equal;
224
225    #[test]
226    fn test_conversion() {
227        // Create Money instance
228        let usd_100 = Money::new(USD, 100.0);
229
230        // Create ExchangeRate instance
231        let usd_to_eur = ExchangeRate::new(USD, EUR, 0.85); // 1 USD = 0.85 EUR as an example
232
233        // Convert USD to EUR
234        let eur_85 = usd_to_eur.convert(usd_100);
235
236        // Verify the conversion
237        assert_eq!(eur_85.currency, EUR);
238        assert_approx_equal!(eur_85.amount, 85.0, EPS);
239    }
240
241    #[test]
242    fn test_add_and_get_rate() {
243        let mut exchange = Exchange::new();
244
245        let usd_to_eur = ExchangeRate::new(USD, EUR, 0.85); // USD to EUR
246        let eur_to_usd = ExchangeRate::new(EUR, USD, 1.18); // EUR to USD
247
248        exchange.add_rate(usd_to_eur);
249        exchange.add_rate(eur_to_usd);
250
251        let retrieved_usd_to_eur = exchange.get_rate(&USD, &EUR).expect("Rate not found");
252        assert_approx_equal!(retrieved_usd_to_eur.rate, 0.85, EPS);
253
254        let retrieved_eur_to_usd = exchange.get_rate(&EUR, &USD).expect("Rate not found");
255        assert_approx_equal!(retrieved_eur_to_usd.rate, 1.18, EPS);
256    }
257
258    #[test]
259    fn test_conversion_with_exchange() {
260        let mut exchange = Exchange::new();
261
262        let usd_to_eur = ExchangeRate::new(USD, EUR, 0.85); // USD to EUR
263        let eur_to_usd = ExchangeRate::new(EUR, USD, 1.18); // EUR to USD
264
265        exchange.add_rate(usd_to_eur);
266        exchange.add_rate(eur_to_usd);
267
268        let usd_100 = Money::new(USD, 100.0); // 100 USD
269        let eur_85 = exchange.convert(usd_100, EUR); // Should be 85 EUR
270
271        assert_eq!(eur_85.currency, EUR);
272        assert_approx_equal!(eur_85.amount, 85.0, EPS);
273    }
274}