//! Crypto currency and exchange tickers and other market-related primatives //! use std::fmt::{self, Display}; use std::str::FromStr; use std::cmp::{PartialEq, Eq}; use std::convert::TryFrom; use serde::{Serialize, Deserialize}; use serde::de::{self, Deserializer, Visitor}; use serde::Serializer; //use decimal::d128; //use chrono::{DateTime, Utc, TimeZone}; #[macro_export] macro_rules! c { ($x:tt) => { $crate::crypto::Currency::$x } } #[macro_export] macro_rules! e { ($x:tt) => { $crate::crypto::Exchange::$x } } #[macro_export] macro_rules! t { ($base:tt-$quote:tt) => { $crate::crypto::Ticker { base: c!($base), quote: c!($quote) } } } /// which side of the transaction (buying or selling) /// a price/trade is /// #[repr(u8)] #[derive(Debug, PartialEq, Clone, Copy, Eq, Serialize)] pub enum Side { Bid = 1, Ask = 2, } impl TryFrom for Side { type Error = Error; fn try_from(n: u8) -> Result { match n { 1 => Ok(Side::Bid), 2 => Ok(Side::Ask), other => Err(Error::IllegalFormat(Box::new(format!("Expected 1 or 2 (received {})", other)))) } } } impl From for u8 { fn from(side: Side) -> Self { match side { Side::Bid => 1, Side::Ask => 2 } } } impl Side { pub fn as_verb(&self) -> &'static str { match *self { Side::Bid => "buy", Side::Ask => "sell" } } pub fn as_past_tense(&self) -> &'static str { match *self { Side::Bid => "bought", Side::Ask => "sold" } } pub fn as_past_tense_title_case(&self) -> &'static str { match *self { Side::Bid => "Bought", Side::Ask => "Sold" } } pub fn to_str(&self) -> &'static str { match *self { Side::Bid => "bid", Side::Ask => "ask" } } pub fn is_bid(&self) -> bool { match self { &Side::Bid => true, _ => false } } pub fn is_ask(&self) -> bool { !self.is_bid() } } impl <'de> Deserialize<'de> for Side { fn deserialize(deserializer: D) -> std::result::Result where D: Deserializer<'de> { let side = String::deserialize(deserializer)?; match side.to_lowercase().as_ref() { "bid" | "buy" | "bids" => Ok(Side::Bid), "ask" | "sell" | "asks" => Ok(Side::Ask), other => Err(de::Error::custom(format!("could not parse Side from '{}'", other))), } } } impl Display for Side { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:?}", self) } } impl FromStr for Side { type Err = Error; fn from_str(s: &str) -> Result { match s.to_lowercase().as_ref() { "bid" | "buy" | "bids" => Ok(Side::Bid), "ask" | "sell" | "asks" => Ok(Side::Ask), s => Err(Error::IllegalFormat(Box::new(s.to_string()))) } } } macro_rules! make_currency { //(@as_ident $name:ident) => { $name }; ($(|$ticker:ident, $name:expr, $code:expr, $upper:ident),*) => { #[derive(Debug, PartialEq, Clone, Hash, Eq, Copy, PartialOrd, Ord, Serialize)] #[allow(non_camel_case_types)] #[repr(u8)] pub enum Currency { $($ticker = $code),* } impl Currency { pub fn name(&self) -> &'static str { match *self { $( c!($ticker) => { $name } ),* } } pub fn as_str(&self) -> &'static str { match *self { $( c!($ticker) => { stringify!($ticker) } ),* } } pub fn to_str_uppercase(&self) -> &'static str { match *self { $( c!($ticker) => { stringify!($upper) } ),* } } #[allow(non_upper_case_globals)] pub fn from_bytes<'a>(bytes: &'a [u8]) -> Result> { // this declares for each currency the equivalent of // const BTC : &[u8] = "btc".as_bytes(); $( const $upper: &[u8] = stringify!($ticker).as_bytes(); )* // this declares for each currency the equivalent of // const btc : &[u8] = "BTC".as_bytes(); $( const $ticker: &[u8] = stringify!($upper).as_bytes(); )* match bytes { // first try lowercase (identified by uppercase consts) $( $upper => { return Ok(Currency::$ticker) } )* // then try uppercase (identified by lowercase consts) $( $ticker => { return Ok(Currency::$ticker) } )* other => Err(UnlistedCurrency(std::str::from_utf8(other).unwrap_or(""))) } } /// Optimized for expecting lowercase. Does not attempt to search beyond /// every currency symbol in lowercase. /// pub fn from_str_lowercase<'a>(s: &'a str) -> Result> { match s { $( stringify!($ticker) => { Ok(c!($ticker)) } )* other => Err(UnlistedCurrency(other)) } } /// Optimized for expecting uppercase. Does not attempt to search beyond /// every currency symbol in uppercase. /// pub fn from_str_uppercase<'a>(s: &'a str) -> Result> { match s { $( stringify!($upper) => { Ok(c!($ticker)) } )* other => Err(UnlistedCurrency(other)) } } pub fn all() -> Vec { vec![ $( c!($ticker) ),* ] } pub fn variant_str_slice() -> &'static [&'static str] { &[ $( stringify!($ticker) ),* ] } } impl TryFrom for Currency { type Error = Error; fn try_from(n: u8) -> Result { match n { $( $code => { Ok(c!($ticker)) } ),* other => Err(Error::IllegalFormat(Box::new(format!("Illegal Currency value: {}", other)))) } } } impl From for u8 { fn from(c: Currency) -> Self { match c { $( c!($ticker) => { $code } ),* } } } impl FromStr for Currency { type Err = Error; fn from_str(s: &str) -> Result { let t = match s.to_lowercase().trim() { $( stringify!($ticker) => { c!($ticker) } ),* "xbt" => c!(btc), _ => { return Err(Error::IllegalCurrency); } }; Ok(t) } } #[test] fn it_verifies_currency_lower_and_upper_consistency() { let currencies = vec![ $(c!($ticker)),* ]; for c in ¤cies { assert_eq!(c.as_str().to_uppercase(), c.to_str_uppercase().to_string()); } } #[test] fn checks_from_bytes_for_lower_and_upper_tickers() { $( assert_eq!( Currency::from_bytes(Currency::$ticker.as_str().as_bytes()).unwrap(), Currency::$ticker ); assert_eq!( Currency::from_bytes(Currency::$ticker.to_str_uppercase().as_bytes()).unwrap(), Currency::$ticker ); )* } #[test] fn check_serde_json_deser() { #[derive(Serialize, Deserialize, PartialEq, Debug)] struct A { pub denom: Currency, } $( { let a = A { denom: Currency::$ticker }; let to_json = serde_json::to_string(&a).unwrap(); let manual_json = format!("{{\"denom\":\"{}\"}}", stringify!($ticker)); let manual_json_uppercase = format!("{{\"denom\":\"{}\"}}", stringify!($upper)); assert_eq!(a, serde_json::from_str(&to_json).unwrap()); assert_eq!(a, serde_json::from_str(&manual_json).unwrap()); assert_eq!(a, serde_json::from_str(&manual_json_uppercase).unwrap()); assert_eq!(a, serde_json::from_slice(manual_json.as_bytes()).unwrap()); assert_eq!(a, serde_json::from_slice(manual_json_uppercase.as_bytes()).unwrap()); } )* } } } make_currency!( | btc, "Bitcoin", 1, BTC, | eth, "Ethereum", 2, ETH, | xmr, "Monero", 3, XMR, | usdt, "Tether", 4, USDT, | ltc, "Litecoin", 5, LTC, | dash, "Dash", 6, DASH, | nvc, "Novacoin", 7, NVC, | ppc, "Peercoin", 8, PPC, | zec, "Zcash", 9, ZEC, | xrp, "Ripple", 10, XRP, | gnt, "Golem", 11, GNT, | steem, "Steem", 12, STEEM, | rep, "Augur", 13, REP, | gno, "Gnosis", 14, GNO, | etc, "Ethereum Classic", 15, ETC, | icn, "Iconomi", 16, ICN, | xlm, "Stellar", 17, XLM, | mln, "Melon", 18, MLN, | bcn, "Bytecoin", 19, BCN, | bch, "Bitcoin Cash", 20, BCH, | doge, "Dogecoin", 21, DOGE, | eos, "Eos", 22, EOS, | nxt, "Nxt", 23, NXT, | sc, "Siacoin", 24, SC, | zrx, "0x", 25, ZRX, | bat, "Basic Attention Token", 26, BAT, | ada, "Cardano", 27, ADA, | usdc, "USD Coin", 28, USDC, | dai, "Dai", 29, DAI, | mkr, "Maker", 30, MKR, | loom, "Loom Network", 31, LOOM, | cvc, "Civic", 32, CVC, | mana, "Decentraland", 33, MANA, | dnt, "district0x", 34, DNT, | zil, "Zilliqa", 35, ZIL, | link, "Chainlink", 36, LINK, | algo, "Algorand", 37, ALGO, | xtz, "Tezos", 38, XTZ, | oxt, "Orchid", 39, OXT, | atom, "Cosmos", 40, ATOM, // fiat: u8 code starts at 100 (can be used to filter/sort) | usd, "U.S. Dollar", 100, USD, | eur, "Euro", 101, EUR, | rur, "Ruble", 102, RUR, | jpy, "Yen", 103, JPY, | gbp, "Pound", 104, GBP, | chf, "Franc", 105, CHF, | cad, "Canadian Dollar", 106, CAD, | aud, "Australian Dollar", 107, AUD, | zar, "Rand", 108, ZAR, | mxn, "Peso", 109, MXN ); impl Currency { /// Convert fiat peg to tether equivalent. /// /// # Examples /// /// ``` /// #[macro_use] /// extern crate markets; /// fn main() { /// assert_eq!(c!(usd).to_tether(), c!(usdt)); /// assert_eq!(c!(btc).to_tether(), c!(btc)); /// } /// ``` /// pub fn to_tether(&self) -> Self { match *self { c!(usd) => c!(usdt), other => other } } /// Converts stablecoins into fiat peg. /// /// # Examples /// /// ``` /// #[macro_use] /// extern crate markets; /// fn main() { /// assert_eq!(c!(usdt).from_tether(), c!(usd)); /// assert_eq!(c!(btc).from_tether(), c!(btc)); /// } /// ``` /// pub fn from_tether(&self) -> Self { match *self { c!(usdt) => c!(usd), other => other } } } /// # Panics /// /// The `TryFrom` implementation in this macro will panic it is passed /// a value greater than the maximum value a `u8` can store. /// macro_rules! use_u8_impl_for_int { ($t:ty, $int:ty) => { impl From<$t> for $int { fn from(t: $t) -> Self { u8::from(t) as $int } } impl TryFrom<$int> for $t { type Error = Error; fn try_from(n: $int) -> Result { TryFrom::try_from(n as u8) } } } } use_u8_impl_for_int!(Currency, i16); use_u8_impl_for_int!(Exchange, i16); use_u8_impl_for_int!(Ticker, i16); use_u8_impl_for_int!(Side, i16); #[test] fn check_generated_currency_fns() { assert_eq!(c!(usd), Currency::usd); assert_eq!(c!(usd).name(), "U.S. Dollar"); assert_eq!(Currency::try_from(1_u8).unwrap(), c!(btc)); assert_eq!(u8::from(c!(usd)), 100_u8); assert_eq!(i16::from(c!(usd)), 100_i16); } struct CurrencyVisitor; struct ExchangeVisitor; struct TickerVisitor; impl<'de> Visitor<'de> for CurrencyVisitor { type Value = Currency; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a currency symbol or ticker, like those used by exchanges (e.g. btc, usd)") } fn visit_str(self, v: &str) -> Result where E: de::Error { Currency::from_str_lowercase(v) .or_else(|_| Currency::from_str_uppercase(v)) .map_err(|UnlistedCurrency(unknown)| E::unknown_variant(unknown, Currency::variant_str_slice())) } fn visit_borrowed_str(self, v: &'de str) -> Result where E: de::Error { Currency::from_str_lowercase(v) .or_else(|_| Currency::from_str_uppercase(v)) .map_err(|UnlistedCurrency(unknown)| E::unknown_variant(unknown, Currency::variant_str_slice())) } fn visit_bytes(self, v: &[u8]) -> Result where E: de::Error { Currency::from_bytes(v) .map_err(|UnlistedCurrency(unknown)| E::unknown_variant(unknown, Currency::variant_str_slice())) } fn visit_borrowed_bytes(self, v: &'de [u8]) -> Result where E: de::Error { Currency::from_bytes(v) .map_err(|UnlistedCurrency(unknown)| E::unknown_variant(unknown, Currency::variant_str_slice())) } } impl<'de> Visitor<'de> for ExchangeVisitor { type Value = Exchange; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a four-character exchange symbol (e.g. gdax, bmex)") } fn visit_str(self, v: &str) -> Result where E: de::Error { Exchange::from_str_lowercase(v) .or_else(|_| Exchange::from_str_uppercase(v)) .map_err(|UnlistedExchange(unknown)| E::unknown_variant(unknown, Exchange::variant_str_slice())) } fn visit_borrowed_str(self, v: &'de str) -> Result where E: de::Error { Exchange::from_str_lowercase(v) .or_else(|_| Exchange::from_str_uppercase(v)) .map_err(|UnlistedExchange(unknown)| E::unknown_variant(unknown, Exchange::variant_str_slice())) } fn visit_bytes(self, v: &[u8]) -> Result where E: de::Error { Exchange::from_bytes(v) .map_err(|UnlistedExchange(unknown)| E::unknown_variant(unknown, Exchange::variant_str_slice())) } fn visit_borrowed_bytes(self, v: &'de [u8]) -> Result where E: de::Error { Exchange::from_bytes(v) .map_err(|UnlistedExchange(unknown)| E::unknown_variant(unknown, Exchange::variant_str_slice())) } } impl<'de> Visitor<'de> for TickerVisitor { type Value = Ticker; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a currency rate symbol or ticker, like those used by exchanges (e.g. btc_usd)") } fn visit_str(self, v: &str) -> Result where E: de::Error { let mut parts = v.split(|c| c == '_' || c == '-' || c == '/'); match parts.next() { Some(base_str) => { match parts.next() { Some(quote_str) => { let base = Currency::from_str_lowercase(base_str) .or_else(|_| Currency::from_str_uppercase(base_str)) .map_err(|UnlistedCurrency(unknown)| E::unknown_variant(unknown, Currency::variant_str_slice()))?; let quote = Currency::from_str_lowercase(quote_str) .or_else(|_| Currency::from_str_uppercase(quote_str)) .map_err(|UnlistedCurrency(unknown)| E::unknown_variant(unknown, Currency::variant_str_slice()))?; Ok(Ticker { base, quote }) } None => Err(E::missing_field("quote")) } } None => Err(E::missing_field("base")) } } fn visit_borrowed_str(self, v: &'de str) -> Result where E: de::Error { let mut parts = v.split(|c| c == '_' || c == '-' || c == '/'); match parts.next() { Some(base_str) => { match parts.next() { Some(quote_str) => { let base = Currency::from_str_lowercase(base_str) .or_else(|_| Currency::from_str_uppercase(base_str)) .map_err(|UnlistedCurrency(unknown)| E::unknown_variant(unknown, Currency::variant_str_slice()))?; let quote = Currency::from_str_lowercase(quote_str) .or_else(|_| Currency::from_str_uppercase(quote_str)) .map_err(|UnlistedCurrency(unknown)| E::unknown_variant(unknown, Currency::variant_str_slice()))?; Ok(Ticker { base, quote }) } None => Err(E::missing_field("quote")) } } None => Err(E::missing_field("base")) } } fn visit_bytes(self, v: &[u8]) -> Result where E: de::Error { let mut parts = v.split(|&c| c == b'_' || c == b'-' || c == b'/'); match parts.next() { Some(base_str) => { match parts.next() { Some(quote_str) => { let base = Currency::from_bytes(base_str) .map_err(|UnlistedCurrency(unknown)| E::unknown_variant(unknown, Currency::variant_str_slice()))?; let quote = Currency::from_bytes(quote_str) .map_err(|UnlistedCurrency(unknown)| E::unknown_variant(unknown, Currency::variant_str_slice()))?; Ok(Ticker { base, quote }) } None => Err(E::missing_field("quote")) } } None => Err(E::missing_field("base")) } } fn visit_borrowed_bytes(self, v: &'de [u8]) -> Result where E: de::Error { let mut parts = v.split(|&c| c == b'_' || c == b'-' || c == b'/'); match parts.next() { Some(base_str) => { match parts.next() { Some(quote_str) => { let base = Currency::from_bytes(base_str) .map_err(|UnlistedCurrency(unknown)| E::unknown_variant(unknown, Currency::variant_str_slice()))?; let quote = Currency::from_bytes(quote_str) .map_err(|UnlistedCurrency(unknown)| E::unknown_variant(unknown, Currency::variant_str_slice()))?; Ok(Ticker { base, quote }) } None => Err(E::missing_field("quote")) } } None => Err(E::missing_field("base")) } } } impl <'de> Deserialize<'de> for Currency { fn deserialize(deserializer: D) -> std::result::Result where D: Deserializer<'de> { deserializer.deserialize_bytes(CurrencyVisitor) } } impl <'de> Deserialize<'de> for Exchange { fn deserialize(deserializer: D) -> std::result::Result where D: Deserializer<'de> { deserializer.deserialize_bytes(ExchangeVisitor) } } impl <'de> Deserialize<'de> for Ticker { fn deserialize(deserializer: D) -> std::result::Result where D: Deserializer<'de> { deserializer.deserialize_bytes(TickerVisitor) } } impl Display for Currency { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{:?}", self) } } macro_rules! make_exchange { ($(|$ticker:ident, $name:expr, $code:expr, $upper:ident),*) => { #[derive(Debug, PartialEq, Clone, Hash, Eq, Copy, PartialOrd, Ord, Serialize)] #[allow(non_camel_case_types)] #[repr(u8)] pub enum Exchange { $( #[doc = $name] $ticker = $code ),* } impl Exchange { pub fn name(&self) -> &'static str { match *self { $( e!($ticker) => { $name } ),* } } pub fn as_str(&self) -> &'static str { match *self { $( e!($ticker) => { stringify!($ticker) } ),* } } pub fn to_str_uppercase(&self) -> &'static str { match *self { $( e!($ticker) => { stringify!($upper) } ),* } } pub fn all() -> Vec { vec![ $( e!($ticker) ),* ] } pub fn variant_str_slice() -> &'static [&'static str] { &[ $( stringify!($ticker) ),* ] } #[allow(non_upper_case_globals)] pub fn from_bytes<'a>(bytes: &'a [u8]) -> Result> { // this declares for each currency the equivalent of // const BTC : &[u8] = "btc".as_bytes(); $( const $upper: &[u8] = stringify!($ticker).as_bytes(); )* // this declares for each currency the equivalent of // const btc : &[u8] = "BTC".as_bytes(); $( const $ticker: &[u8] = stringify!($upper).as_bytes(); )* match bytes { // first try lowercase (identified by uppercase consts) $( $upper => { return Ok(Exchange::$ticker) } )* // then try uppercase (identified by lowercase consts) $( $ticker => { return Ok(Exchange::$ticker) } )* other => Err(UnlistedExchange(std::str::from_utf8(other).unwrap_or(""))) } } /// Optimized for expecting lowercase. Does not attempt to search beyond /// every currency symbol in lowercase. /// pub fn from_str_lowercase<'a>(s: &'a str) -> Result> { match s { $( stringify!($ticker) => { Ok(Exchange::$ticker) } )* other => Err(UnlistedExchange(other)) } } /// Optimized for expecting uppercase. Does not attempt to search beyond /// every currency symbol in uppercase. /// pub fn from_str_uppercase<'a>(s: &'a str) -> Result> { match s { $( stringify!($upper) => { Ok(Exchange::$ticker) } )* other => Err(UnlistedExchange(other)) } } } impl TryFrom for Exchange { type Error = Error; fn try_from(n: u8) -> Result { match n { $( $code => { Ok(e!($ticker)) } ),* _ => Err(Error::IllegalExchangeCode(n)) } } } impl From for u8 { fn from(c: Exchange) -> Self { match c { $( e!($ticker) => { $code } ),* } } } impl FromStr for Exchange { type Err = Error; fn from_str(s: &str) -> Result { let t = match s.to_lowercase().trim() { $( stringify!($ticker) => { e!($ticker) } ),* _ => { return Err(Error::IllegalExchange); } }; Ok(t) } } /// Holds an object of the same type for each exchange. /// pub struct ByExchange { $( pub $ticker: T ),* } impl ByExchange { pub fn new( $( $ticker: T ),* ) -> Self { Self { $( $ticker ),* } } pub fn of(&self, exchange: &Exchange) -> &T { match exchange { $( &e!($ticker) => &self.$ticker ),* } } pub fn of_mut(&mut self, exchange: &Exchange) -> &mut T { match exchange { $( &e!($ticker) => &mut self.$ticker ),* } } // to match map apis: pub fn get(&self, exchange: &Exchange) -> Option<&T> { Some(self.of(exchange)) } pub fn get_mut(&mut self, exchange: &Exchange) -> Option<&mut T> { Some(self.of_mut(exchange)) } } impl Default for ByExchange where T: Default { fn default() -> Self { ByExchange { $( $ticker: Default::default() ),* } } } impl Clone for ByExchange where T: Clone { fn clone(&self) -> Self { ByExchange { $( $ticker: self.$ticker.clone() ),* } } } #[test] fn it_verifies_exch_lower_and_upper_consistency() { for x in Exchange::all() { assert_eq!(x.as_str().to_uppercase(), x.to_str_uppercase().to_string()); } } #[test] fn checks_from_bytes_for_lower_and_upper_tickers_exch() { $( assert_eq!( Exchange::from_bytes(Exchange::$ticker.as_str().as_bytes()).unwrap(), Exchange::$ticker ); assert_eq!( Exchange::from_bytes(Exchange::$ticker.to_str_uppercase().as_bytes()).unwrap(), Exchange::$ticker ); )* } #[test] fn check_serde_json_deser_exch() { #[derive(Serialize, Deserialize, PartialEq, Debug)] struct A { pub exch: Exchange, } $( { let a = A { exch: Exchange::$ticker }; let to_json = serde_json::to_string(&a).unwrap(); let manual_json = format!("{{\"exch\":\"{}\"}}", stringify!($ticker)); let manual_json_uppercase = format!("{{\"exch\":\"{}\"}}", stringify!($upper)); assert_eq!(a, serde_json::from_str(&to_json).unwrap()); assert_eq!(a, serde_json::from_str(&manual_json).unwrap()); assert_eq!(a, serde_json::from_str(&manual_json_uppercase).unwrap()); assert_eq!(a, serde_json::from_slice(manual_json.as_bytes()).unwrap()); assert_eq!(a, serde_json::from_slice(manual_json_uppercase.as_bytes()).unwrap()); } )* } } } make_exchange!( | plnx, "Poloniex", 1, PLNX, | krkn, "Kraken", 2, KRKN, | gdax, "GDAX", 3, GDAX, | exmo, "Exmo", 4, EXMO, | bits, "Bitstamp", 5, BITS, | bmex, "Bitmex", 6, BMEX, | btfx, "Bitfinex", 7, BTFX, | bnce, "Binance", 8, BNCE, | okex, "OKEx", 9, OKEX, | drbt, "Deribit", 10, DRBT ); #[test] fn check_generated_exchange_fns() { assert_eq!(e!(plnx), Exchange::plnx); assert_eq!(e!(plnx).name(), "Poloniex"); assert_eq!(Exchange::try_from(2_u8).unwrap(), e!(krkn)); assert_eq!(u8::from(e!(gdax)), 3_u8); } impl Display for Exchange { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", self.as_str()) } } #[derive(Debug, Clone, Hash, PartialEq, Eq, Copy, PartialOrd, Ord)] pub struct Ticker { pub base: Currency, pub quote: Currency, } impl Ticker { pub fn flip(&self) -> Self { Ticker { base: self.quote, quote: self.base } } pub fn to_s(&self) -> String { format!("{}", self) } } macro_rules! ticker_to_u8 { ($(|$base:tt-$quote:tt, $u_base:tt-$u_quote:tt, $n:expr),*) => { impl From for u8 { fn from(t: Ticker) -> Self { match t { $( t!($base-$quote) => { $n } ),* _ => 0 } } } impl TryFrom for Ticker { type Error = Error; fn try_from(n: u8) -> Result { match n { $( $n => { Ok(t!($base-$quote)) } ),* other => Err(Error::IllegalValue(other)) } } } impl Ticker { pub fn as_str(&self) -> &'static str { match *self { $( t!($base-$quote) => { concat!(stringify!($base), "_", stringify!($quote)) } ),* _ => "xxx_xxx" } } pub fn as_str_dash(&self) -> &'static str { match *self { $( t!($base-$quote) => { concat!(stringify!($base), "-", stringify!($quote)) } ),* _ => "xxx-xxx" } } pub fn to_str_uppercase(&self) -> &'static str { match *self { $( t!($base-$quote) => { concat!(stringify!($u_base), "_", stringify!($u_quote)) } ),* _ => "XXX_XXX" } } pub fn to_str_reversed(&self) -> &'static str { match *self { $( t!($base-$quote) => { concat!(stringify!($quote), "_", stringify!($base)) } ),* _ => "xxx_xxx" } } pub fn to_str_uppercase_reversed(&self) -> &'static str { match *self { $( t!($base-$quote) => { concat!(stringify!($u_quote), "_", stringify!($u_base)) } ),* _ => "XXX_XXX" } } /// t!(btc-usd) -> "BTC-USD" /// pub fn to_str_uppercase_dash_sep(&self) -> &'static str { match *self { $( t!($base-$quote) => { concat!(stringify!($u_base), "-", stringify!($u_quote)) } ),* _ => "XXX-XXX" } } pub fn as_gdax_str(&self) -> &'static str { self.to_str_uppercase_dash_sep() } /// Note - this performs the flip and fails for tickers not in the list. /// pub fn from_str_uppercase_reverse<'a>(s: &'a str) -> Result> { match s { $( concat!(stringify!($u_quote), "_", stringify!($u_base)) => { Ok(t!($base-$quote)) } )* other => Err(UnlistedTicker(other)) } } pub fn from_str_uppercase_dash_sep(s: &str) -> Result { match s { $( concat!(stringify!($u_base), "-", stringify!($u_quote)) => { Ok(t!($base-$quote)) } )* other => ticker_parse_last_resort(other) } } pub fn from_str_bitfinex(s: &str) -> Result { match s { $( concat!(stringify!($u_base), stringify!($u_quote)) => { Ok(t!($base-$quote)) } )* other => ticker_parse_last_resort(other) } } pub fn from_str_bitmex(s: &str) -> Result { match s { "XBTUSD" => Ok(t!(btc-usd)), "ETHUSD" => Ok(t!(eth-usd)), $( concat!(stringify!($u_base), stringify!($u_quote)) => { Ok(t!($base-$quote)) } )* other => ticker_parse_last_resort(other) } } } fn ticker_parse_last_resort(other: &str) -> Result { match other.find('_').or_else(|| other.find('-')).or_else(|| other.find('/')) { Some(sep) if sep < other.len() - 1 => { let base = Currency::from_str(&other[..sep])?; let quote = Currency::from_str(&other[sep + 1..])?; Ok(Ticker { base, quote }) } _ => Err(Error::IllegalFormat(Box::new(format!("failed to parse Ticker '{}'", other)))) } } impl FromStr for Ticker { type Err = Error; fn from_str(s: &str) -> Result { match s { $( concat!(stringify!($base), "_", stringify!($quote)) //| concat!(stringify!($base), "-", stringify!($quote)) //| concat!(stringify!($base), "/", stringify!($quote)) //| concat!(stringify!($u_base), "_", stringify!($u_quote)) //| concat!(stringify!($u_base), "-", stringify!($u_quote)) //| concat!(stringify!($u_base), "-", stringify!($u_quote)) => { Ok(t!($base-$quote)) } )* other => ticker_parse_last_resort(other) } } } #[test] fn it_verifies_ticker_lower_and_upper_consistency() { let tickers = vec![ $(t!($base-$quote)),*]; for t in &tickers { assert_eq!(t.as_str().to_uppercase(), t.to_str_uppercase().to_string()); } } #[test] fn check_serde_json_deser_ticker() { #[derive(Serialize, Deserialize, PartialEq, Debug)] struct A { pub ticker: Ticker, } $( { let a = A { ticker: Ticker { base: Currency::$base, quote: Currency::$quote } }; let to_json = serde_json::to_string(&a).unwrap(); assert_eq!(a, serde_json::from_str(&to_json).unwrap()); for sep in ["_", "-", "/"].iter() { let manual_json = format!("{{\"ticker\":\"{}{}{}\"}}", stringify!($base), sep, stringify!($quote)); let manual_json_uppercase = format!("{{\"ticker\":\"{}{}{}\"}}", stringify!($u_base), sep, stringify!($u_quote)); assert_eq!(a, serde_json::from_str(&manual_json).unwrap()); assert_eq!(a, serde_json::from_str(&manual_json_uppercase).unwrap()); assert_eq!(a, serde_json::from_slice(manual_json.as_bytes()).unwrap()); assert_eq!(a, serde_json::from_slice(manual_json_uppercase.as_bytes()).unwrap()); } } )* } } } ticker_to_u8!( |btc-usd, BTC-USD, 1, |xmr-usd, XMR-USD, 2, |eth-usd, ETH-USD, 3, |ltc-usd, LTC-USD, 4, |etc-usd, ETC-USD, 5, |xrp-usd, XRP-USD, 6, |bch-usd, BCH-USD, 7, |zec-usd, ZEC-USD, 8, |dash-usd, DASH-USD, 9, |rep-usd, REP-USD, 10, |btc-usdt, BTC-USDT, 11, |xmr-usdt, XMR-USDT, 12, |eth-usdt, ETH-USDT, 13, |ltc-usdt, LTC-USDT, 14, |etc-usdt, ETC-USDT, 15, |xrp-usdt, XRP-USDT, 16, |bch-usdt, BCH-USDT, 17, |zec-usdt, ZEC-USDT, 18, |dash-usdt,DASH-USDT,19, |rep-usdt, REP-USDT, 20, |xmr-btc, XMR-BTC, 21, |eth-btc, ETH-BTC, 22, |ltc-btc, LTC-BTC, 23, |etc-btc, ETC-BTC, 24, |xrp-btc, XRP-BTC, 25, |bch-btc, BCH-BTC, 26, |zec-btc, ZEC-BTC, 27, |dash-btc, DASH-BTC, 28, |rep-btc, REP-BTC, 29, |zec-eth, ZEC-ETH, 30, |etc-eth, ETC-ETH, 31, |bch-eth, BCH-ETH, 32, |rep-eth, REP-ETH, 33, |btc-eur, BTC-EUR, 34, |btc-jpy, BTC-JPY, 35, |btc-gbp, BTC-GBP, 36, |btc-cad, BTC-CAD, 37, |eth-eur, ETH-EUR, 38, |eth-jpy, ETH-JPY, 39, |eth-gbp, ETH-GBP, 40, |eth-cad, ETH-CAD, 41, |ltc-eur, LTC-EUR, 42, |ltc-jpy, LTC-JPY, 43, |ltc-gbp, LTC-GBP, 44, |ltc-cad, LTC-CAD, 45, |xmr-eur, XMR-EUR, 46, |bch-eur, BCH-EUR, 47, |rep-eur, REP-EUR, 48, |etc-eur, ETC-EUR, 49, |usdt-usd, USDT-USD, 50 // Note: a trailing comma will throw off the whole thing. ); #[test] fn check_generated_ticker_fns() { assert_eq!(t!(btc-usd), Ticker { base: Currency::btc, quote: Currency::usd }); assert_eq!(t!(btc-usd).as_str(), "btc_usd"); assert_eq!(t!(eth-jpy).as_str(), "eth_jpy"); assert_eq!(t!(usd-btc).as_str(), "xxx_xxx"); assert_eq!(u8::from(t!(btc-usd)), 1_u8); assert_eq!(u8::from(t!(btc-cad)), 37_u8); assert_eq!(Ticker::try_from(1_u8).unwrap(), t!(btc-usd)); assert_eq!(Ticker::try_from(21_u8).unwrap(), t!(xmr-btc)); assert!(Ticker::try_from(121_u8).is_err()); } impl Into for Ticker { fn into(self) -> String { format!("{}_{}", self.base, self.quote) } } impl From<(Currency, Currency)> for Ticker { fn from(basequote: (Currency, Currency)) -> Self { let (base, quote) = basequote; Ticker { base, quote } } } impl Serialize for Ticker { fn serialize(&self, serializer: S) -> ::std::result::Result where S: Serializer { serializer.serialize_str(&format!("{}_{}", self.base, self.quote)) } } impl Display for Ticker { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}_{}", self.base, self.quote) } } #[derive(Debug, Clone)] pub enum Error { IllegalCurrency, IllegalExchange, IllegalValue(u8), IllegalFormat(Box), IllegalExchangeCode(u8), NaN, } impl Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{:?}", self) } } impl ::std::error::Error for Error { fn description(&self) -> &str { "MoneyError" } } #[derive(Debug)] pub struct UnlistedTicker<'a>(pub &'a str); impl<'a> Display for UnlistedTicker<'a> { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "UnlistedTicker({})", self.0) } } #[derive(Debug)] pub struct UnlistedCurrency<'a>(&'a str); impl<'a> Display for UnlistedCurrency<'a> { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "UnlistedCurrency({})", self.0) } } #[derive(Debug)] pub struct UnlistedExchange<'a>(&'a str); impl<'a> Display for UnlistedExchange<'a> { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "UnlistedExchange({})", self.0) } } #[allow(unused)] #[cfg(test)] mod tests { use super::*; #[cfg(feature = "unstable")] use test::{black_box, Bencher}; #[test] fn ticker_from_str_advanced() { assert_eq!(Ticker::from_str("btc_usd").unwrap(), t!(btc-usd)); assert_eq!(Ticker::from_str("btc-usd").unwrap(), t!(btc-usd)); assert_eq!(Ticker::from_str("btc/usd").unwrap(), t!(btc-usd)); assert_eq!(Ticker::from_str("BTC_USD").unwrap(), t!(btc-usd)); assert_eq!(Ticker::from_str("BTC-USD").unwrap(), t!(btc-usd)); assert_eq!(Ticker::from_str("BTC/USD").unwrap(), t!(btc-usd)); assert_eq!(Ticker::from_str("USD_BTC").unwrap(), t!(usd-btc)); assert!(Ticker::from_str("not_a_ticker_format").is_err()); } #[test] fn it_parses_bcn_ticker() { let ticker = Ticker::from_str("bcn_btc").unwrap(); } #[test] fn it_checks_simple_macro_use() { let btc_cur = c!(btc); assert_eq!(btc_cur, Currency::btc); let plnx_exch = e!(plnx); assert_eq!(plnx_exch, Exchange::plnx); let eth_usd_ticker = t!(eth-usd); assert_eq!(eth_usd_ticker.base, c!(eth)); assert_eq!(eth_usd_ticker.quote, c!(usd)); } #[cfg(feature = "unstable")] #[bench] fn ticker_to_str_obfuscated(b: &mut Bencher) { let ticker = Ticker::from_str("btc_usd").unwrap(); b.iter(|| { black_box(ticker.as_str()) }); } #[cfg(feature = "unstable")] #[bench] fn ticker_from_str_bench(b: &mut Bencher) { let t = black_box(String::from("btc_usd")); b.iter(||{ Ticker::from_str(&t).unwrap() }); } #[cfg(feature = "unstable")] #[bench] fn ticker_from_str_bench_last(b: &mut Bencher) { let t = black_box(String::from("ltc_cad")); b.iter(||{ Ticker::from_str(&t).unwrap() }); } #[cfg(feature = "unstable")] #[bench] fn ticker_from_str_bench_catchall(b: &mut Bencher) { let t = black_box(String::from("usd_zar")); b.iter(||{ Ticker::from_str(&t).unwrap() }); } #[cfg(feature = "unstable")] #[bench] fn it_converts_ticker_to_u8(b: &mut Bencher) { let t = black_box(t!(btc-usd)); b.iter(|| { u8::from(black_box(t)) }); } #[cfg(feature = "unstable")] #[bench] fn it_converts_ticker_u8_plus_base_and_quote(b: &mut Bencher) { struct A { pub ticker: i16, pub base: i16, pub quote: i16 } let t = black_box(t!(btc-usd)); b.iter(|| { let ticker = u8::from(black_box(t)) as i16; let base = u8::from(black_box(t.base)) as i16; let quote = u8::from(black_box(t.quote)) as i16; A { ticker, base, quote } }); } #[test] fn it_asserts_that_ticker_takes_up_two_bytes() { assert_eq!(::std::mem::size_of::(), 2); } #[test] fn it_checks_a_currency_to_str_return_value() { assert_eq!(c!(btc).as_str(), "btc"); } #[cfg(feature = "unstable")] #[bench] fn ticker_to_string(b: &mut Bencher) { let ticker = t!(btc-usd); b.iter(|| { black_box(ticker.to_string()) }); } #[cfg(feature = "unstable")] #[bench] fn ticker_to_s(b: &mut Bencher) { let ticker = t!(btc-usd); b.iter(|| { black_box(ticker.to_s()) }); } #[cfg(feature = "unstable")] #[bench] fn ticker_to_str(b: &mut Bencher) { let ticker = t!(btc-usd); b.iter(|| { black_box(ticker.as_str()) }); } #[cfg(feature = "unstable")] #[bench] fn currency_to_str(b: &mut Bencher) { let usd_currency = c!(usd); b.iter(|| { usd_currency.as_str() }); } #[cfg(feature = "unstable")] #[bench] fn currency_to_str_uppercase(b: &mut Bencher) { let usd_currency = c!(usd); b.iter(|| { usd_currency.to_str_uppercase() }); } }