|
- //! 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<u8> for Side {
- type Error = Error;
- fn try_from(n: u8) -> Result<Self, Self::Error> {
- 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<Side> 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<D>(deserializer: D) -> std::result::Result<Self, D::Error>
- 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<Self, Error> {
- 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<Self, UnlistedCurrency<'a>> {
- // 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("<utf8 error>")))
- }
- }
-
- /// 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<Self, UnlistedCurrency<'a>> {
- 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<Self, UnlistedCurrency<'a>> {
- match s {
- $(
- stringify!($upper) => { Ok(c!($ticker)) }
-
- )*
-
- other => Err(UnlistedCurrency(other))
- }
- }
-
- pub fn all() -> Vec<Self> {
- vec![
- $( c!($ticker) ),*
- ]
- }
-
- pub fn variant_str_slice() -> &'static [&'static str] {
- &[
- $(
- stringify!($ticker)
- ),*
- ]
- }
- }
-
- impl TryFrom<u8> for Currency {
- type Error = Error;
- fn try_from(n: u8) -> Result<Self, Self::Error> {
- match n {
- $(
- $code => { Ok(c!($ticker)) }
- ),*
-
- other => Err(Error::IllegalFormat(Box::new(format!("Illegal Currency value: {}", other))))
- }
- }
- }
-
- impl From<Currency> 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<Self, Self::Err> {
- 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<Self, Self::Error> {
- 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<E>(self, v: &str) -> Result<Self::Value, E> 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<E>(self, v: &'de str) -> Result<Self::Value, E> 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<E>(self, v: &[u8]) -> Result<Self::Value, E> where
- E: de::Error
- {
- Currency::from_bytes(v)
- .map_err(|UnlistedCurrency(unknown)| E::unknown_variant(unknown, Currency::variant_str_slice()))
- }
-
- fn visit_borrowed_bytes<E>(self, v: &'de [u8]) -> Result<Self::Value, E> 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<E>(self, v: &str) -> Result<Self::Value, E> 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<E>(self, v: &'de str) -> Result<Self::Value, E> 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<E>(self, v: &[u8]) -> Result<Self::Value, E> where
- E: de::Error
- {
- Exchange::from_bytes(v)
- .map_err(|UnlistedExchange(unknown)| E::unknown_variant(unknown, Exchange::variant_str_slice()))
- }
-
- fn visit_borrowed_bytes<E>(self, v: &'de [u8]) -> Result<Self::Value, E> 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<E>(self, v: &str) -> Result<Self::Value, E> 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<E>(self, v: &'de str) -> Result<Self::Value, E> 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<E>(self, v: &[u8]) -> Result<Self::Value, E> 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<E>(self, v: &'de [u8]) -> Result<Self::Value, E> 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<D>(deserializer: D) -> std::result::Result<Self, D::Error>
- where D: Deserializer<'de>
- {
- deserializer.deserialize_bytes(CurrencyVisitor)
- }
- }
-
- impl <'de> Deserialize<'de> for Exchange {
- fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
- where D: Deserializer<'de>
- {
- deserializer.deserialize_bytes(ExchangeVisitor)
- }
- }
-
- impl <'de> Deserialize<'de> for Ticker {
- fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
- 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 {
- $($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<Self> {
- 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<Self, UnlistedExchange<'a>> {
- // 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("<utf8 error>")))
- }
- }
-
- /// 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<Self, UnlistedExchange<'a>> {
- 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<Self, UnlistedExchange<'a>> {
- match s {
- $(
- stringify!($upper) => { Ok(Exchange::$ticker) }
-
- )*
-
- other => Err(UnlistedExchange(other))
- }
- }
- }
-
- impl TryFrom<u8> for Exchange {
- type Error = Error;
- fn try_from(n: u8) -> Result<Self, Self::Error> {
- match n {
- $(
- $code => { Ok(e!($ticker)) }
- ),*
-
- _ => Err(Error::IllegalExchangeCode(n))
- }
- }
- }
-
- impl From<Exchange> 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<Self, Self::Err> {
- 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<T> {
- $(
- pub $ticker: T
- ),*
- }
-
- impl<T> ByExchange<T> {
- 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<T> Default for ByExchange<T>
- where T: Default
- {
- fn default() -> Self {
- ByExchange {
- $(
- $ticker: Default::default()
- ),*
- }
- }
- }
-
- impl<T> Clone for ByExchange<T>
- 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<Ticker> for u8 {
- fn from(t: Ticker) -> Self {
- match t {
- $(
- t!($base-$quote) => { $n }
- ),*
-
- _ => 0
- }
- }
- }
-
- impl TryFrom<u8> for Ticker {
- type Error = Error;
- fn try_from(n: u8) -> Result<Self, Self::Error> {
- 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<Ticker, UnlistedTicker<'a>> {
- 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<Ticker, Error> {
- 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<Ticker, Error> {
- 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<Ticker, Error> {
- 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<Ticker, Error> {
- 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<Self, Error> {
- 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<String> 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<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
- 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<String>),
- 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::<Ticker>(), 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()
- });
- }
- }
|