use std::collections::HashMap;
pub const DIVISOR_5_DECIMALS: f64 = 100_000.0;
pub const DIVISOR_3_DECIMALS: f64 = 1_000.0;
pub const DIVISOR_2_DECIMALS: f64 = 100.0;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct InstrumentConfig {
pub price_divisor: f64,
pub decimal_places: u32,
}
impl InstrumentConfig {
#[inline]
pub const fn new(price_divisor: f64, decimal_places: u32) -> Self {
Self {
price_divisor,
decimal_places,
}
}
pub const STANDARD: Self = Self::new(DIVISOR_5_DECIMALS, 5);
pub const JPY: Self = Self::new(DIVISOR_3_DECIMALS, 3);
pub const METALS: Self = Self::new(DIVISOR_3_DECIMALS, 3);
pub const RUB: Self = Self::new(DIVISOR_3_DECIMALS, 3);
pub const INDEX: Self = Self::new(DIVISOR_3_DECIMALS, 2);
}
impl Default for InstrumentConfig {
fn default() -> Self {
Self::STANDARD
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CurrencyCategory {
Standard,
Jpy,
Rub,
Metal,
Unknown,
}
impl CurrencyCategory {
pub fn from_code(code: &str) -> Self {
let code_upper = normalize_code(code);
match code_upper.as_str() {
"JPY" => Self::Jpy,
"RUB" => Self::Rub,
"XAU" | "XAG" | "XPT" | "XPD" => Self::Metal,
"USD" | "EUR" | "GBP" | "AUD" | "NZD" | "CAD" | "CHF" | "SEK" | "NOK" | "DKK"
| "SGD" | "HKD" | "MXN" | "ZAR" | "TRY" | "PLN" | "CZK" | "HUF" | "CNH" | "CNY"
| "INR" | "THB" | "KRW" | "TWD" | "BRL" | "ILS" => Self::Standard,
_ => Self::Unknown,
}
}
pub const fn config(&self) -> InstrumentConfig {
match self {
Self::Jpy => InstrumentConfig::JPY,
Self::Rub => InstrumentConfig::RUB,
Self::Metal => InstrumentConfig::METALS,
Self::Standard | Self::Unknown => InstrumentConfig::STANDARD,
}
}
}
pub fn resolve_instrument_config(from: &str, to: &str) -> InstrumentConfig {
if looks_like_index_code(from)
|| looks_like_index_code(to)
|| looks_like_equity_code(from)
|| looks_like_equity_code(to)
{
return InstrumentConfig::INDEX;
}
let from_cat = CurrencyCategory::from_code(from);
let to_cat = CurrencyCategory::from_code(to);
match (from_cat, to_cat) {
(CurrencyCategory::Metal, _) | (_, CurrencyCategory::Metal) => InstrumentConfig::METALS,
(CurrencyCategory::Jpy, _) | (_, CurrencyCategory::Jpy) => InstrumentConfig::JPY,
(CurrencyCategory::Rub, _) | (_, CurrencyCategory::Rub) => InstrumentConfig::RUB,
_ => InstrumentConfig::STANDARD,
}
}
#[inline]
fn normalize_code(code: &str) -> String {
code.trim().to_ascii_uppercase()
}
#[inline]
fn pair_key(from: &str, to: &str) -> String {
format!("{}/{}", normalize_code(from), normalize_code(to))
}
#[inline]
fn looks_like_index_code(code: &str) -> bool {
let normalized = normalize_code(code);
normalized.contains("IDX") || normalized.chars().any(|ch| ch.is_ascii_digit())
}
#[inline]
fn looks_like_equity_code(code: &str) -> bool {
let normalized = normalize_code(code);
normalized.len() > 3
&& normalized.ends_with("US")
&& normalized[..normalized.len() - 2]
.chars()
.all(|ch| ch.is_ascii_uppercase())
}
pub trait HasInstrumentConfig {
fn instrument_config(&self) -> InstrumentConfig;
#[inline]
fn price_divisor(&self) -> f64 {
self.instrument_config().price_divisor
}
#[inline]
fn decimal_places(&self) -> u32 {
self.instrument_config().decimal_places
}
}
pub trait InstrumentProvider: Send + Sync {
fn get_config(&self, from: &str, to: &str) -> InstrumentConfig;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct DefaultInstrumentProvider;
impl InstrumentProvider for DefaultInstrumentProvider {
fn get_config(&self, from: &str, to: &str) -> InstrumentConfig {
resolve_instrument_config(from, to)
}
}
#[derive(Debug, Clone, Default)]
pub struct OverrideInstrumentProvider {
overrides: HashMap<String, InstrumentConfig>,
}
impl OverrideInstrumentProvider {
pub fn new() -> Self {
Self::default()
}
pub fn add_override(&mut self, from: &str, to: &str, config: InstrumentConfig) -> &mut Self {
self.overrides.insert(pair_key(from, to), config);
self
}
pub fn remove_override(&mut self, from: &str, to: &str) -> &mut Self {
self.overrides.remove(&pair_key(from, to));
self
}
pub fn has_override(&self, from: &str, to: &str) -> bool {
self.overrides.contains_key(&pair_key(from, to))
}
pub fn override_count(&self) -> usize {
self.overrides.len()
}
}
impl InstrumentProvider for OverrideInstrumentProvider {
fn get_config(&self, from: &str, to: &str) -> InstrumentConfig {
self.overrides
.get(&pair_key(from, to))
.copied()
.unwrap_or_else(|| resolve_instrument_config(from, to))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_currency_category() {
assert_eq!(CurrencyCategory::from_code("JPY"), CurrencyCategory::Jpy);
assert_eq!(CurrencyCategory::from_code("jpy"), CurrencyCategory::Jpy);
assert_eq!(CurrencyCategory::from_code("XAU"), CurrencyCategory::Metal);
assert_eq!(
CurrencyCategory::from_code("USD"),
CurrencyCategory::Standard
);
assert_eq!(
CurrencyCategory::from_code("XYZ"),
CurrencyCategory::Unknown
);
}
#[test]
fn test_resolve_config() {
assert_eq!(
resolve_instrument_config("EUR", "USD"),
InstrumentConfig::STANDARD
);
assert_eq!(
resolve_instrument_config("USD", "JPY"),
InstrumentConfig::JPY
);
assert_eq!(
resolve_instrument_config("XAU", "USD"),
InstrumentConfig::METALS
);
assert_eq!(
resolve_instrument_config("USD", "RUB"),
InstrumentConfig::RUB
);
assert_eq!(
resolve_instrument_config("DE40", "USD"),
InstrumentConfig::INDEX
);
assert_eq!(
resolve_instrument_config("DEUIDX", "EUR"),
InstrumentConfig::INDEX
);
assert_eq!(
resolve_instrument_config("AAPLUS", "USD"),
InstrumentConfig::INDEX
);
}
#[test]
fn test_override_provider() {
let mut provider = OverrideInstrumentProvider::new();
provider.add_override("BTC", "USD", InstrumentConfig::new(100.0, 2));
assert_eq!(provider.get_config("BTC", "USD").price_divisor, 100.0);
assert_eq!(
provider.get_config("EUR", "USD"),
InstrumentConfig::STANDARD
);
}
#[test]
fn test_override_provider_key_collision_regression() {
let mut provider = OverrideInstrumentProvider::new();
let first = InstrumentConfig::new(10.0, 1);
let second = InstrumentConfig::new(20.0, 2);
provider.add_override("AB", "CDE", first);
provider.add_override("ABC", "DE", second);
assert_eq!(provider.get_config("AB", "CDE"), first);
assert_eq!(provider.get_config("ABC", "DE"), second);
}
#[test]
fn test_config_constants() {
assert_eq!(InstrumentConfig::STANDARD.price_divisor, 100_000.0);
assert_eq!(InstrumentConfig::JPY.price_divisor, 1_000.0);
assert_eq!(InstrumentConfig::METALS.price_divisor, 1_000.0);
assert_eq!(InstrumentConfig::INDEX.price_divisor, 1_000.0);
}
}