diff --git a/Cargo.toml b/Cargo.toml index 708dc67..6dc4b08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "markets" -version = "0.1.0" +version = "0.2.0" authors = ["Jonathan Strong "] edition = "2018" @@ -10,5 +10,10 @@ edition = "2018" serde = { version = "1", features = ["derive"] } chrono = { version = "0.4", features = ["serde"] } chrono-tz = "0.4" -uuid = { version = "0.7.0", features = ["serde", "v4", "slog"] } +#uuid = { version = "0.7.0", features = ["serde", "v4", "slog"] } hashbrown = { version = "0.6.3", default_features = false, features = ["serde", "ahash"] } +#decimal = { git = "https://git.mmcxi.com/mm/decimal", branch = "v2.3.x" } +#decimal-macros = { git = "https://git.mmcxi.com/mm/decimal-macros", rev = "v0.2.1", optional = true } + +[features] +unstable = [] diff --git a/src/crypto.rs b/src/crypto.rs new file mode 100644 index 0000000..22cb000 --- /dev/null +++ b/src/crypto.rs @@ -0,0 +1,1089 @@ +//! 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}; +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 +/// +#[derive(Debug, PartialEq, Clone, Copy, Eq, Serialize)] +pub enum Side { + Bid, + Ask +} + +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 { + ($(|$ticker:ident, $name:expr, $code:expr, $upper:expr),*) => { + #[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) } + ),* + } + } + + #[deprecated(since="0.2.3", note="Use `as_str()`")] + pub fn to_str(&self) -> &'static str { self.as_str() } + + pub fn to_str_uppercase(&self) -> &'static str { + match *self { + $( + c!($ticker) => { stringify!($upper) } + ),* + } + } + + /// 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)) + } + } + } + + 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()); + } + } + } +} + +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); +} + +impl <'de> Deserialize<'de> for Currency { + fn deserialize(deserializer: D) -> std::result::Result + where D: Deserializer<'de> { + + use serde::de::Error; + let c = String::deserialize(deserializer)?; + Currency::from_str(&c).map_err(|err| D::Error::custom(format!("{:?}", err))) + } +} + +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),*) => { + #[derive(Debug, PartialEq, Clone, Hash, Eq, Copy, PartialOrd, Ord, + Serialize, Deserialize)] + #[allow(non_camel_case_types)] + #[repr(u8)] + pub enum Exchange { + $($ticker = $code),* + } + + impl Exchange { + pub fn name(&self) -> &'static str { + match *self { + $( + e!($ticker) => { $name } + ),* + } + } + + #[deprecated(since="0.2.3", note="Use `as_str()`")] + pub fn to_str(&self) -> &'static str { self.as_str() } + + pub fn as_str(&self) -> &'static str { + match *self { + $( + e!($ticker) => { stringify!($ticker) } + ),* + } + } + } + + 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() + ),* + } + } + } + } +} + +make_exchange!( + | plnx, "Poloniex", 1, + | krkn, "Kraken", 2, + | gdax, "GDAX", 3, + | exmo, "Exmo", 4, + | bits, "Bitstamp", 5, + | bmex, "Bitmex", 6, + | btfx, "Bitfinex", 7, + | bnce, "Binance", 8, + | okex, "OKEx", 9, + | drbt, "Deribit", 10 +); + +#[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" + } + } + + #[deprecated(since="0.2.3", note="Use `as_str()`")] + pub fn to_str(&self) -> &'static str { self.as_str() } + + 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()); + } + } + } +} + +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 { + + let s: String = format!("{}_{}", self.base, self.quote); + serializer.serialize_str(&s) + } +} + +impl <'de> Deserialize<'de> for Ticker { + fn deserialize(deserializer: D) -> std::result::Result + where D: Deserializer<'de> { + + let s = String::deserialize(deserializer)?; + Ticker::from_str(s.as_str()) + .map_err(|_| de::Error::custom(format!("invalid Ticker '{}'", s))) + } +} + +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>(&'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) + } +} + +#[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() + }); + } +} diff --git a/src/iso.rs b/src/iso.rs new file mode 100644 index 0000000..f74dc65 --- /dev/null +++ b/src/iso.rs @@ -0,0 +1,46 @@ +use serde::{Serialize, Deserialize}; +use chrono_tz::Tz; +use chrono_tz::US::{Eastern, Central, Pacific}; +use chrono_tz::Etc::GMTPlus5; + +use Market::*; + +#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)] +#[repr(u8)] +pub enum Market { + Pjm = 1, + Miso = 2, + Caiso = 3, + Ercot = 4, + Spp = 5, + Nyiso = 6, + Iso = 7, +} + +impl Market { + pub fn time_zone(&self) -> Tz { + match self { + Pjm => Eastern, + Miso => GMTPlus5, + Caiso => Pacific, + Ercot => Central, + Spp => Central, + Nyiso => Eastern, + Iso => Eastern, + } + } + + pub fn as_str(&self) -> &'static str { + match self { + Pjm => "pjm", + Miso => "miso", + Caiso => "caiso", + Ercot => "ercot", + Spp => "spp", + Nyiso => "nyiso", + Iso => "isone", + } + } +} + + diff --git a/src/lib.rs b/src/lib.rs index 289c7a1..98db712 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,52 +1,8 @@ -use serde::{Serialize, Deserialize}; -use chrono_tz::Tz; -use chrono_tz::US::{Eastern, Central, Pacific}; -use chrono_tz::Etc::GMTPlus5; +#![cfg_attr(all(test, feature = "unstable"), feature(test))] -use Market::*; +#[cfg(all(test, feature = "unstable"))] +extern crate test; -#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)] -#[repr(u8)] -pub enum Market { - Pjm = 1, - Miso = 2, - Caiso = 3, - Ercot = 4, - Spp = 5, - Nyiso = 6, - Iso = 7, -} - -impl Market { - pub fn time_zone(&self) -> Tz { - match self { - Pjm => Eastern, - Miso => GMTPlus5, - Caiso => Pacific, - Ercot => Central, - Spp => Central, - Nyiso => Eastern, - Iso => Eastern, - } - } - - pub fn as_str(&self) -> &'static str { - match self { - Pjm => "pjm", - Miso => "miso", - Caiso => "caiso", - Ercot => "ercot", - Spp => "spp", - Nyiso => "nyiso", - Iso => "isone", - } - } -} - -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); - } -} +pub mod iso; +//#[macro_use] +pub mod crypto;