use chrono::NaiveDate;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CurrencyPair {
pub from_currency: String,
pub to_currency: String,
}
impl CurrencyPair {
#[allow(clippy::too_many_arguments)]
pub fn new(from: &str, to: &str) -> Self {
Self {
from_currency: from.to_uppercase(),
to_currency: to.to_uppercase(),
}
}
pub fn inverse(&self) -> Self {
Self {
from_currency: self.to_currency.clone(),
to_currency: self.from_currency.clone(),
}
}
pub fn as_string(&self) -> String {
format!("{}/{}", self.from_currency, self.to_currency)
}
pub fn is_same_currency(&self) -> bool {
self.from_currency == self.to_currency
}
}
impl std::fmt::Display for CurrencyPair {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}/{}", self.from_currency, self.to_currency)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum RateType {
Spot,
Closing,
Average,
Budget,
Historical,
Negotiated,
Custom(String),
}
impl std::fmt::Display for RateType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RateType::Spot => write!(f, "SPOT"),
RateType::Closing => write!(f, "CLOSING"),
RateType::Average => write!(f, "AVERAGE"),
RateType::Budget => write!(f, "BUDGET"),
RateType::Historical => write!(f, "HISTORICAL"),
RateType::Negotiated => write!(f, "NEGOTIATED"),
RateType::Custom(s) => write!(f, "{s}"),
}
}
}
#[derive(Debug, Clone)]
pub struct FxRate {
pub pair: CurrencyPair,
pub rate_type: RateType,
pub effective_date: NaiveDate,
pub rate: Decimal,
pub inverse_rate: Decimal,
pub source: String,
pub valid_until: Option<NaiveDate>,
}
impl FxRate {
pub fn new(
from_currency: &str,
to_currency: &str,
rate_type: RateType,
effective_date: NaiveDate,
rate: Decimal,
source: &str,
) -> Self {
let inverse = if rate > Decimal::ZERO {
(Decimal::ONE / rate).round_dp(6)
} else {
Decimal::ZERO
};
Self {
pair: CurrencyPair::new(from_currency, to_currency),
rate_type,
effective_date,
rate,
inverse_rate: inverse,
source: source.to_string(),
valid_until: None,
}
}
pub fn with_validity(mut self, valid_until: NaiveDate) -> Self {
self.valid_until = Some(valid_until);
self
}
pub fn convert(&self, amount: Decimal) -> Decimal {
(amount * self.rate).round_dp(2)
}
pub fn convert_inverse(&self, amount: Decimal) -> Decimal {
(amount * self.inverse_rate).round_dp(2)
}
pub fn is_valid_on(&self, date: NaiveDate) -> bool {
if date < self.effective_date {
return false;
}
if let Some(valid_until) = self.valid_until {
date <= valid_until
} else {
true
}
}
}
#[derive(Debug, Clone, Default)]
pub struct FxRateTable {
rates: HashMap<(String, String), Vec<FxRate>>,
pub base_currency: String,
}
impl FxRateTable {
pub fn new(base_currency: &str) -> Self {
Self {
rates: HashMap::new(),
base_currency: base_currency.to_uppercase(),
}
}
pub fn add_rate(&mut self, rate: FxRate) {
let key = (
format!("{}_{}", rate.pair.from_currency, rate.pair.to_currency),
rate.rate_type.to_string(),
);
self.rates.entry(key).or_default().push(rate);
}
pub fn get_rate(
&self,
from_currency: &str,
to_currency: &str,
rate_type: &RateType,
date: NaiveDate,
) -> Option<&FxRate> {
if from_currency.to_uppercase() == to_currency.to_uppercase() {
return None;
}
let key = (
format!(
"{}_{}",
from_currency.to_uppercase(),
to_currency.to_uppercase()
),
rate_type.to_string(),
);
self.rates.get(&key).and_then(|rates| {
rates
.iter()
.filter(|r| r.is_valid_on(date))
.max_by_key(|r| r.effective_date)
})
}
pub fn get_closing_rate(
&self,
from_currency: &str,
to_currency: &str,
date: NaiveDate,
) -> Option<&FxRate> {
self.get_rate(from_currency, to_currency, &RateType::Closing, date)
}
pub fn get_average_rate(
&self,
from_currency: &str,
to_currency: &str,
date: NaiveDate,
) -> Option<&FxRate> {
self.get_rate(from_currency, to_currency, &RateType::Average, date)
}
pub fn get_spot_rate(
&self,
from_currency: &str,
to_currency: &str,
date: NaiveDate,
) -> Option<&FxRate> {
self.get_rate(from_currency, to_currency, &RateType::Spot, date)
}
pub fn convert(
&self,
amount: Decimal,
from_currency: &str,
to_currency: &str,
rate_type: &RateType,
date: NaiveDate,
) -> Option<Decimal> {
if from_currency.to_uppercase() == to_currency.to_uppercase() {
return Some(amount);
}
if let Some(rate) = self.get_rate(from_currency, to_currency, rate_type, date) {
return Some(rate.convert(amount));
}
if let Some(rate) = self.get_rate(to_currency, from_currency, rate_type, date) {
return Some(rate.convert_inverse(amount));
}
if from_currency.to_uppercase() != self.base_currency
&& to_currency.to_uppercase() != self.base_currency
{
let to_base = self.get_rate(from_currency, &self.base_currency, rate_type, date);
let from_base = self.get_rate(&self.base_currency, to_currency, rate_type, date);
if let (Some(r1), Some(r2)) = (to_base, from_base) {
let base_amount = r1.convert(amount);
return Some(r2.convert(base_amount));
}
}
None
}
pub fn get_all_rates(&self, from_currency: &str, to_currency: &str) -> Vec<&FxRate> {
self.rates
.iter()
.filter(|((pair, _), _)| {
*pair
== format!(
"{}_{}",
from_currency.to_uppercase(),
to_currency.to_uppercase()
)
})
.flat_map(|(_, rates)| rates.iter())
.collect()
}
pub fn len(&self) -> usize {
self.rates.values().map(std::vec::Vec::len).sum()
}
pub fn is_empty(&self) -> bool {
self.rates.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TranslationMethod {
CurrentRate,
Temporal,
MonetaryNonMonetary,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TranslationAccountType {
Asset,
Liability,
Equity,
Revenue,
Expense,
RetainedEarnings,
CommonStock,
AdditionalPaidInCapital,
}
#[derive(Debug, Clone)]
pub struct TranslatedAmount {
pub local_amount: Decimal,
pub local_currency: String,
pub group_amount: Decimal,
pub group_currency: String,
pub rate_used: Decimal,
pub rate_type: RateType,
pub translation_date: NaiveDate,
}
#[derive(Debug, Clone)]
pub struct CTAEntry {
pub entry_id: String,
pub company_code: String,
pub local_currency: String,
pub group_currency: String,
pub fiscal_year: i32,
pub fiscal_period: u8,
pub period_end_date: NaiveDate,
pub cta_amount: Decimal,
pub opening_rate: Decimal,
pub closing_rate: Decimal,
pub average_rate: Decimal,
pub opening_net_assets_local: Decimal,
pub closing_net_assets_local: Decimal,
pub net_income_local: Decimal,
pub components: Vec<CTAComponent>,
}
#[derive(Debug, Clone)]
pub struct CTAComponent {
pub description: String,
pub local_amount: Decimal,
pub rate: Decimal,
pub group_amount: Decimal,
}
impl CTAEntry {
pub fn new(
entry_id: String,
company_code: String,
local_currency: String,
group_currency: String,
fiscal_year: i32,
fiscal_period: u8,
period_end_date: NaiveDate,
) -> Self {
Self {
entry_id,
company_code,
local_currency,
group_currency,
fiscal_year,
fiscal_period,
period_end_date,
cta_amount: Decimal::ZERO,
opening_rate: Decimal::ONE,
closing_rate: Decimal::ONE,
average_rate: Decimal::ONE,
opening_net_assets_local: Decimal::ZERO,
closing_net_assets_local: Decimal::ZERO,
net_income_local: Decimal::ZERO,
components: Vec::new(),
}
}
pub fn calculate_current_rate_method(&mut self) {
let closing_translated = self.closing_net_assets_local * self.closing_rate;
let opening_translated = self.opening_net_assets_local * self.opening_rate;
let income_translated = self.net_income_local * self.average_rate;
self.cta_amount = closing_translated - opening_translated - income_translated;
self.components = vec![
CTAComponent {
description: "Closing net assets at closing rate".to_string(),
local_amount: self.closing_net_assets_local,
rate: self.closing_rate,
group_amount: closing_translated,
},
CTAComponent {
description: "Opening net assets at opening rate".to_string(),
local_amount: self.opening_net_assets_local,
rate: self.opening_rate,
group_amount: opening_translated,
},
CTAComponent {
description: "Net income at average rate".to_string(),
local_amount: self.net_income_local,
rate: self.average_rate,
group_amount: income_translated,
},
];
}
}
#[derive(Debug, Clone)]
pub struct RealizedFxGainLoss {
pub document_number: String,
pub company_code: String,
pub transaction_date: NaiveDate,
pub settlement_date: NaiveDate,
pub transaction_currency: String,
pub local_currency: String,
pub original_amount: Decimal,
pub original_local_amount: Decimal,
pub settlement_local_amount: Decimal,
pub gain_loss: Decimal,
pub transaction_rate: Decimal,
pub settlement_rate: Decimal,
}
impl RealizedFxGainLoss {
#[allow(clippy::too_many_arguments)]
pub fn new(
document_number: String,
company_code: String,
transaction_date: NaiveDate,
settlement_date: NaiveDate,
transaction_currency: String,
local_currency: String,
original_amount: Decimal,
transaction_rate: Decimal,
settlement_rate: Decimal,
) -> Self {
let original_local = (original_amount * transaction_rate).round_dp(2);
let settlement_local = (original_amount * settlement_rate).round_dp(2);
let gain_loss = settlement_local - original_local;
Self {
document_number,
company_code,
transaction_date,
settlement_date,
transaction_currency,
local_currency,
original_amount,
original_local_amount: original_local,
settlement_local_amount: settlement_local,
gain_loss,
transaction_rate,
settlement_rate,
}
}
pub fn is_gain(&self) -> bool {
self.gain_loss > Decimal::ZERO
}
}
#[derive(Debug, Clone)]
pub struct UnrealizedFxGainLoss {
pub revaluation_id: String,
pub company_code: String,
pub revaluation_date: NaiveDate,
pub account_code: String,
pub document_number: String,
pub transaction_currency: String,
pub local_currency: String,
pub open_amount: Decimal,
pub book_value_local: Decimal,
pub revalued_local: Decimal,
pub gain_loss: Decimal,
pub original_rate: Decimal,
pub revaluation_rate: Decimal,
}
pub mod currencies {
pub const USD: &str = "USD";
pub const EUR: &str = "EUR";
pub const GBP: &str = "GBP";
pub const JPY: &str = "JPY";
pub const CHF: &str = "CHF";
pub const CAD: &str = "CAD";
pub const AUD: &str = "AUD";
pub const CNY: &str = "CNY";
pub const INR: &str = "INR";
pub const BRL: &str = "BRL";
pub const MXN: &str = "MXN";
pub const KRW: &str = "KRW";
pub const SGD: &str = "SGD";
pub const HKD: &str = "HKD";
pub const SEK: &str = "SEK";
pub const NOK: &str = "NOK";
pub const DKK: &str = "DKK";
pub const PLN: &str = "PLN";
pub const ZAR: &str = "ZAR";
pub const THB: &str = "THB";
}
pub fn base_rates_usd() -> HashMap<String, Decimal> {
let mut rates = HashMap::new();
rates.insert("EUR".to_string(), dec!(1.10)); rates.insert("GBP".to_string(), dec!(1.27)); rates.insert("JPY".to_string(), dec!(0.0067)); rates.insert("CHF".to_string(), dec!(1.13)); rates.insert("CAD".to_string(), dec!(0.74)); rates.insert("AUD".to_string(), dec!(0.65)); rates.insert("CNY".to_string(), dec!(0.14)); rates.insert("INR".to_string(), dec!(0.012)); rates.insert("BRL".to_string(), dec!(0.20)); rates.insert("MXN".to_string(), dec!(0.058)); rates.insert("KRW".to_string(), dec!(0.00075)); rates.insert("SGD".to_string(), dec!(0.75)); rates.insert("HKD".to_string(), dec!(0.128)); rates.insert("SEK".to_string(), dec!(0.095)); rates.insert("NOK".to_string(), dec!(0.093)); rates.insert("DKK".to_string(), dec!(0.147)); rates.insert("PLN".to_string(), dec!(0.25)); rates.insert("ZAR".to_string(), dec!(0.053)); rates.insert("THB".to_string(), dec!(0.028)); rates
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_currency_pair() {
let pair = CurrencyPair::new("EUR", "USD");
assert_eq!(pair.from_currency, "EUR");
assert_eq!(pair.to_currency, "USD");
assert_eq!(pair.as_string(), "EUR/USD");
let inverse = pair.inverse();
assert_eq!(inverse.from_currency, "USD");
assert_eq!(inverse.to_currency, "EUR");
}
#[test]
fn test_fx_rate_conversion() {
let rate = FxRate::new(
"EUR",
"USD",
RateType::Spot,
NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
dec!(1.10),
"ECB",
);
let converted = rate.convert(dec!(100));
assert_eq!(converted, dec!(110.00));
let inverse = rate.convert_inverse(dec!(110));
assert_eq!(inverse, dec!(100.00));
}
#[test]
fn test_fx_rate_table() {
let mut table = FxRateTable::new("USD");
table.add_rate(FxRate::new(
"EUR",
"USD",
RateType::Spot,
NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
dec!(1.10),
"ECB",
));
let converted = table.convert(
dec!(100),
"EUR",
"USD",
&RateType::Spot,
NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
);
assert_eq!(converted, Some(dec!(110.00)));
}
#[test]
fn test_cta_calculation() {
let mut cta = CTAEntry::new(
"CTA-001".to_string(),
"1200".to_string(),
"EUR".to_string(),
"USD".to_string(),
2024,
12,
NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
);
cta.opening_net_assets_local = dec!(1000000);
cta.closing_net_assets_local = dec!(1100000);
cta.net_income_local = dec!(100000);
cta.opening_rate = dec!(1.08);
cta.closing_rate = dec!(1.12);
cta.average_rate = dec!(1.10);
cta.calculate_current_rate_method();
assert_eq!(cta.cta_amount, dec!(42000));
}
}