use crate::{impl_sqlx_type_display_from_str, olivia}; use anyhow::{Context, Result}; use bdk::bitcoin::{Address, Amount, Denomination}; use chrono::DateTime; use reqwest::Url; use rust_decimal::prelude::ToPrimitive; use rust_decimal::Decimal; use serde::de::Error as _; use serde::{Deserialize, Serialize}; use serde_with::{DeserializeFromStr, SerializeDisplay}; use std::convert::TryInto; use std::num::NonZeroU8; use std::ops::{Add, Div, Mul, Sub}; use std::time::{SystemTime, UNIX_EPOCH}; use std::{fmt, str}; use time::{OffsetDateTime, PrimitiveDateTime, Time}; pub mod cfd; #[derive(thiserror::Error, Debug)] pub enum Error { #[error("Price of zero is not allowed.")] ZeroPrice, #[error("Negative Price is unimplemented.")] NegativePrice, } #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct Usd(Decimal); impl Usd { pub fn new(value: Decimal) -> Self { Self(value) } pub fn try_into_u64(&self) -> Result { self.0.to_u64().context("could not fit decimal into u64") } pub fn try_into_f64(&self) -> Result { self.0.to_f64().context("Could not fit decimal into f64") } #[must_use] pub fn into_decimal(self) -> Decimal { self.0 } } impl fmt::Display for Usd { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.round_dp(2).fmt(f) } } impl str::FromStr for Usd { type Err = anyhow::Error; fn from_str(s: &str) -> Result { let dec = Decimal::from_str(s)?; Ok(Usd(dec)) } } impl_sqlx_type_display_from_str!(Usd); #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct Price(Decimal); impl_sqlx_type_display_from_str!(Price); impl Price { pub fn new(value: Decimal) -> Result { if value == Decimal::ZERO { return Result::Err(Error::ZeroPrice); } if value < Decimal::ZERO { return Result::Err(Error::NegativePrice); } Ok(Self(value)) } pub fn try_into_u64(&self) -> Result { self.0.to_u64().context("Could not fit decimal into u64") } pub fn try_into_f64(&self) -> Result { self.0.to_f64().context("Could not fit decimal into f64") } #[must_use] pub fn into_decimal(self) -> Decimal { self.0 } } impl fmt::Display for Price { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } impl str::FromStr for Price { type Err = anyhow::Error; fn from_str(s: &str) -> Result { let dec = Decimal::from_str(s)?; Ok(Price(dec)) } } #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct InversePrice(Decimal); impl InversePrice { pub fn new(value: Price) -> Result { if value.0 == Decimal::ZERO { return Result::Err(Error::ZeroPrice); } if value.0 < Decimal::ZERO { return Result::Err(Error::NegativePrice); } Ok(Self(Decimal::ONE / value.0)) } pub fn try_into_u64(&self) -> Result { self.0.to_u64().context("Could not fit decimal into u64") } pub fn try_into_f64(&self) -> Result { self.0.to_f64().context("Could not fit decimal into f64") } } #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, sqlx::Type)] #[sqlx(transparent)] pub struct Leverage(u8); impl Leverage { pub fn new(value: u8) -> Result { let val = NonZeroU8::new(value).context("Cannot use non-positive values")?; Ok(Self(u8::from(val))) } pub fn get(&self) -> u8 { self.0 } } // add impl's to do algebra with Usd, Leverage, and ExhangeRate as required impl Mul for Usd { type Output = Usd; fn mul(self, rhs: Leverage) -> Self::Output { let value = self.0 * Decimal::from(rhs.0); Self(value) } } impl Div for Usd { type Output = Usd; fn div(self, rhs: Leverage) -> Self::Output { Self(self.0 / Decimal::from(rhs.0)) } } impl Mul for Leverage { type Output = Usd; fn mul(self, rhs: Usd) -> Self::Output { let value = Decimal::from(self.0) * rhs.0; Usd(value) } } impl Mul for Usd { type Output = Usd; fn mul(self, rhs: u8) -> Self::Output { let value = self.0 * Decimal::from(rhs); Self(value) } } impl Div for Usd { type Output = Usd; fn div(self, rhs: u8) -> Self::Output { let value = self.0 / Decimal::from(rhs); Self(value) } } impl Div for Price { type Output = Price; fn div(self, rhs: u8) -> Self::Output { let value = self.0 / Decimal::from(rhs); Self(value) } } impl Add for Usd { type Output = Usd; fn add(self, rhs: Usd) -> Self::Output { let value = self.0 + rhs.0; Self(value) } } impl Sub for Usd { type Output = Usd; fn sub(self, rhs: Usd) -> Self::Output { let value = self.0 - rhs.0; Self(value) } } impl Div for Usd { type Output = Amount; fn div(self, rhs: Price) -> Self::Output { let mut btc = self.0 / rhs.0; btc.rescale(8); Amount::from_str_in(&btc.to_string(), Denomination::Bitcoin) .expect("Error computing BTC amount") } } impl Mul for Price { type Output = Price; fn mul(self, rhs: Leverage) -> Self::Output { let value = self.0 * Decimal::from(rhs.0); Self(value) } } impl Mul for Leverage { type Output = Price; fn mul(self, rhs: Price) -> Self::Output { let value = Decimal::from(self.0) * rhs.0; Price(value) } } impl Div for Price { type Output = Price; fn div(self, rhs: Leverage) -> Self::Output { let value = self.0 / Decimal::from(rhs.0); Self(value) } } impl Mul for Usd { type Output = Amount; fn mul(self, rhs: InversePrice) -> Self::Output { let mut btc = self.0 * rhs.0; btc.rescale(8); Amount::from_str_in(&btc.to_string(), Denomination::Bitcoin) .expect("Error computing BTC amount") } } impl Mul for InversePrice { type Output = InversePrice; fn mul(self, rhs: Leverage) -> Self::Output { let value = self.0 * Decimal::from(rhs.0); Self(value) } } impl Mul for Leverage { type Output = InversePrice; fn mul(self, rhs: InversePrice) -> Self::Output { let value = Decimal::from(self.0) * rhs.0; InversePrice(value) } } impl Div for InversePrice { type Output = InversePrice; fn div(self, rhs: Leverage) -> Self::Output { let value = self.0 / Decimal::from(rhs.0); Self(value) } } impl Add for Price { type Output = Price; fn add(self, rhs: Price) -> Self::Output { Self(self.0 + rhs.0) } } impl Sub for Price { type Output = Price; fn sub(self, rhs: Price) -> Self::Output { Self(self.0 - rhs.0) } } impl Add for InversePrice { type Output = InversePrice; fn add(self, rhs: InversePrice) -> Self::Output { Self(self.0 + rhs.0) } } impl Sub for InversePrice { type Output = InversePrice; fn sub(self, rhs: InversePrice) -> Self::Output { Self(self.0 - rhs.0) } } impl Add for Leverage { type Output = Leverage; fn add(self, rhs: u8) -> Self::Output { Self(self.0 + rhs) } } impl Add for u8 { type Output = Leverage; fn add(self, rhs: Leverage) -> Self::Output { Leverage(self + rhs.0) } } impl Div for Leverage { type Output = Decimal; fn div(self, rhs: Leverage) -> Self::Output { Decimal::from(self.0) / Decimal::from(rhs.0) } } #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] pub struct Percent(Decimal); impl Percent { #[must_use] pub fn round_dp(self, digits: u32) -> Self { Self(self.0.round_dp(digits)) } } impl fmt::Display for Percent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.round_dp(2).fmt(f) } } impl From for Percent { fn from(decimal: Decimal) -> Self { Percent(decimal) } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, sqlx::Type)] pub enum TradingPair { BtcUsd, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, sqlx::Type)] pub enum Position { Long, Short, } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct Identity(x25519_dalek::PublicKey); impl Identity { pub fn new(key: x25519_dalek::PublicKey) -> Self { Self(key) } } impl Serialize for Identity { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { serializer.collect_str(self) } } impl<'de> Deserialize<'de> for Identity { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let hex = String::deserialize(deserializer)?; let mut bytes = [0u8; 32]; hex::decode_to_slice(&hex, &mut bytes).map_err(D::Error::custom)?; Ok(Self(x25519_dalek::PublicKey::from(bytes))) } } impl fmt::Display for Identity { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", hex::encode(self.0.as_bytes())) } } impl str::FromStr for Identity { type Err = hex::FromHexError; fn from_str(s: &str) -> Result { let mut key = [0u8; 32]; hex::decode_to_slice(s, &mut key)?; Ok(Self(key.into())) } } impl_sqlx_type_display_from_str!(Identity); #[derive(Debug, Clone)] pub struct WalletInfo { pub balance: Amount, pub address: Address, pub last_updated_at: Timestamp, } #[derive( Debug, Clone, Copy, SerializeDisplay, DeserializeFromStr, PartialEq, Eq, Hash, PartialOrd, Ord, )] pub struct BitMexPriceEventId { /// The timestamp this price event refers to. timestamp: OffsetDateTime, digits: usize, } impl BitMexPriceEventId { pub fn new(timestamp: OffsetDateTime, digits: usize) -> Self { let (hours, minutes, seconds) = timestamp.time().as_hms(); let time_without_nanos = Time::from_hms(hours, minutes, seconds).expect("original timestamp was valid"); let timestamp_without_nanos = timestamp.replace_time(time_without_nanos); Self { timestamp: timestamp_without_nanos, digits, } } pub fn with_20_digits(timestamp: OffsetDateTime) -> Self { Self::new(timestamp, 20) } /// Checks whether this event has likely already occurred. /// /// We can't be sure about it because our local clock might be off from the oracle's clock. pub fn has_likely_occured(&self) -> bool { let now = OffsetDateTime::now_utc(); now > self.timestamp } pub fn to_olivia_url(self) -> Url { "https://h00.ooo" .parse::() .expect("valid URL from constant") .join(&self.to_string()) .expect("Event id can be joined") } pub fn timestamp(&self) -> OffsetDateTime { self.timestamp } } impl fmt::Display for BitMexPriceEventId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "/x/BitMEX/BXBT/{}.price?n={}", self.timestamp .format(&olivia::EVENT_TIME_FORMAT) .expect("should always format and we can't return an error here"), self.digits ) } } impl str::FromStr for BitMexPriceEventId { type Err = anyhow::Error; fn from_str(s: &str) -> Result { let remaining = s.trim_start_matches("/x/BitMEX/BXBT/"); let (timestamp, rest) = remaining.split_at(19); let digits = rest.trim_start_matches(".price?n="); Ok(Self { timestamp: PrimitiveDateTime::parse(timestamp, &olivia::EVENT_TIME_FORMAT) .with_context(|| format!("Failed to parse {} as timestamp", timestamp))? .assume_utc(), digits: digits.parse()?, }) } } impl_sqlx_type_display_from_str!(BitMexPriceEventId); #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, sqlx::Type)] pub struct Timestamp(i64); impl Timestamp { pub fn new(seconds: i64) -> Self { Self(seconds) } pub fn now() -> Self { let seconds: i64 = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("time not to go backwards") .as_secs() .try_into() .expect("seconds of system time to fit into i64"); Self(seconds) } pub fn parse_from_rfc3339(datetime_str: &str) -> Result { let datetime = DateTime::parse_from_rfc3339(datetime_str) .context("Unable to parse datetime as RFC3339")?; let seconds = datetime.timestamp(); Ok(Self(seconds)) } pub fn seconds(&self) -> i64 { self.0 } pub fn seconds_u64(&self) -> Result { let out = self.0.try_into().context("Unable to convert i64 to u64")?; Ok(out) } } #[cfg(test)] mod tests { use rust_decimal_macros::dec; use time::macros::datetime; use super::*; #[test] fn to_olivia_url() { let url = BitMexPriceEventId::with_20_digits(datetime!(2021-09-23 10:00:00).assume_utc()) .to_olivia_url(); assert_eq!( url, "https://h00.ooo/x/BitMEX/BXBT/2021-09-23T10:00:00.price?n=20" .parse() .unwrap() ); } #[test] fn parse_event_id() { let parsed = "/x/BitMEX/BXBT/2021-09-23T10:00:00.price?n=20" .parse::() .unwrap(); let expected = BitMexPriceEventId::with_20_digits(datetime!(2021-09-23 10:00:00).assume_utc()); assert_eq!(parsed, expected); } #[test] fn new_event_has_no_nanos() { let now = BitMexPriceEventId::with_20_digits(OffsetDateTime::now_utc()); assert_eq!(now.timestamp.nanosecond(), 0); } #[test] fn has_occured_if_in_the_past() { let past_event = BitMexPriceEventId::with_20_digits(datetime!(2021-09-23 10:00:00).assume_utc()); assert!(past_event.has_likely_occured()); } #[test] fn algebra_with_usd() { let usd_0 = Usd::new(dec!(1.234)); let usd_1 = Usd::new(dec!(9.876)); let usd_sum = usd_0 + usd_1; let usd_diff = usd_0 - usd_1; let half = usd_0 / 2; let double = usd_1 * 2; assert_eq!(usd_sum.0, dec!(11.110)); assert_eq!(usd_diff.0, dec!(-8.642)); assert_eq!(half.0, dec!(0.617)); assert_eq!(double.0, dec!(19.752)); } #[test] fn usd_for_1_btc_buys_1_btc() { let usd = Usd::new(dec!(61234.5678)); let price = Price::new(dec!(61234.5678)).unwrap(); let inv_price = InversePrice::new(price).unwrap(); let res_0 = usd / price; let res_1 = usd * inv_price; assert_eq!(res_0, Amount::ONE_BTC); assert_eq!(res_1, Amount::ONE_BTC); } #[test] fn leverage_does_not_alter_type() { let usd = Usd::new(dec!(61234.5678)); let leverage = Leverage::new(3).unwrap(); let res = usd * leverage / leverage; assert_eq!(res.0, usd.0); } #[test] fn test_algebra_with_types() { let usd = Usd::new(dec!(61234.5678)); let leverage = Leverage::new(5).unwrap(); let price = Price::new(dec!(61234.5678)).unwrap(); let expected_buyin = Amount::from_str_in("0.2", Denomination::Bitcoin).unwrap(); let liquidation_price = price * leverage / (leverage + 1); let inv_price = InversePrice::new(price).unwrap(); let inv_liquidation_price = InversePrice::new(liquidation_price).unwrap(); let long_buyin = usd / (price * leverage); let long_payout = (usd / leverage) * ((leverage + 1) * inv_price - leverage * inv_liquidation_price); assert_eq!(long_buyin, expected_buyin); assert_eq!(long_payout, Amount::ZERO); } #[test] fn test_timestamp() { let datetime_str_a = "1999-12-31T23:59:00.00Z"; let datetime_str_b = "1999-12-31T23:59:00.00+10:00"; let ts_a = Timestamp::parse_from_rfc3339(datetime_str_a).unwrap(); let ts_b = Timestamp::parse_from_rfc3339(datetime_str_b).unwrap(); assert_eq!(ts_b.seconds() - ts_a.seconds(), -36000); } #[test] fn roundtrip_taker_id_serde() { let id = Identity::new(x25519_dalek::PublicKey::from([42u8; 32])); serde_test::assert_tokens( &id, &[serde_test::Token::String( "2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", )], ); } }