use serde::{Deserialize, Serialize};
use std::{fmt, str::FromStr};
use strum::{Display, EnumIter, IntoStaticStr};
use strum_macros::EnumString;
use crate::{
error::{ChapatyError, DataError, TransportError},
generated::chapaty::{
bq_exporter::v1::EconomicCategory as RpcEconomicCategory,
bq_exporter::v1::EconomicImportance as RpcEconomicImportance,
data::v1::DataBroker as RpcDataBroker,
},
impl_abs_primitive, impl_add_sub_mul_div_primitive, impl_from_primitive, impl_neg_primitive,
};
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default, Serialize, Deserialize)]
pub struct Price(pub f64);
impl_from_primitive!(Price, f64);
impl_add_sub_mul_div_primitive!(Price, f64);
impl_neg_primitive!(Price, f64);
impl_abs_primitive!(Price, f64);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default, Serialize, Deserialize)]
pub struct Tick(pub i64);
impl_from_primitive!(Tick, i64);
impl_add_sub_mul_div_primitive!(Tick, i64);
impl_neg_primitive!(Tick, i64);
impl_abs_primitive!(Tick, i64);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default, Serialize, Deserialize)]
pub struct Quantity(pub f64);
impl_from_primitive!(Quantity, f64);
impl_add_sub_mul_div_primitive!(Quantity, f64);
pub type Volume = Quantity;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
pub struct Count(pub i64);
impl_from_primitive!(Count, i64);
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize,
)]
pub struct TradeId(pub i64);
impl_from_primitive!(TradeId, i64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct TimeframeIdx(pub u32);
impl_from_primitive!(TimeframeIdx, u32);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default, Serialize, Deserialize)]
pub struct EconomicValue(pub f64);
impl_from_primitive!(EconomicValue, f64);
impl_add_sub_mul_div_primitive!(EconomicValue, f64);
#[derive(
Debug,
Clone,
Copy,
PartialEq,
PartialOrd,
Eq,
Hash,
Serialize,
Deserialize,
EnumString,
EnumIter,
)]
#[strum(serialize_all = "lowercase")]
pub enum CandleDirection {
Bullish,
Bearish,
Doji,
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
PartialOrd,
Eq,
Hash,
Serialize,
Deserialize,
EnumString,
EnumIter,
)]
#[strum(serialize_all = "lowercase")]
pub enum TradeSide {
Buy,
Sell,
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
PartialOrd,
Eq,
Hash,
Serialize,
Deserialize,
EnumString,
EnumIter,
)]
#[strum(serialize_all = "lowercase")]
pub enum LiquiditySide {
Bid,
Ask,
}
impl LiquiditySide {
pub fn trade_side(&self) -> TradeSide {
match self {
LiquiditySide::Bid => TradeSide::Sell,
LiquiditySide::Ask => TradeSide::Buy,
}
}
}
impl From<bool> for LiquiditySide {
fn from(value: bool) -> Self {
if value {
LiquiditySide::Bid
} else {
LiquiditySide::Ask
}
}
}
impl From<&LiquiditySide> for bool {
fn from(value: &LiquiditySide) -> Self {
match value {
LiquiditySide::Bid => true,
LiquiditySide::Ask => false,
}
}
}
impl From<LiquiditySide> for bool {
fn from(value: LiquiditySide) -> Self {
(&value).into()
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
PartialOrd,
Eq,
Hash,
Serialize,
Deserialize,
EnumString,
EnumIter,
)]
#[strum(serialize_all = "lowercase")]
pub enum ExecutionDepth {
TopOfBook,
BookSweep,
}
impl From<bool> for ExecutionDepth {
fn from(is_best_match: bool) -> Self {
if is_best_match {
ExecutionDepth::TopOfBook
} else {
ExecutionDepth::BookSweep
}
}
}
impl From<&ExecutionDepth> for bool {
fn from(trade_match_quality: &ExecutionDepth) -> Self {
match trade_match_quality {
ExecutionDepth::TopOfBook => true,
ExecutionDepth::BookSweep => false,
}
}
}
impl From<ExecutionDepth> for bool {
fn from(value: ExecutionDepth) -> Self {
(&value).into()
}
}
#[derive(
Copy,
Clone,
Debug,
EnumString,
EnumIter,
Display,
PartialEq,
Eq,
Hash,
Deserialize,
Serialize,
PartialOrd,
Ord,
)]
#[strum(serialize_all = "lowercase")]
pub enum DataBroker {
NinjaTrader,
Binance,
InvestingCom,
}
impl DataBroker {
pub fn supports_economic_calendar(&self) -> bool {
matches!(self, DataBroker::InvestingCom)
}
}
impl From<&DataBroker> for RpcDataBroker {
fn from(broker: &DataBroker) -> Self {
match broker {
DataBroker::Binance => RpcDataBroker::Binance,
DataBroker::NinjaTrader => RpcDataBroker::NinjaTrader,
DataBroker::InvestingCom => RpcDataBroker::InvestingCom,
}
}
}
impl From<DataBroker> for RpcDataBroker {
fn from(broker: DataBroker) -> Self {
(&broker).into()
}
}
impl TryFrom<RpcDataBroker> for DataBroker {
type Error = ChapatyError;
fn try_from(proto: RpcDataBroker) -> Result<Self, Self::Error> {
match proto {
RpcDataBroker::Binance => Ok(DataBroker::Binance),
RpcDataBroker::NinjaTrader => Ok(DataBroker::NinjaTrader),
RpcDataBroker::InvestingCom => Ok(DataBroker::InvestingCom),
RpcDataBroker::Unspecified => Err(TransportError::RpcTypeNotFound(
"Broker cannot be unspecified in this context".to_string(),
)
.into()),
}
}
}
#[derive(
Copy,
Clone,
Debug,
EnumString,
Display,
PartialEq,
Eq,
Hash,
Deserialize,
Serialize,
PartialOrd,
Ord,
)]
#[strum(serialize_all = "lowercase")]
pub enum Exchange {
Cme,
Binance,
}
impl TryFrom<DataBroker> for Exchange {
type Error = ChapatyError;
fn try_from(broker: DataBroker) -> Result<Self, Self::Error> {
match broker {
DataBroker::NinjaTrader => Ok(Exchange::Cme),
DataBroker::Binance => Ok(Exchange::Binance),
DataBroker::InvestingCom => Err(DataError::UnexpectedEnumVariant(format!(
"{} does not map to an exchange",
broker
))
.into()),
}
}
}
#[derive(
Copy,
Clone,
Debug,
EnumString,
Display,
PartialEq,
Eq,
Hash,
Deserialize,
Serialize,
PartialOrd,
Ord,
)]
#[strum(serialize_all = "lowercase")]
pub enum EconomicDataSource {
InvestingCom,
}
impl TryFrom<DataBroker> for EconomicDataSource {
type Error = ChapatyError;
fn try_from(broker: DataBroker) -> Result<Self, Self::Error> {
match broker {
DataBroker::InvestingCom => Ok(EconomicDataSource::InvestingCom),
DataBroker::NinjaTrader | DataBroker::Binance => Err(DataError::UnexpectedEnumVariant(
format!("{} does not map to an economic data source", broker),
)
.into()),
}
}
}
#[derive(
Copy,
Clone,
Debug,
Hash,
PartialEq,
Eq,
Deserialize,
Serialize,
PartialOrd,
Ord,
EnumIter,
EnumString,
Display,
IntoStaticStr,
)]
#[strum(serialize_all = "lowercase")]
pub enum Period {
#[strum(serialize = "{0}h")]
Hour(u8),
#[strum(serialize = "{0}m")]
Minute(u8),
#[strum(serialize = "{0}d")]
Day(u8),
#[strum(serialize = "{0}mo")]
Month(u8),
#[strum(serialize = "{0}s")]
Second(u8),
#[strum(serialize = "{0}w")]
Week(u8),
}
#[derive(
Copy,
Clone,
Debug,
Hash,
PartialEq,
Eq,
Deserialize,
Serialize,
PartialOrd,
Ord,
EnumIter,
EnumString,
Display,
IntoStaticStr,
)]
#[strum(serialize_all = "lowercase")]
pub enum MarketType {
Spot,
Future,
}
impl From<Symbol> for MarketType {
fn from(value: Symbol) -> Self {
(&value).into()
}
}
impl From<&Symbol> for MarketType {
fn from(value: &Symbol) -> Self {
match value {
Symbol::Future(_) => Self::Future,
Symbol::Spot(_) => Self::Spot,
}
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord, IntoStaticStr,
)]
pub enum Symbol {
Spot(SpotPair),
Future(FutureContract),
}
impl fmt::Display for Symbol {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Symbol::Spot(s) => write!(f, "{}", s),
Symbol::Future(s) => write!(f, "{}", s),
}
}
}
impl FromStr for Symbol {
type Err = ChapatyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(spot) = SpotPair::from_str(s) {
return Ok(Symbol::Spot(spot));
}
if let Ok(future) = FutureContract::from_str(s) {
return Ok(Symbol::Future(future));
}
Err(DataError::InvalidSymbol(s.to_string()).into())
}
}
impl Symbol {
pub fn market_type(&self) -> MarketType {
self.into()
}
}
#[derive(
Copy,
Clone,
Debug,
PartialEq,
Eq,
Hash,
Display,
EnumString,
Serialize,
Deserialize,
PartialOrd,
Ord,
IntoStaticStr,
)]
#[strum(serialize_all = "kebab-case")]
pub enum SpotPair {
BtcUsdt,
BnbUsdt,
EthUsdt,
SolUsdt,
XrpUsdt,
TrxUsdt,
AdaUsdt,
XlmUsdt,
}
#[derive(
Clone,
Copy,
Debug,
Display,
EnumString,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
PartialOrd,
Ord,
)]
#[strum(serialize_all = "lowercase")]
pub enum ContractMonth {
#[strum(serialize = "f")]
January = 1,
#[strum(serialize = "g")]
February = 2,
#[strum(serialize = "h")]
March = 3,
#[strum(serialize = "j")]
April = 4,
#[strum(serialize = "k")]
May = 5,
#[strum(serialize = "m")]
June = 6,
#[strum(serialize = "n")]
July = 7,
#[strum(serialize = "q")]
August = 8,
#[strum(serialize = "u")]
September = 9,
#[strum(serialize = "v")]
October = 10,
#[strum(serialize = "x")]
November = 11,
#[strum(serialize = "z")]
December = 12,
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Display,
EnumString,
Serialize,
Deserialize,
PartialOrd,
Ord,
)]
#[strum(serialize_all = "lowercase")]
pub enum FutureRoot {
#[strum(serialize = "6a")]
AudUsd,
#[strum(serialize = "6b")]
GbpUsd,
#[strum(serialize = "6c")]
CadUsd,
#[strum(serialize = "6e")]
EurUsd,
#[strum(serialize = "6j")]
JpyUsd,
#[strum(serialize = "6n")]
NzdUsd,
#[strum(serialize = "btc")]
Btc,
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Display,
EnumString,
Serialize,
Deserialize,
PartialOrd,
Ord,
)]
#[strum(serialize_all = "lowercase")]
pub enum ContractYear {
#[strum(serialize = "0")]
Y0 = 0,
#[strum(serialize = "1")]
Y1 = 1,
#[strum(serialize = "2")]
Y2 = 2,
#[strum(serialize = "3")]
Y3 = 3,
#[strum(serialize = "4")]
Y4 = 4,
#[strum(serialize = "5")]
Y5 = 5,
#[strum(serialize = "6")]
Y6 = 6,
#[strum(serialize = "7")]
Y7 = 7,
#[strum(serialize = "8")]
Y8 = 8,
#[strum(serialize = "9")]
Y9 = 9,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
pub struct FutureContract {
pub root: FutureRoot,
pub month: ContractMonth,
pub year: ContractYear,
}
impl fmt::Display for FutureContract {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}{}{}", self.root, self.month, self.year)
}
}
impl FromStr for FutureContract {
type Err = ChapatyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.to_lowercase();
if s.len() < 3 {
return Err(DataError::InvalidSymbol(format!(
"Future contract string too short: {}",
s
))
.into());
}
let (root, remainder) = if s.len() >= 3 && FutureRoot::from_str(&s[..2]).is_ok() {
(&s[..2], &s[2..])
} else if s.len() >= 4 && FutureRoot::from_str(&s[..3]).is_ok() {
(&s[..3], &s[3..])
} else {
return Err(DataError::InvalidSymbol(format!("Invalid future root in: {}", s)).into());
};
if remainder.len() != 2 {
return Err(
DataError::InvalidSymbol(format!("Invalid future contract format: {}", s)).into(),
);
}
let root = FutureRoot::from_str(root).map_err(DataError::ParseEnum)?;
let month = ContractMonth::from_str(&remainder[..1]).map_err(DataError::ParseEnum)?;
let year = ContractYear::from_str(&remainder[1..]).map_err(DataError::ParseEnum)?;
Ok(FutureContract { root, month, year })
}
}
#[derive(
PartialEq,
Copy,
Clone,
Debug,
Display,
EnumString,
EnumIter,
Hash,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
)]
#[strum(serialize_all = "camelCase")]
pub enum EconomicCategory {
Employment = 1,
EconomicActivity = 2,
Inflation = 3,
Credit = 4,
CentralBanks = 5,
ConfidenceIndex = 6,
Balance = 7,
#[strum(serialize = "Bonds")]
Bonds = 8,
}
impl From<&EconomicCategory> for RpcEconomicCategory {
fn from(category: &EconomicCategory) -> Self {
match category {
EconomicCategory::Employment => Self::Employment,
EconomicCategory::EconomicActivity => Self::EconomicActivity,
EconomicCategory::Inflation => Self::Inflation,
EconomicCategory::Credit => Self::Credit,
EconomicCategory::CentralBanks => Self::CentralBanks,
EconomicCategory::ConfidenceIndex => Self::ConfidenceIndex,
EconomicCategory::Balance => Self::Balance,
EconomicCategory::Bonds => Self::Bonds,
}
}
}
impl From<EconomicCategory> for RpcEconomicCategory {
fn from(category: EconomicCategory) -> Self {
(&category).into()
}
}
impl TryFrom<RpcEconomicCategory> for EconomicCategory {
type Error = ChapatyError;
fn try_from(proto: RpcEconomicCategory) -> Result<Self, Self::Error> {
match proto {
RpcEconomicCategory::Employment => Ok(Self::Employment),
RpcEconomicCategory::EconomicActivity => Ok(Self::EconomicActivity),
RpcEconomicCategory::Inflation => Ok(Self::Inflation),
RpcEconomicCategory::Credit => Ok(Self::Credit),
RpcEconomicCategory::CentralBanks => Ok(Self::CentralBanks),
RpcEconomicCategory::ConfidenceIndex => Ok(Self::ConfidenceIndex),
RpcEconomicCategory::Balance => Ok(Self::Balance),
RpcEconomicCategory::Bonds => Ok(Self::Bonds),
RpcEconomicCategory::Unspecified => Err(TransportError::RpcTypeNotFound(
"Economic category cannot be unspecified in this context".to_string(),
)
.into()),
}
}
}
#[derive(
Debug,
Clone,
Copy,
Hash,
PartialEq,
Eq,
EnumIter,
Display,
Serialize,
Deserialize,
PartialOrd,
Ord,
)]
#[strum(serialize_all = "lowercase")]
pub enum EconomicEventImpact {
Low = 1,
Medium = 2,
High = 3,
}
impl From<&EconomicEventImpact> for RpcEconomicImportance {
fn from(value: &EconomicEventImpact) -> Self {
match value {
EconomicEventImpact::Low => Self::Low,
EconomicEventImpact::Medium => Self::Moderate,
EconomicEventImpact::High => Self::High,
}
}
}
impl From<EconomicEventImpact> for RpcEconomicImportance {
fn from(value: EconomicEventImpact) -> Self {
(&value).into()
}
}
impl TryFrom<RpcEconomicImportance> for EconomicEventImpact {
type Error = ChapatyError;
fn try_from(proto: RpcEconomicImportance) -> Result<Self, Self::Error> {
match proto {
RpcEconomicImportance::Low => Ok(Self::Low),
RpcEconomicImportance::Moderate => Ok(Self::Medium),
RpcEconomicImportance::High => Ok(Self::High),
RpcEconomicImportance::Unspecified => Err(TransportError::RpcTypeNotFound(
"Economic importance cannot be unspecified in this context".to_string(),
)
.into()),
}
}
}
#[derive(
PartialEq,
Copy,
Clone,
Debug,
Display,
EnumIter,
EnumString,
Hash,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
)]
#[strum(serialize_all = "UPPERCASE")]
pub enum CountryCode {
Au,
Ca,
Ez,
Gb,
Jp,
Nz,
Us,
}
pub trait Instrument {
fn tick_size(&self) -> f64;
fn tick_value_usd(&self) -> f64;
fn usd_to_ticks(&self, usd: f64) -> Tick {
let ticks = (usd / self.tick_value_usd()).round() as i64;
Tick(ticks)
}
fn ticks_to_usd(&self, ticks: Tick) -> f64 {
ticks.0 as f64 * self.tick_value_usd()
}
fn price_to_ticks(&self, price_dist: Price) -> Tick {
let ticks = (price_dist.0 / self.tick_size()).round() as i64;
Tick(ticks)
}
fn ticks_to_price(&self, ticks: Tick) -> Price {
Price(ticks.0 as f64 * self.tick_size())
}
fn usd_to_price_dist(&self, usd: f64) -> Price {
let ticks = self.usd_to_ticks(usd);
self.ticks_to_price(ticks)
}
fn normalize_price(&self, price: f64) -> f64 {
let ticks = (price / self.tick_size()).round();
ticks * self.tick_size()
}
}
impl Instrument for SpotPair {
fn tick_size(&self) -> f64 {
match self {
SpotPair::BtcUsdt | SpotPair::BnbUsdt | SpotPair::EthUsdt | SpotPair::SolUsdt => 0.01,
SpotPair::XrpUsdt | SpotPair::TrxUsdt | SpotPair::AdaUsdt | SpotPair::XlmUsdt => 0.0001,
}
}
fn tick_value_usd(&self) -> f64 {
self.tick_size()
}
}
impl Instrument for FutureRoot {
fn tick_size(&self) -> f64 {
match self {
FutureRoot::AudUsd | FutureRoot::CadUsd | FutureRoot::EurUsd | FutureRoot::NzdUsd => {
0.00005
}
FutureRoot::GbpUsd => 0.0001,
FutureRoot::JpyUsd => 0.0000005,
FutureRoot::Btc => 5.0,
}
}
fn tick_value_usd(&self) -> f64 {
match self {
FutureRoot::EurUsd | FutureRoot::GbpUsd | FutureRoot::JpyUsd => 6.25,
FutureRoot::AudUsd | FutureRoot::CadUsd | FutureRoot::NzdUsd => 5.0,
FutureRoot::Btc => 25.0,
}
}
}
impl Instrument for Symbol {
fn tick_size(&self) -> f64 {
match self {
Symbol::Spot(spot) => spot.tick_size(),
Symbol::Future(future) => future.root.tick_size(),
}
}
fn tick_value_usd(&self) -> f64 {
match self {
Symbol::Spot(spot) => spot.tick_value_usd(),
Symbol::Future(future) => future.root.tick_value_usd(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn future_contracts_format_as_root_month_year() {
let cases = [
(
FutureRoot::EurUsd,
ContractMonth::December,
ContractYear::Y5,
"6ez5",
),
(
FutureRoot::GbpUsd,
ContractMonth::March,
ContractYear::Y4,
"6bh4",
),
(
FutureRoot::AudUsd,
ContractMonth::June,
ContractYear::Y3,
"6am3",
),
(
FutureRoot::CadUsd,
ContractMonth::September,
ContractYear::Y2,
"6cu2",
),
(
FutureRoot::JpyUsd,
ContractMonth::January,
ContractYear::Y1,
"6jf1",
),
(
FutureRoot::NzdUsd,
ContractMonth::July,
ContractYear::Y0,
"6nn0",
),
(
FutureRoot::Btc,
ContractMonth::November,
ContractYear::Y9,
"btcx9",
),
];
for (root, month, year, expected) in cases {
let contract = FutureContract { root, month, year };
let symbol = Symbol::Future(contract);
assert_eq!(symbol.to_string(), expected, "Failed for {:?}", contract);
}
}
#[test]
fn parses_future_contracts_case_insensitive() {
let cases = [
(
"6ez5",
FutureRoot::EurUsd,
ContractMonth::December,
ContractYear::Y5,
),
(
"6EZ5",
FutureRoot::EurUsd,
ContractMonth::December,
ContractYear::Y5,
),
(
"6bh4",
FutureRoot::GbpUsd,
ContractMonth::March,
ContractYear::Y4,
),
(
"btcx9",
FutureRoot::Btc,
ContractMonth::November,
ContractYear::Y9,
),
(
"BTCX9",
FutureRoot::Btc,
ContractMonth::November,
ContractYear::Y9,
),
];
for (input, root, month, year) in cases {
let parsed: Symbol = input
.parse()
.unwrap_or_else(|_| panic!("Failed to parse '{}'", input));
let expected = Symbol::Future(FutureContract { root, month, year });
assert_eq!(parsed, expected, "Mismatch for '{}'", input);
}
}
#[test]
fn parses_all_future_roots() {
let cases = [
("6az5", FutureRoot::AudUsd),
("6bz5", FutureRoot::GbpUsd),
("6cz5", FutureRoot::CadUsd),
("6ez5", FutureRoot::EurUsd),
("6jz5", FutureRoot::JpyUsd),
("6nz5", FutureRoot::NzdUsd),
("btcz5", FutureRoot::Btc),
];
for (input, expected_root) in cases {
let parsed: Symbol = input
.parse()
.unwrap_or_else(|_| panic!("Failed to parse '{}'", input));
match parsed {
Symbol::Future(contract) => assert_eq!(contract.root, expected_root),
_ => panic!("Expected Future variant for '{}'", input),
}
}
}
#[test]
fn rejects_invalid_symbols() {
let invalid = [
"", "invalid", "btc", "6e", "6ez", "btc-", "-usdt", "btcusdt", "6ez55", "xxz5", ];
for input in invalid {
let result: Result<Symbol, _> = input.parse();
assert!(result.is_err(), "Expected '{}' to fail parsing", input);
}
}
#[test]
fn future_contracts_survive_round_trip() {
let contracts = [
FutureContract {
root: FutureRoot::EurUsd,
month: ContractMonth::December,
year: ContractYear::Y5,
},
FutureContract {
root: FutureRoot::Btc,
month: ContractMonth::March,
year: ContractYear::Y0,
},
FutureContract {
root: FutureRoot::JpyUsd,
month: ContractMonth::September,
year: ContractYear::Y9,
},
];
for contract in contracts {
let original = Symbol::Future(contract);
let serialized = original.to_string();
let deserialized: Symbol = serialized.parse().unwrap();
assert_eq!(
original, deserialized,
"Round-trip failed for {:?}",
contract
);
}
}
#[test]
fn canonical_strings_parse_back_unchanged() {
let canonical = ["btc-usdt", "eth-usdt", "6ez5", "6bh4", "btcx9"];
for input in canonical {
let parsed: Symbol = input.parse().unwrap();
let output = parsed.to_string();
assert_eq!(input, output, "Canonical form changed for '{}'", input);
}
}
#[test]
fn importance_numeration() {
assert_eq!(EconomicEventImpact::Low as u8, 1, "Low should be 1");
assert_eq!(EconomicEventImpact::Medium as u8, 2, "Medium should be 2");
assert_eq!(EconomicEventImpact::High as u8, 3, "High should be 3");
}
fn future_sym(root: FutureRoot) -> Symbol {
Symbol::Future(FutureContract {
root,
month: ContractMonth::December, year: ContractYear::Y5, })
}
#[test]
fn test_quant_math_eur_usd() {
let eur = future_sym(FutureRoot::EurUsd);
assert_eq!(eur.tick_size(), 0.00005);
assert_eq!(eur.tick_value_usd(), 6.25);
let risk_dist = eur.usd_to_price_dist(100.0);
assert!((risk_dist.0 - 0.0008).abs() < f64::EPSILON);
let norm = eur.normalize_price(1.00003);
assert!((norm - 1.00005).abs() < f64::EPSILON);
}
#[test]
fn test_quant_math_btc_future() {
let btc = future_sym(FutureRoot::Btc);
let entry = 50_000.0;
let exit = 50_100.0;
let ticks = btc.price_to_ticks(Price(exit - entry));
let pnl = btc.ticks_to_usd(ticks);
assert_eq!(ticks.0, 20);
assert_eq!(pnl, 500.0);
}
#[test]
fn handling_floating_point_artifacts() {
let eur = future_sym(FutureRoot::EurUsd);
let valid_price = 1.10000;
let dirty_high = valid_price + 0.00000001;
let norm_high = eur.normalize_price(dirty_high);
assert!(
(norm_high - valid_price).abs() < f64::EPSILON,
"Failed to round down dirty high input: {:.8} -> {:.8}",
dirty_high,
norm_high
);
let dirty_low = valid_price - 0.00000001;
let norm_low = eur.normalize_price(dirty_low);
assert!(
(norm_low - valid_price).abs() < f64::EPSILON,
"Failed to round up dirty low input: {:.8} -> {:.8}",
dirty_low,
norm_low
);
}
#[test]
fn tick_conversion_ignores_noise() {
let eur = future_sym(FutureRoot::EurUsd);
let clean_dist = 0.00050;
let expected_ticks = 10;
let noisy_dist = Price(clean_dist + 0.00000001);
let ticks = eur.price_to_ticks(noisy_dist);
assert_eq!(
ticks.0, expected_ticks,
"Positive noise caused tick mismatch"
);
let noisy_dist_neg = Price(clean_dist - 0.00000001);
let ticks_neg = eur.price_to_ticks(noisy_dist_neg);
assert_eq!(
ticks_neg.0, expected_ticks,
"Negative noise caused tick mismatch"
);
}
}