use std::fmt;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Currency(String);
impl Currency {
pub fn new(code: impl AsRef<str>) -> Self {
Self(code.as_ref().trim().to_uppercase())
}
pub fn eur() -> Self {
Self("EUR".to_string())
}
pub fn usd() -> Self {
Self("USD".to_string())
}
pub fn gbp() -> Self {
Self("GBP".to_string())
}
pub fn chf() -> Self {
Self("CHF".to_string())
}
pub fn jpy() -> Self {
Self("JPY".to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for Currency {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl From<&str> for Currency {
fn from(s: &str) -> Self {
Self::new(s)
}
}
impl From<String> for Currency {
fn from(s: String) -> Self {
Self::new(s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum InstrumentType {
Equity,
LinearFuture,
Forex,
Cfd,
CryptoSpot,
Option,
}
impl InstrumentType {
pub fn is_linear(&self) -> bool {
match self {
Self::Equity | Self::LinearFuture | Self::Forex | Self::Cfd | Self::CryptoSpot => true,
Self::Option => false,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ContractSpec {
pub price_currency: Currency,
pub settlement_currency: Currency,
pub multiplier: f64,
pub quantity_step: f64,
pub min_quantity: f64,
pub instrument_type: InstrumentType,
}
impl Default for ContractSpec {
fn default() -> Self {
Self {
price_currency: Currency::usd(),
settlement_currency: Currency::usd(),
multiplier: 1.0,
quantity_step: 1.0,
min_quantity: 1.0,
instrument_type: InstrumentType::Equity,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ContractSpecError {
NonPositiveMultiplier,
NonFiniteMultiplier,
NonPositiveQuantityStep,
NonFiniteQuantityStep,
InvalidMinQuantity,
EmptyPriceCurrency,
EmptySettlementCurrency,
}
impl fmt::Display for ContractSpecError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NonPositiveMultiplier => f.write_str("multiplier must be > 0"),
Self::NonFiniteMultiplier => f.write_str("multiplier must be finite"),
Self::NonPositiveQuantityStep => f.write_str("quantity_step must be > 0"),
Self::NonFiniteQuantityStep => f.write_str("quantity_step must be finite"),
Self::InvalidMinQuantity => {
f.write_str("min_quantity must be finite and >= quantity_step")
}
Self::EmptyPriceCurrency => f.write_str("price_currency must not be empty"),
Self::EmptySettlementCurrency => f.write_str("settlement_currency must not be empty"),
}
}
}
impl std::error::Error for ContractSpecError {}
impl ContractSpec {
pub fn validate(&self) -> Result<(), ContractSpecError> {
if !self.multiplier.is_finite() {
return Err(ContractSpecError::NonFiniteMultiplier);
}
if self.multiplier <= 0.0 {
return Err(ContractSpecError::NonPositiveMultiplier);
}
if !self.quantity_step.is_finite() {
return Err(ContractSpecError::NonFiniteQuantityStep);
}
if self.quantity_step <= 0.0 {
return Err(ContractSpecError::NonPositiveQuantityStep);
}
if !self.min_quantity.is_finite() || self.min_quantity < self.quantity_step {
return Err(ContractSpecError::InvalidMinQuantity);
}
if self.price_currency.as_str().is_empty() {
return Err(ContractSpecError::EmptyPriceCurrency);
}
if self.settlement_currency.as_str().is_empty() {
return Err(ContractSpecError::EmptySettlementCurrency);
}
Ok(())
}
pub fn round_quantity_down(&self, quantity: f64) -> f64 {
if !quantity.is_finite() || quantity <= 0.0 || self.quantity_step <= 0.0 {
return 0.0;
}
let steps = (quantity / self.quantity_step + 1e-12).floor();
let rounded = steps * self.quantity_step;
if rounded + 1e-12 < self.min_quantity {
0.0
} else {
rounded
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct FxRate {
pub base: Currency,
pub quote: Currency,
pub rate: f64,
pub timestamp: i64,
}
impl FxRate {
pub fn new(base: impl Into<Currency>, quote: impl Into<Currency>, rate: f64) -> Self {
Self {
base: base.into(),
quote: quote.into(),
rate,
timestamp: 0,
}
}
pub fn convert(
&self,
amount: f64,
from: &Currency,
to: &Currency,
) -> Result<f64, FxConversionError> {
if from == to {
return Ok(amount);
}
if !self.rate.is_finite() || self.rate <= 0.0 {
return Err(FxConversionError::InvalidRate(self.rate));
}
if from == &self.base && to == &self.quote {
Ok(amount * self.rate)
} else if from == &self.quote && to == &self.base {
Ok(amount / self.rate)
} else {
Err(FxConversionError::MissingPair {
from: from.clone(),
to: to.clone(),
})
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum FxConversionError {
InvalidRate(f64),
MissingPair { from: Currency, to: Currency },
StaleRate { age_seconds: i64, max_age: i64 },
}
impl fmt::Display for FxConversionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidRate(r) => write!(f, "invalid non-positive FX rate: {r}"),
Self::MissingPair { from, to } => {
write!(f, "no FX rate available to convert from {from} to {to}")
}
Self::StaleRate {
age_seconds,
max_age,
} => write!(
f,
"FX rate is stale: age {age_seconds}s exceeds max {max_age}s"
),
}
}
}
impl std::error::Error for FxConversionError {}
#[derive(Debug, Clone, PartialEq)]
pub enum ValuationError {
InvalidContract(ContractSpecError),
FxUnavailable(FxConversionError),
NonPositivePrice(f64),
NonFiniteInput(&'static str),
}
impl fmt::Display for ValuationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidContract(e) => write!(f, "invalid contract specification: {e}"),
Self::FxUnavailable(e) => write!(f, "currency conversion failed: {e}"),
Self::NonPositivePrice(p) => write!(f, "price must be positive: {p}"),
Self::NonFiniteInput(field) => write!(f, "input {field} must be finite"),
}
}
}
impl std::error::Error for ValuationError {}
impl From<ContractSpecError> for ValuationError {
fn from(e: ContractSpecError) -> Self {
Self::InvalidContract(e)
}
}
impl From<FxConversionError> for ValuationError {
fn from(e: FxConversionError) -> Self {
Self::FxUnavailable(e)
}
}
pub fn notional_value(
price: f64,
quantity: f64,
spec: &ContractSpec,
fx_to_account: Option<f64>,
) -> Result<f64, ValuationError> {
spec.validate()?;
if !price.is_finite() || price <= 0.0 {
return Err(ValuationError::NonPositivePrice(price));
}
if !quantity.is_finite() || quantity < 0.0 {
return Err(ValuationError::NonFiniteInput("quantity"));
}
let fx = match fx_to_account {
Some(rate) if rate.is_finite() && rate > 0.0 => rate,
Some(invalid) => {
return Err(ValuationError::FxUnavailable(
FxConversionError::InvalidRate(invalid),
))
}
None => {
return Err(ValuationError::FxUnavailable(
FxConversionError::MissingPair {
from: spec.price_currency.clone(),
to: spec.settlement_currency.clone(),
},
))
}
};
Ok(quantity * price * spec.multiplier * fx)
}
pub fn stop_risk_amount(
entry: f64,
stop: f64,
quantity: f64,
spec: &ContractSpec,
fx_to_account: Option<f64>,
) -> Result<f64, ValuationError> {
spec.validate()?;
if !entry.is_finite() || entry <= 0.0 {
return Err(ValuationError::NonPositivePrice(entry));
}
if !stop.is_finite() || stop <= 0.0 {
return Err(ValuationError::NonPositivePrice(stop));
}
if !quantity.is_finite() || quantity < 0.0 {
return Err(ValuationError::NonFiniteInput("quantity"));
}
let fx = match fx_to_account {
Some(rate) if rate.is_finite() && rate > 0.0 => rate,
Some(invalid) => {
return Err(ValuationError::FxUnavailable(
FxConversionError::InvalidRate(invalid),
))
}
None => {
return Err(ValuationError::FxUnavailable(
FxConversionError::MissingPair {
from: spec.price_currency.clone(),
to: spec.settlement_currency.clone(),
},
))
}
};
let price_diff = (entry - stop).abs();
Ok(quantity * price_diff * spec.multiplier * fx)
}
pub fn contract_pnl(
entry: f64,
exit: f64,
quantity: f64,
is_long: bool,
spec: &ContractSpec,
fx_to_account: Option<f64>,
) -> Result<f64, ValuationError> {
spec.validate()?;
if !entry.is_finite() || entry <= 0.0 {
return Err(ValuationError::NonPositivePrice(entry));
}
if !exit.is_finite() || exit <= 0.0 {
return Err(ValuationError::NonPositivePrice(exit));
}
if !quantity.is_finite() || quantity < 0.0 {
return Err(ValuationError::NonFiniteInput("quantity"));
}
let fx = match fx_to_account {
Some(rate) if rate.is_finite() && rate > 0.0 => rate,
Some(invalid) => {
return Err(ValuationError::FxUnavailable(
FxConversionError::InvalidRate(invalid),
))
}
None => {
return Err(ValuationError::FxUnavailable(
FxConversionError::MissingPair {
from: spec.price_currency.clone(),
to: spec.settlement_currency.clone(),
},
))
}
};
let diff = if is_long { exit - entry } else { entry - exit };
Ok(diff * quantity * spec.multiplier * fx)
}
pub fn contract_tick_value(
tick_size: f64,
spec: &ContractSpec,
fx_to_account: Option<f64>,
) -> Result<f64, ValuationError> {
spec.validate()?;
if !tick_size.is_finite() || tick_size <= 0.0 {
return Err(ValuationError::NonPositivePrice(tick_size));
}
let fx = match fx_to_account {
Some(rate) if rate.is_finite() && rate > 0.0 => rate,
Some(invalid) => {
return Err(ValuationError::FxUnavailable(
FxConversionError::InvalidRate(invalid),
))
}
None => {
return Err(ValuationError::FxUnavailable(
FxConversionError::MissingPair {
from: spec.price_currency.clone(),
to: spec.settlement_currency.clone(),
},
))
}
};
Ok(tick_size * spec.multiplier * fx)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_contract_spec_validation() {
let default_spec = ContractSpec::default();
assert_eq!(default_spec.validate(), Ok(()));
let bad_multiplier = ContractSpec {
multiplier: 0.0,
..ContractSpec::default()
};
assert_eq!(
bad_multiplier.validate(),
Err(ContractSpecError::NonPositiveMultiplier)
);
let bad_step = ContractSpec {
quantity_step: -1.0,
..ContractSpec::default()
};
assert_eq!(
bad_step.validate(),
Err(ContractSpecError::NonPositiveQuantityStep)
);
let bad_min = ContractSpec {
quantity_step: 10.0,
min_quantity: 5.0,
..ContractSpec::default()
};
assert_eq!(
bad_min.validate(),
Err(ContractSpecError::InvalidMinQuantity)
);
}
#[test]
fn test_quantity_down_rounding() {
let spec = ContractSpec {
quantity_step: 0.5,
min_quantity: 1.0,
..ContractSpec::default()
};
assert_eq!(spec.round_quantity_down(2.8), 2.5);
assert_eq!(spec.round_quantity_down(1.0), 1.0);
assert_eq!(spec.round_quantity_down(0.9), 0.0);
assert_eq!(spec.round_quantity_down(0.5), 0.0);
}
#[test]
fn test_fx_conversion() {
let fx = FxRate::new("EUR", "USD", 1.08);
assert_eq!(
fx.convert(100.0, &Currency::eur(), &Currency::eur())
.unwrap(),
100.0
);
let usd = fx
.convert(100.0, &Currency::eur(), &Currency::usd())
.unwrap();
assert!((usd - 108.0).abs() < 1e-9);
let eur = fx
.convert(108.0, &Currency::usd(), &Currency::eur())
.unwrap();
assert!((eur - 100.0).abs() < 1e-9);
let err = fx.convert(100.0, &Currency::gbp(), &Currency::usd());
assert!(matches!(err, Err(FxConversionError::MissingPair { .. })));
}
}