use std::collections::HashMap;
use std::sync::Arc;
use std::sync::RwLock;
use async_trait::async_trait;
use chrono::{DateTime, Local};
use crate::datatypes::{
Asset, Currency, CurrencyConverter, CurrencyError, DataError, DataItem, Quote, QuoteHandler,
Ticker,
};
pub async fn insert_fx_quote(
fx_rate: f64,
base_currency: Currency,
quote_currency: Currency,
time: DateTime<Local>,
quotes: Arc<dyn QuoteHandler + Send + Sync>,
) -> Result<(), DataError> {
let base_id = if let Ok(id) = base_currency.get_id() {
id
} else {
quotes.insert_asset(&Asset::Currency(base_currency)).await?
};
let currency_pair = format!("{base_currency}/{quote_currency}");
let ticker_id = quotes
.insert_ticker(&Ticker {
id: None,
name: currency_pair,
asset: base_id,
source: "manual".to_string(),
priority: 10,
currency: quote_currency,
factor: 1.0,
tz: None,
cal: None,
})
.await?;
quotes
.insert_quote(&Quote {
id: None,
ticker: ticker_id,
price: fx_rate,
time,
volume: None,
})
.await?;
let quote_id = if let Ok(id) = quote_currency.get_id() {
id
} else {
quotes.insert_asset(&Asset::Currency(quote_currency)).await?
};
let currency_pair = format!("{quote_currency}/{base_currency}");
let ticker_id = quotes
.insert_ticker(&Ticker {
id: None,
name: currency_pair,
asset: quote_id,
source: "manual".to_string(),
priority: 10,
currency: base_currency,
factor: 1.0,
tz: None,
cal: None,
})
.await?;
quotes
.insert_quote(&Quote {
id: None,
ticker: ticker_id,
price: 1.0 / fx_rate,
time,
volume: None,
})
.await?;
Ok(())
}
pub struct SimpleCurrencyConverter {
fx_rates: RwLock<HashMap<String, f64>>,
}
#[async_trait]
impl CurrencyConverter for SimpleCurrencyConverter {
async fn fx_rate(
&self,
base_currency: Currency,
quote_currency: Currency,
_time: DateTime<Local>,
) -> Result<f64, CurrencyError> {
let currency_string = format!(
"{}/{}",
&base_currency.to_string(),
"e_currency.to_string()
);
if let Ok(fx_store) = self.fx_rates.read() {
if fx_store.contains_key(¤cy_string) {
Ok(fx_store[¤cy_string])
} else {
Err(CurrencyError::ConversionFailed)
}
} else {
Err(CurrencyError::ConversionFailed)
}
}
}
impl SimpleCurrencyConverter {
pub fn new() -> SimpleCurrencyConverter {
SimpleCurrencyConverter {
fx_rates: RwLock::new(HashMap::new()),
}
}
pub fn insert_fx_rate(
&mut self,
base_currency: Currency,
quote_currency: Currency,
fx_rate: f64,
) {
let base_key = base_currency.to_string();
let quote_key = quote_currency.to_string();
if let Ok(mut fx_store) = self.fx_rates.write() {
fx_store.insert(format!("{base_key}/{quote_key}"), fx_rate);
fx_store.insert(format!("{quote_key}/{base_key}"), 1. / fx_rate);
}
}
}
impl Default for SimpleCurrencyConverter {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use chrono::offset::TimeZone;
use chrono::Local;
use crate::datatypes::CurrencyISOCode;
use crate::market::Market;
use crate::postgres::PostgresDB;
async fn prepare_db(db: Arc<dyn QuoteHandler + Send + Sync>) {
let time = Local.ymd(1970, 1, 1).and_hms_milli(0, 0, 1, 444);
let eur = db
.get_or_new_currency(CurrencyISOCode::new("EUR").unwrap())
.await
.unwrap();
let usd = db
.get_or_new_currency(CurrencyISOCode::new("USD").unwrap())
.await
.unwrap();
insert_fx_quote(0.9, usd, eur, time, db).await.unwrap();
}
#[tokio::test]
async fn test_get_fx_rate() {
let db_url = std::env::var("FINQL_TEST_DATABASE_URL");
assert!(
db_url.is_ok(),
"environment variable $FINQL_TEST_DATABASE_URL is not set"
);
let db = PostgresDB::new(&db_url.unwrap()).await.unwrap();
db.clean().await.unwrap();
let qh: Arc<dyn QuoteHandler + Send + Sync> = Arc::new(db);
prepare_db(qh.clone()).await;
let tol = 1.0e-6_f64;
let mut market = Market::new(qh).await;
let eur = market.get_currency("EUR").await.unwrap();
let usd = market.get_currency("USD").await.unwrap();
let time = Local::now();
let fx = market.fx_rate(usd, eur, time).await.unwrap();
assert_fuzzy_eq!(fx, 0.9, tol);
}
}