diff --git a/.gitignore b/.gitignore index 276d4de..b8e586d 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,7 @@ .pnp.* # Artifacts from running the daemons -/*.sqlite* +*.sqlite* /maker_seed /taker_seed /mainnet diff --git a/daemon/src/bitmex_price_feed.rs b/daemon/src/bitmex_price_feed.rs index 83ef37c..96ee267 100644 --- a/daemon/src/bitmex_price_feed.rs +++ b/daemon/src/bitmex_price_feed.rs @@ -1,4 +1,4 @@ -use crate::model::Usd; +use crate::model::Price; use anyhow::Result; use futures::{StreamExt, TryStreamExt}; use rust_decimal::Decimal; @@ -43,8 +43,8 @@ pub async fn new() -> Result<(impl Future, watch::Receiver)> #[derive(Clone, Debug)] pub struct Quote { pub timestamp: SystemTime, - pub bid: Usd, - pub ask: Usd, + pub bid: Price, + pub ask: Price, } impl Quote { @@ -63,25 +63,22 @@ impl Quote { Ok(Some(Self { timestamp: SystemTime::now(), - bid: Usd::from(Decimal::try_from(quote.bid_price)?), - ask: Usd::from(Decimal::try_from(quote.ask_price)?), + bid: Price::new(Decimal::try_from(quote.bid_price)?)?, + ask: Price::new(Decimal::try_from(quote.ask_price)?)?, })) } - pub fn for_maker(&self) -> Usd { + pub fn for_maker(&self) -> Price { self.ask } - pub fn for_taker(&self) -> Usd { + pub fn for_taker(&self) -> Price { // TODO: verify whether this is correct - self.mid_range().expect("decimal arithmetic to not fail") + self.mid_range() } - fn mid_range(&self) -> Result { - let sum = self.bid.checked_add(self.ask)?; - let half = sum.half(); - - Ok(half) + fn mid_range(&self) -> Price { + (self.bid + self.ask) / 2 } } @@ -117,7 +114,7 @@ mod tests { let quote = Quote::from_message(message).unwrap().unwrap(); - assert_eq!(quote.bid, Usd::new(dec!(42640.5))); - assert_eq!(quote.ask, Usd::new(dec!(42641))); + assert_eq!(quote.bid, Price::new(dec!(42640.5)).unwrap()); + assert_eq!(quote.ask, Price::new(dec!(42641)).unwrap()); } } diff --git a/daemon/src/db.rs b/daemon/src/db.rs index e4d444d..5a129c0 100644 --- a/daemon/src/db.rs +++ b/daemon/src/db.rs @@ -1,5 +1,5 @@ use crate::model::cfd::{Cfd, CfdState, Order, OrderId}; -use crate::model::{BitMexPriceEventId, Usd}; +use crate::model::{BitMexPriceEventId, Price, Usd}; use anyhow::{Context, Result}; use rust_decimal::Decimal; use sqlx::pool::PoolConnection; @@ -39,7 +39,7 @@ pub async fn insert_order(order: &Order, conn: &mut PoolConnection) -> a .bind(&order.price.to_string()) .bind(&order.min_quantity.to_string()) .bind(&order.max_quantity.to_string()) - .bind(order.leverage.0) + .bind(order.leverage.get()) .bind(&order.liquidation_price.to_string()) .bind( order @@ -101,11 +101,11 @@ pub async fn load_order_by_id( id: row.uuid, trading_pair: row.trading_pair, position: row.position, - price: Usd::new(Decimal::from_str(&row.initial_price)?), - min_quantity: Usd::new(Decimal::from_str(&row.min_quantity)?), - max_quantity: Usd::new(Decimal::from_str(&row.max_quantity)?), + price: row.initial_price.parse::()?, + min_quantity: row.min_quantity.parse::()?, + max_quantity: row.max_quantity.parse::()?, leverage: row.leverage, - liquidation_price: Usd::new(Decimal::from_str(&row.liquidation_price)?), + liquidation_price: row.liquidation_price.parse::()?, creation_timestamp: convert_to_system_time(row.ts_secs, row.ts_nanos)?, term: Duration::new(row.term_secs, row.term_nanos), origin: row.origin, @@ -315,11 +315,11 @@ pub async fn load_cfd_by_order_id( id: row.uuid, trading_pair: row.trading_pair, position: row.position, - price: Usd::new(Decimal::from_str(&row.initial_price)?), - min_quantity: Usd::new(Decimal::from_str(&row.min_quantity)?), - max_quantity: Usd::new(Decimal::from_str(&row.max_quantity)?), + price: row.initial_price.parse::()?, + min_quantity: row.min_quantity.parse::()?, + max_quantity: row.max_quantity.parse::()?, leverage: row.leverage, - liquidation_price: Usd::new(Decimal::from_str(&row.liquidation_price)?), + liquidation_price: row.liquidation_price.parse::()?, creation_timestamp: convert_to_system_time(row.ts_secs, row.ts_nanos)?, term: Duration::new(row.term_secs, row.term_nanos), origin: row.origin, @@ -417,11 +417,11 @@ pub async fn load_all_cfds(conn: &mut PoolConnection) -> anyhow::Result< id: row.uuid, trading_pair: row.trading_pair, position: row.position, - price: Usd::new(Decimal::from_str(&row.initial_price)?), - min_quantity: Usd::new(Decimal::from_str(&row.min_quantity)?), - max_quantity: Usd::new(Decimal::from_str(&row.max_quantity)?), + price: row.initial_price.parse::()?, + min_quantity: row.min_quantity.parse::()?, + max_quantity: row.max_quantity.parse::()?, leverage: row.leverage, - liquidation_price: Usd::new(Decimal::from_str(&row.liquidation_price)?), + liquidation_price: row.liquidation_price.parse::()?, creation_timestamp: convert_to_system_time(row.ts_secs, row.ts_nanos)?, term: Duration::new(row.term_secs, row.term_nanos), origin: row.origin, @@ -527,11 +527,11 @@ pub async fn load_cfds_by_oracle_event_id( id: row.uuid, trading_pair: row.trading_pair, position: row.position, - price: Usd::new(Decimal::from_str(&row.initial_price)?), - min_quantity: Usd::new(Decimal::from_str(&row.min_quantity)?), - max_quantity: Usd::new(Decimal::from_str(&row.max_quantity)?), + price: row.initial_price.parse::()?, + min_quantity: row.min_quantity.parse::()?, + max_quantity: row.max_quantity.parse::()?, leverage: row.leverage, - liquidation_price: Usd::new(Decimal::from_str(&row.liquidation_price)?), + liquidation_price: row.liquidation_price.parse::()?, creation_timestamp: convert_to_system_time(row.ts_secs, row.ts_nanos)?, term: Duration::new(row.term_secs, row.term_nanos), origin: row.origin, @@ -540,7 +540,7 @@ pub async fn load_cfds_by_oracle_event_id( Ok(Cfd { order, - quantity_usd: Usd::new(Decimal::from_str(&row.quantity_usd)?), + quantity_usd: row.quantity_usd.parse::()?, state: serde_json::from_str(row.state.as_str())?, }) }) @@ -807,7 +807,7 @@ mod tests { impl Order { fn dummy() -> Self { Order::new( - Usd::new(dec!(1000)), + Price::new(dec!(1000)).unwrap(), Usd::new(dec!(100)), Usd::new(dec!(1000)), Origin::Theirs, diff --git a/daemon/src/maker_cfd.rs b/daemon/src/maker_cfd.rs index b4e8535..b44cf00 100644 --- a/daemon/src/maker_cfd.rs +++ b/daemon/src/maker_cfd.rs @@ -6,7 +6,7 @@ use crate::model::cfd::{ OrderId, Origin, Role, RollOverProposal, SettlementKind, SettlementProposal, UpdateCfdProposal, UpdateCfdProposals, }; -use crate::model::{TakerId, Usd}; +use crate::model::{Price, TakerId, Usd}; use crate::monitor::MonitorParams; use crate::{log_error, maker_inc_connections, monitor, oracle, setup_contract, wallet, wire}; use anyhow::{Context as _, Result}; @@ -34,7 +34,7 @@ pub enum CfdAction { } pub struct NewOrder { - pub price: Usd, + pub price: Price, pub min_quantity: Usd, pub max_quantity: Usd, } @@ -633,7 +633,7 @@ where { async fn handle_new_order( &mut self, - price: Usd, + price: Price, min_quantity: Usd, max_quantity: Usd, ) -> Result<()> { diff --git a/daemon/src/model.rs b/daemon/src/model.rs index 7a918e3..da0e4d8 100644 --- a/daemon/src/model.rs +++ b/daemon/src/model.rs @@ -1,12 +1,14 @@ use crate::olivia; + use anyhow::{Context, Result}; -use bdk::bitcoin::{Address, Amount}; +use bdk::bitcoin::{Address, Amount, Denomination}; use reqwest::Url; use rust_decimal::prelude::ToPrimitive; use rust_decimal::Decimal; -use rust_decimal_macros::dec; use serde::{Deserialize, Serialize}; use serde_with::{DeserializeFromStr, SerializeDisplay}; +use std::num::NonZeroU8; +use std::ops::{Add, Div, Mul, Sub}; use std::time::SystemTime; use std::{fmt, str}; use time::{OffsetDateTime, PrimitiveDateTime, Time}; @@ -14,53 +16,92 @@ use uuid::Uuid; 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)] pub struct Usd(Decimal); impl Usd { - pub fn new(dec: Decimal) -> Self { - Self(dec) + pub fn new(value: Decimal) -> Self { + Self(value) } - pub fn checked_add(&self, other: Usd) -> Result { - let result = self.0.checked_add(other.0).context("addition error")?; - Ok(Usd(result)) + pub fn try_into_u64(&self) -> Result { + self.0.to_u64().context("could not fit decimal into u64") } - pub fn checked_sub(&self, other: Usd) -> Result { - let result = self.0.checked_sub(other.0).context("subtraction error")?; - Ok(Usd(result)) + pub fn try_into_f64(&self) -> Result { + self.0.to_f64().context("Could not fit decimal into f64") } +} - // TODO: Usd * Usd = Usd^2 not Usd !!! - pub fn checked_mul(&self, other: Usd) -> Result { - let result = self - .0 - .checked_mul(other.0) - .context("multiplication error")?; - Ok(Usd(result)) +impl fmt::Display for Usd { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.round_dp(2).fmt(f) } +} - pub fn checked_div(&self, other: Usd) -> Result { - let result = self.0.checked_div(other.0).context("division error")?; - Ok(Usd(result)) +impl Serialize for Usd { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + ::serialize(&self.0.round_dp(2), serializer) } +} - pub fn try_into_u64(&self) -> Result { - self.0.to_u64().context("could not fit decimal into u64") +impl<'de> Deserialize<'de> for Usd { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let dec = ::deserialize(deserializer)?.round_dp(2); + + Ok(Usd(dec)) } +} - pub fn half(&self) -> Usd { - let half = self - .0 - .checked_div(dec!(2)) - .expect("can always divide by two"); +impl str::FromStr for Usd { + type Err = anyhow::Error; - Usd(half) + fn from_str(s: &str) -> Result { + let dec = Decimal::from_str(s)?; + Ok(Usd(dec)) } } -impl Serialize for Usd { +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] +pub struct Price(Decimal); + +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") + } +} + +impl Serialize for Price { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, @@ -69,26 +110,292 @@ impl Serialize for Usd { } } -impl<'de> Deserialize<'de> for Usd { +impl<'de> Deserialize<'de> for Price { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let dec = ::deserialize(deserializer)?.round_dp(2); - Ok(Usd(dec)) + Ok(Price(dec)) } } -impl fmt::Display for Usd { +impl fmt::Display for Price { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } -impl From for Usd { - fn from(decimal: Decimal) -> Self { - Usd(decimal) +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)] +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") + } +} + +impl Serialize for InversePrice { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + ::serialize(&self.0.round_dp(2), serializer) + } +} + +impl<'de> Deserialize<'de> for InversePrice { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let dec = ::deserialize(deserializer)?.round_dp(2); + + Ok(InversePrice(dec)) + } +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, sqlx::Type)] +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) } } @@ -127,10 +434,6 @@ impl From for Percent { } } -#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, sqlx::Type)] -#[sqlx(transparent)] -pub struct Leverage(pub u8); - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, sqlx::Type)] pub enum TradingPair { BtcUsd, @@ -245,6 +548,7 @@ impl str::FromStr for BitMexPriceEventId { #[cfg(test)] mod tests { + use rust_decimal_macros::dec; use serde_test::{assert_de_tokens, assert_ser_tokens, Token}; use time::macros::datetime; @@ -302,4 +606,60 @@ mod tests { 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); + } } diff --git a/daemon/src/model/cfd.rs b/daemon/src/model/cfd.rs index a6af4e0..aba446d 100644 --- a/daemon/src/model/cfd.rs +++ b/daemon/src/model/cfd.rs @@ -7,20 +7,22 @@ use bdk::descriptor::Descriptor; use bdk::miniscript::DescriptorTrait; use cfd_protocol::secp256k1_zkp::{self, EcdsaAdaptorSignature, SECP256K1}; use cfd_protocol::{finalize_spend_transaction, spending_tx_sighash, TransactionExt}; + use rocket::request::FromParam; -use rust_decimal::prelude::{FromPrimitive, ToPrimitive}; +use rust_decimal::prelude::FromPrimitive; use rust_decimal::Decimal; -use rust_decimal_macros::dec; use serde::de::Error as _; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fmt; -use std::ops::{Neg, RangeInclusive}; +use std::ops::RangeInclusive; use std::time::SystemTime; use time::{Duration, OffsetDateTime}; use uuid::adapter::Hyphenated; use uuid::Uuid; +use super::{InversePrice, Price}; + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, sqlx::Type)] #[sqlx(transparent)] pub struct OrderId(Hyphenated); @@ -99,7 +101,7 @@ pub struct Order { pub trading_pair: TradingPair, pub position: Position, - pub price: Usd, + pub price: Price, // TODO: [post-MVP] Representation of the contract size; at the moment the contract size is // always 1 USD @@ -109,7 +111,7 @@ pub struct Order { // TODO: [post-MVP] - Once we have multiple leverage we will have to move leverage and // liquidation_price into the CFD and add a calculation endpoint for the taker buy screen pub leverage: Leverage, - pub liquidation_price: Usd, + pub liquidation_price: Price, pub creation_timestamp: SystemTime, @@ -126,17 +128,15 @@ pub struct Order { impl Order { pub fn new( - price: Usd, + price: Price, min_quantity: Usd, max_quantity: Usd, origin: Origin, oracle_event_id: BitMexPriceEventId, term: Duration, ) -> Result { - let leverage = Leverage(2); - let maintenance_margin_rate = dec!(0.005); - let liquidation_price = - calculate_liquidation_price(&leverage, &price, &maintenance_margin_rate)?; + let leverage = Leverage::new(2)?; + let liquidation_price = calculate_liquidation_price(leverage, price); Ok(Order { id: OrderId::default(), @@ -155,25 +155,6 @@ impl Order { } } -fn calculate_liquidation_price( - leverage: &Leverage, - price: &Usd, - maintenance_margin_rate: &Decimal, -) -> Result { - let leverage = Decimal::from(leverage.0).into(); - let maintenance_margin_rate: Usd = (*maintenance_margin_rate).into(); - - // liquidation price calc in isolated margin mode - // currently based on: https://help.bybit.com/hc/en-us/articles/360039261334-How-to-calculate-Liquidation-Price-Inverse-Contract- - let liquidation_price = price.checked_mul(leverage)?.checked_div( - leverage - .checked_add(Decimal::ONE.into())? - .checked_sub(maintenance_margin_rate.checked_mul(leverage)?)?, - )?; - - Ok(liquidation_price) -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Error { Connect, @@ -420,8 +401,9 @@ impl Attestation { }) } - pub fn price(&self) -> Usd { - Usd(Decimal::from(self.price)) + pub fn price(&self) -> Result { + let dec = Decimal::from_u64(self.price).context("Could not convert u64 to decimal")?; + Ok(Price::new(dec)?) } pub fn txid(&self) -> Txid { @@ -568,7 +550,7 @@ pub struct SettlementProposal { pub timestamp: SystemTime, pub taker: Amount, pub maker: Amount, - pub price: Usd, + pub price: Price, } /// Proposed collaborative settlement @@ -608,9 +590,9 @@ impl Cfd { pub fn margin(&self) -> Result { let margin = match self.position() { Position::Buy => { - calculate_buy_margin(self.order.price, self.quantity_usd, self.order.leverage)? + calculate_buy_margin(self.order.price, self.quantity_usd, self.order.leverage) } - Position::Sell => calculate_sell_margin(self.order.price, self.quantity_usd)?, + Position::Sell => calculate_sell_margin(self.order.price, self.quantity_usd), }; Ok(margin) @@ -618,20 +600,20 @@ impl Cfd { pub fn counterparty_margin(&self) -> Result { let margin = match self.position() { - Position::Buy => calculate_sell_margin(self.order.price, self.quantity_usd)?, + Position::Buy => calculate_sell_margin(self.order.price, self.quantity_usd), Position::Sell => { - calculate_buy_margin(self.order.price, self.quantity_usd, self.order.leverage)? + calculate_buy_margin(self.order.price, self.quantity_usd, self.order.leverage) } }; Ok(margin) } - pub fn profit(&self, current_price: Usd) -> Result<(SignedAmount, Percent)> { + pub fn profit(&self, current_price: Price) -> Result<(SignedAmount, Percent)> { let closing_price = match (self.attestation(), self.collaborative_close()) { (Some(_attestation), Some(collaborative_close)) => collaborative_close.price, (None, Some(collaborative_close)) => collaborative_close.price, - (Some(attestation), None) => attestation.price(), + (Some(attestation), None) => attestation.price()?, (None, None) => current_price, }; @@ -639,14 +621,14 @@ impl Cfd { self.order.price, closing_price, self.quantity_usd, - self.margin()?, + self.order.leverage, self.position(), )?; Ok((p_n_l, p_n_l_percent)) } - pub fn calculate_settlement(&self, current_price: Usd) -> Result { + pub fn calculate_settlement(&self, current_price: Price) -> Result { let payout_curve = payout_curve::calculate(self.order.price, self.quantity_usd, self.order.leverage)?; @@ -1301,92 +1283,6 @@ pub enum CfdStateChangeEvent { ProposalSigned(CollaborativeSettlement), } -/// Returns the Profit/Loss (P/L) as Bitcoin. Losses are capped by the provided margin -fn calculate_profit( - initial_price: Usd, - closing_price: Usd, - quantity: Usd, - margin: Amount, - position: Position, -) -> Result<(SignedAmount, Percent)> { - let margin_as_sat = - Decimal::from_u64(margin.as_sat()).context("Expect to be a valid decimal")?; - - let initial_price_inverse = dec!(1) - .checked_div(initial_price.0) - .context("Calculating inverse of initial_price resulted in overflow")?; - let current_price_inverse = dec!(1) - .checked_div(closing_price.0) - .context("Calculating inverse of current_price resulted in overflow")?; - - // calculate profit/loss (P and L) in BTC - let profit_btc = match position { - Position::Buy => { - // for long: profit_btc = quantity_usd * ((1/initial_price)-(1/current_price)) - quantity - .0 - .checked_mul( - initial_price_inverse - .checked_sub(current_price_inverse) - .context("Subtracting current_price_inverse from initial_price_inverse resulted in an overflow")?, - ) - } - Position::Sell => { - // for short: profit_btc = quantity_usd * ((1/current_price)-(1/initial_price)) - quantity - .0 - .checked_mul( - current_price_inverse - .checked_sub(initial_price_inverse) - .context("Subtracting initial_price_inverse from current_price_inverse resulted in an overflow")?, - ) - } - } - .context("Calculating profit/loss resulted in an overflow")?; - - let sat_adjust = Decimal::from(Amount::ONE_BTC.as_sat()); - let profit_btc_as_sat = profit_btc - .checked_mul(sat_adjust) - .context("Could not adjust profit to satoshi")?; - - // loss cannot be more than provided margin - let margin_plus_profit_btc = margin_as_sat - .checked_add(profit_btc_as_sat) - .context("Adding up margin and profit_btc resulted in an overflow")?; - - let in_percent = if profit_btc_as_sat.is_zero() { - Decimal::ZERO - } else { - profit_btc_as_sat - .checked_div(margin_as_sat) - .context("Profit divided by margin resulted in overflow")? - }; - - if margin_plus_profit_btc < Decimal::ZERO { - return Ok(( - SignedAmount::from_sat( - margin_as_sat - .neg() - .to_i64() - .context("Could not convert margin to i64")?, - ), - dec!(-100).into(), - )); - } - - Ok(( - SignedAmount::from_sat( - profit_btc_as_sat - .to_i64() - .context("Could not convert profit to i64")?, - ), - in_percent - .checked_mul(dec!(100.0)) - .context("Converting to percent resulted in an overflow")? - .into(), - )) -} - pub trait AsBlocks { /// Calculates the duration in Bitcoin blocks. /// @@ -1406,16 +1302,8 @@ impl AsBlocks for Duration { /// The margin is the initial margin and represents the collateral the buyer has to come up with to /// satisfy the contract. Here we calculate the initial buy margin as: quantity / (initial_price * /// leverage) -pub fn calculate_buy_margin(price: Usd, quantity: Usd, leverage: Leverage) -> Result { - let leverage = Decimal::from(leverage.0).into(); - - let margin = quantity.checked_div(price.checked_mul(leverage)?)?; - - let sat_adjust = Decimal::from(Amount::ONE_BTC.as_sat()).into(); - let margin = margin.checked_mul(sat_adjust)?; - let margin = Amount::from_sat(margin.try_into_u64()?); - - Ok(margin) +pub fn calculate_buy_margin(price: Price, quantity: Usd, leverage: Leverage) -> Amount { + quantity / (price * leverage) } /// Calculates the seller's margin in BTC @@ -1423,14 +1311,96 @@ pub fn calculate_buy_margin(price: Usd, quantity: Usd, leverage: Leverage) -> Re /// The seller margin is represented as the quantity of the contract given the initial price. /// The seller can currently not leverage the position but always has to cover the complete /// quantity. -fn calculate_sell_margin(price: Usd, quantity: Usd) -> Result { - let margin = quantity.checked_div(price)?; +fn calculate_sell_margin(price: Price, quantity: Usd) -> Amount { + quantity / price +} + +fn calculate_liquidation_price(leverage: Leverage, price: Price) -> Price { + price * leverage / (leverage + 1) +} - let sat_adjust = Decimal::from(Amount::ONE_BTC.as_sat()).into(); - let margin = margin.checked_mul(sat_adjust)?; - let margin = Amount::from_sat(margin.try_into_u64()?); +/// Returns the Profit/Loss (P/L) as Bitcoin. Losses are capped by the provided margin +fn calculate_profit( + initial_price: Price, + closing_price: Price, + quantity: Usd, + leverage: Leverage, + position: Position, +) -> Result<(SignedAmount, Percent)> { + let inv_initial_price = + InversePrice::new(initial_price).context("cannot invert invalid price")?; + let inv_closing_price = + InversePrice::new(closing_price).context("cannot invert invalid price")?; + let long_liquidation_price = calculate_liquidation_price(leverage, initial_price); + let long_is_liquidated = closing_price <= long_liquidation_price; + + let long_margin = calculate_buy_margin(initial_price, quantity, leverage) + .to_signed() + .context("Unable to compute long margin")?; + let short_margin = calculate_sell_margin(initial_price, quantity) + .to_signed() + .context("Unable to compute short margin")?; + let amount_changed = (quantity * inv_initial_price) + .to_signed() + .context("Unable to convert to SignedAmount")? + - (quantity * inv_closing_price) + .to_signed() + .context("Unable to convert to SignedAmount")?; - Ok(margin) + // calculate profit/loss (P and L) in BTC + let (margin, payout) = match position { + // TODO: + // Assuming that Buy == Taker, Sell == Maker which in turn has + // implications for being short or long (since taker can only go + // long at the momnet) and if leverage can be used + // (long_leverage == leverage, short_leverage == 1) which also + // has the effect that the right boundary `b` below is infinite + // and not used. + // + // The general case is: + // let: + // P = payout + // Q = quantity + // Ll = long_leverage + // Ls = short_leverage + // xi = initial_price + // xc = closing_price + // + // a = xi * Ll / (Ll + 1) + // b = xi * Ls / (Ls - 1) + // + // P_long(xc) = { + // 0 if xc <= a, + // Q / (xi * Ll) + Q * (1 / xi - 1 / xc) if a < xc < b, + // Q / xi * (1/Ll + 1/Ls) if xc if xc >= b + // } + // + // P_short(xc) = { + // Q / xi * (1/Ll + 1/Ls) if xc <= a, + // Q / (xi * Ls) - Q * (1 / xi - 1 / xc) if a < xc < b, + // 0 if xc >= b + // } + Position::Buy => { + let payout = match long_is_liquidated { + true => SignedAmount::ZERO, + false => long_margin + amount_changed, + }; + (long_margin, payout) + } + Position::Sell => { + let payout = match long_is_liquidated { + true => long_margin + short_margin, + false => short_margin - amount_changed, + }; + (short_margin, payout) + } + }; + + let profit = payout - margin; + let percent = Decimal::from_f64(100. * profit.as_sat() as f64 / margin.as_sat() as f64) + .context("Unable to compute percent")?; + + Ok((profit, Percent(percent))) } #[cfg(test)] @@ -1440,64 +1410,63 @@ mod tests { #[test] fn given_default_values_then_expected_liquidation_price() { - let leverage = Leverage(5); - let price = Usd(dec!(49000)); - let maintenance_margin_rate = dec!(0.005); + let price = Price::new(dec!(46125)).unwrap(); + let leverage = Leverage::new(5).unwrap(); + let expected = Price::new(dec!(38437.5)).unwrap(); - let liquidation_price = - calculate_liquidation_price(&leverage, &price, &maintenance_margin_rate).unwrap(); + let liquidation_price = calculate_liquidation_price(leverage, price); - assert_eq!(liquidation_price, Usd(dec!(41004.184100418410041841004184))); + assert_eq!(liquidation_price, expected); } #[test] fn given_leverage_of_one_and_equal_price_and_quantity_then_buy_margin_is_one_btc() { - let price = Usd(dec!(40000)); - let quantity = Usd(dec![40000]); - let leverage = Leverage(1); + let price = Price::new(dec!(40000)).unwrap(); + let quantity = Usd::new(dec!(40000)); + let leverage = Leverage::new(1).unwrap(); - let buy_margin = calculate_buy_margin(price, quantity, leverage).unwrap(); + let buy_margin = calculate_buy_margin(price, quantity, leverage); assert_eq!(buy_margin, Amount::ONE_BTC); } #[test] fn given_leverage_of_one_and_leverage_of_ten_then_buy_margin_is_lower_factor_ten() { - let price = Usd(dec!(40000)); - let quantity = Usd(dec![40000]); - let leverage = Leverage(10); + let price = Price::new(dec!(40000)).unwrap(); + let quantity = Usd::new(dec!(40000)); + let leverage = Leverage::new(10).unwrap(); - let buy_margin = calculate_buy_margin(price, quantity, leverage).unwrap(); + let buy_margin = calculate_buy_margin(price, quantity, leverage); assert_eq!(buy_margin, Amount::from_btc(0.1).unwrap()); } #[test] fn given_quantity_equals_price_then_sell_margin_is_one_btc() { - let price = Usd(dec!(40000)); - let quantity = Usd(dec![40000]); + let price = Price::new(dec!(40000)).unwrap(); + let quantity = Usd::new(dec!(40000)); - let sell_margin = calculate_sell_margin(price, quantity).unwrap(); + let sell_margin = calculate_sell_margin(price, quantity); assert_eq!(sell_margin, Amount::ONE_BTC); } #[test] fn given_quantity_half_of_price_then_sell_margin_is_half_btc() { - let price = Usd(dec!(40000)); - let quantity = Usd(dec![20000]); + let price = Price::new(dec!(40000)).unwrap(); + let quantity = Usd::new(dec!(20000)); - let sell_margin = calculate_sell_margin(price, quantity).unwrap(); + let sell_margin = calculate_sell_margin(price, quantity); assert_eq!(sell_margin, Amount::from_btc(0.5).unwrap()); } #[test] fn given_quantity_double_of_price_then_sell_margin_is_two_btc() { - let price = Usd(dec!(40000)); - let quantity = Usd(dec![80000]); + let price = Price::new(dec!(40000)).unwrap(); + let quantity = Usd::new(dec!(80000)); - let sell_margin = calculate_sell_margin(price, quantity).unwrap(); + let sell_margin = calculate_sell_margin(price, quantity); assert_eq!(sell_margin, Amount::from_btc(2.0).unwrap()); } @@ -1522,10 +1491,10 @@ mod tests { #[test] fn calculate_profit_and_loss() { assert_profit_loss_values( - Usd::from(dec!(10_000)), - Usd::from(dec!(10_000)), - Usd::from(dec!(10_000)), - Amount::ONE_BTC, + Price::new(dec!(10_000)).unwrap(), + Price::new(dec!(10_000)).unwrap(), + Usd::new(dec!(10_000)), + Leverage::new(2).unwrap(), Position::Buy, SignedAmount::ZERO, Decimal::ZERO.into(), @@ -1533,74 +1502,74 @@ mod tests { ); assert_profit_loss_values( - Usd::from(dec!(10_000)), - Usd::from(dec!(20_000)), - Usd::from(dec!(10_000)), - Amount::ONE_BTC, + Price::new(dec!(10_000)).unwrap(), + Price::new(dec!(20_000)).unwrap(), + Usd::new(dec!(10_000)), + Leverage::new(2).unwrap(), Position::Buy, SignedAmount::from_sat(50_000_000), - dec!(50).into(), - "A 100% price increase should be 50% profit", + dec!(100).into(), + "A price increase of 2x should result in a profit of 100% (long)", ); assert_profit_loss_values( - Usd::from(dec!(10_000)), - Usd::from(dec!(5_000)), - Usd::from(dec!(10_000)), - Amount::ONE_BTC, + Price::new(dec!(9_000)).unwrap(), + Price::new(dec!(6_000)).unwrap(), + Usd::new(dec!(9_000)), + Leverage::new(2).unwrap(), Position::Buy, - SignedAmount::from_sat(-100_000_000), + SignedAmount::from_sat(-50_000_000), dec!(-100).into(), - "A 50% drop should result in 100% loss", + "A price drop of 1/(Leverage + 1) x should result in 100% loss (long)", ); assert_profit_loss_values( - Usd::from(dec!(10_000)), - Usd::from(dec!(2_500)), - Usd::from(dec!(10_000)), - Amount::ONE_BTC, + Price::new(dec!(10_000)).unwrap(), + Price::new(dec!(5_000)).unwrap(), + Usd::new(dec!(10_000)), + Leverage::new(2).unwrap(), Position::Buy, - SignedAmount::from_sat(-100_000_000), + SignedAmount::from_sat(-50_000_000), dec!(-100).into(), - "A loss should be capped by 100%", + "A loss should be capped at 100% (long)", ); assert_profit_loss_values( - Usd::from(dec!(50_400)), - Usd::from(dec!(60_000)), - Usd::from(dec!(10_000)), - Amount::from_btc(0.01984).unwrap(), + Price::new(dec!(50_400)).unwrap(), + Price::new(dec!(60_000)).unwrap(), + Usd::new(dec!(10_000)), + Leverage::new(2).unwrap(), Position::Buy, SignedAmount::from_sat(3_174_603), - dec!(160.01024065540194572452620968).into(), - "buy position should make a profit when price goes up", + dec!(31.99999798400001).into(), + "long position should make a profit when price goes up", ); assert_profit_loss_values( - Usd::from(dec!(10_000)), - Usd::from(dec!(16_000)), - Usd::from(dec!(10_000)), - Amount::ONE_BTC, + Price::new(dec!(50_400)).unwrap(), + Price::new(dec!(60_000)).unwrap(), + Usd::new(dec!(10_000)), + Leverage::new(2).unwrap(), Position::Sell, - SignedAmount::from_sat(-37_500_000), - dec!(-37.5).into(), + SignedAmount::from_sat(-3_174_603), + dec!(-15.99999899200001).into(), "sell position should make a loss when price goes up", ); } #[allow(clippy::too_many_arguments)] fn assert_profit_loss_values( - initial_price: Usd, - current_price: Usd, + initial_price: Price, + current_price: Price, quantity: Usd, - margin: Amount, + leverage: Leverage, position: Position, should_profit: SignedAmount, should_profit_in_percent: Percent, msg: &str, ) { let (profit, in_percent) = - calculate_profit(initial_price, current_price, quantity, margin, position).unwrap(); + calculate_profit(initial_price, current_price, quantity, leverage, position).unwrap(); assert_eq!(profit, should_profit, "{}", msg); assert_eq!(in_percent, should_profit_in_percent, "{}", msg); @@ -1608,15 +1577,15 @@ mod tests { #[test] fn test_profit_calculation_loss_plus_profit_should_be_zero() { - let initial_price = Usd::from(dec!(10_000)); - let closing_price = Usd::from(dec!(16_000)); - let quantity = Usd::from(dec!(10_000)); - let margin = Amount::ONE_BTC; + let initial_price = Price::new(dec!(10_000)).unwrap(); + let closing_price = Price::new(dec!(16_000)).unwrap(); + let quantity = Usd::new(dec!(10_000)); + let leverage = Leverage::new(1).unwrap(); let (profit, profit_in_percent) = calculate_profit( initial_price, closing_price, quantity, - margin, + leverage, Position::Buy, ) .unwrap(); @@ -1624,18 +1593,57 @@ mod tests { initial_price, closing_price, quantity, - margin, + leverage, Position::Sell, ) .unwrap(); assert_eq!(profit.checked_add(loss).unwrap(), SignedAmount::ZERO); + // NOTE: + // this is only true when long_leverage == short_leverage assert_eq!( profit_in_percent.0.checked_add(loss_in_percent.0).unwrap(), Decimal::ZERO ); } + #[test] + fn margin_remains_constant() { + let initial_price = Price::new(dec!(15_000)).unwrap(); + let quantity = Usd::new(dec!(10_000)); + let leverage = Leverage::new(2).unwrap(); + let long_margin = calculate_buy_margin(initial_price, quantity, leverage) + .to_signed() + .unwrap(); + let short_margin = calculate_sell_margin(initial_price, quantity) + .to_signed() + .unwrap(); + let pool_amount = SignedAmount::ONE_BTC; + let closing_prices = [ + Price::new(dec!(0.15)).unwrap(), + Price::new(dec!(1.5)).unwrap(), + Price::new(dec!(15)).unwrap(), + Price::new(dec!(150)).unwrap(), + Price::new(dec!(1_500)).unwrap(), + Price::new(dec!(15_000)).unwrap(), + Price::new(dec!(150_000)).unwrap(), + Price::new(dec!(1_500_000)).unwrap(), + Price::new(dec!(15_000_000)).unwrap(), + ]; + + for price in closing_prices { + let (long_profit, _) = + calculate_profit(initial_price, price, quantity, leverage, Position::Buy).unwrap(); + let (short_profit, _) = + calculate_profit(initial_price, price, quantity, leverage, Position::Sell).unwrap(); + + assert_eq!( + long_profit + long_margin + short_profit + short_margin, + pool_amount + ); + } + } + #[test] fn order_id_serde_roundtrip() { let id = OrderId::default(); @@ -1786,11 +1794,11 @@ pub struct CollaborativeSettlement { pub timestamp: SystemTime, #[serde(with = "::bdk::bitcoin::util::amount::serde::as_sat")] payout: Amount, - price: Usd, + price: Price, } impl CollaborativeSettlement { - pub fn new(tx: Transaction, own_script_pubkey: Script, price: Usd) -> Self { + pub fn new(tx: Transaction, own_script_pubkey: Script, price: Price) -> Self { // Falls back to Amount::ZERO in case we don't find an output that matches out script pubkey // The assumption is, that this can happen for cases where we were liuqidated let payout = match tx diff --git a/daemon/src/payout_curve.rs b/daemon/src/payout_curve.rs index 1168800..e729b0e 100644 --- a/daemon/src/payout_curve.rs +++ b/daemon/src/payout_curve.rs @@ -1,6 +1,6 @@ use std::fmt; -use crate::model::{Leverage, Usd}; +use crate::model::{Leverage, Price, Usd}; use crate::payout_curve::curve::Curve; use anyhow::{Context, Result}; use bdk::bitcoin; @@ -43,7 +43,7 @@ mod utils; /// ### Returns /// /// The list of [`Payout`]s for the given price, quantity and leverage. -pub fn calculate(price: Usd, quantity: Usd, leverage: Leverage) -> Result> { +pub fn calculate(price: Price, quantity: Usd, leverage: Leverage) -> Result> { let payouts = calculate_payout_parameters(price, quantity, leverage)? .into_iter() .map(PayoutParameter::into_payouts) @@ -62,20 +62,20 @@ const SHORT_LEVERAGE: usize = 1; /// To ease testing, we write our tests against this function because it has a more human-friendly /// output. The design goal here is that the the above `calculate` function is as thin as possible. fn calculate_payout_parameters( - price: Usd, + price: Price, quantity: Usd, long_leverage: Leverage, ) -> Result> { let initial_rate = price - .try_into_u64() - .context("Cannot convert price to u64")? as f64; + .try_into_f64() + .context("Cannot convert price to f64")?; let quantity = quantity .try_into_u64() .context("Cannot convert quantity to u64")? as usize; let payout_curve = PayoutCurve::new( - initial_rate as f64, - long_leverage.0 as usize, + initial_rate, + long_leverage.get() as usize, SHORT_LEVERAGE, quantity, CONTRACT_VALUE, @@ -506,9 +506,9 @@ mod tests { #[test] fn calculate_snapshot() { let actual_payouts = calculate_payout_parameters( - Usd::new(dec!(54000.00)), + Price::new(dec!(54000.00)).unwrap(), Usd::new(dec!(3500.00)), - Leverage(5), + Leverage::new(5).unwrap(), ) .unwrap(); @@ -722,9 +722,9 @@ mod tests { #[test] fn verfiy_tails() { let actual_payouts = calculate_payout_parameters( - Usd::new(dec!(54000.00)), + Price::new(dec!(54000.00)).unwrap(), Usd::new(dec!(3500.00)), - Leverage(5), + Leverage::new(5).unwrap(), ) .unwrap(); diff --git a/daemon/src/routes_maker.rs b/daemon/src/routes_maker.rs index d092738..761dcc4 100644 --- a/daemon/src/routes_maker.rs +++ b/daemon/src/routes_maker.rs @@ -2,7 +2,7 @@ use anyhow::Result; use bdk::bitcoin::Network; use daemon::auth::Authenticated; use daemon::model::cfd::{Cfd, Order, OrderId, Role, UpdateCfdProposals}; -use daemon::model::{Usd, WalletInfo}; +use daemon::model::{Price, Usd, WalletInfo}; use daemon::routes::EmbeddedFileExt; use daemon::to_sse_event::{CfdAction, CfdsWithAuxData, ToSseEvent}; use daemon::{bitmex_price_feed, maker_cfd}; @@ -101,7 +101,7 @@ pub async fn maker_feed( // TODO: Use Rocket form? #[derive(Debug, Clone, Deserialize)] pub struct CfdNewOrderRequest { - pub price: Usd, + pub price: Price, // TODO: [post-MVP] Representation of the contract size; at the moment the contract size is // always 1 USD pub min_quantity: Usd, diff --git a/daemon/src/routes_taker.rs b/daemon/src/routes_taker.rs index 1c3d09d..5b8a75f 100644 --- a/daemon/src/routes_taker.rs +++ b/daemon/src/routes_taker.rs @@ -1,6 +1,6 @@ use bdk::bitcoin::{Amount, Network}; use daemon::model::cfd::{calculate_buy_margin, Cfd, Order, OrderId, Role, UpdateCfdProposals}; -use daemon::model::{Leverage, Usd, WalletInfo}; +use daemon::model::{Leverage, Price, Usd, WalletInfo}; use daemon::routes::EmbeddedFileExt; use daemon::to_sse_event::{CfdAction, CfdsWithAuxData, ToSseEvent}; use daemon::{bitmex_price_feed, taker_cfd}; @@ -160,7 +160,7 @@ pub fn get_health_check() {} #[derive(Debug, Clone, Copy, Deserialize)] pub struct MarginRequest { - pub price: Usd, + pub price: Price, pub quantity: Usd, pub leverage: Leverage, } @@ -182,8 +182,7 @@ pub fn margin_calc( margin_request.price, margin_request.quantity, margin_request.leverage, - ) - .map_err(|e| status::BadRequest(Some(e.to_string())))?; + ); Ok(status::Accepted(Some(Json(MarginResponse { margin })))) } diff --git a/daemon/src/taker_cfd.rs b/daemon/src/taker_cfd.rs index 5c97296..33c9c4d 100644 --- a/daemon/src/taker_cfd.rs +++ b/daemon/src/taker_cfd.rs @@ -5,7 +5,7 @@ use crate::model::cfd::{ OrderId, Origin, Role, RollOverProposal, SettlementKind, SettlementProposal, UpdateCfdProposal, UpdateCfdProposals, }; -use crate::model::{BitMexPriceEventId, Usd}; +use crate::model::{BitMexPriceEventId, Price, Usd}; use crate::monitor::{self, MonitorParams}; use crate::wire::{MakerToTaker, RollOverMsg, SetupMsg}; use crate::{log_error, oracle, setup_contract, wallet, wire}; @@ -28,7 +28,7 @@ pub struct TakeOffer { pub enum CfdAction { ProposeSettlement { order_id: OrderId, - current_price: Usd, + current_price: Price, }, ProposeRollOver { order_id: OrderId, @@ -193,7 +193,7 @@ where async fn handle_propose_settlement( &mut self, order_id: OrderId, - current_price: Usd, + current_price: Price, ) -> Result<()> { let mut conn = self.db.acquire().await?; let cfd = load_cfd_by_order_id(order_id, &mut conn).await?; diff --git a/daemon/src/to_sse_event.rs b/daemon/src/to_sse_event.rs index 309761d..414da13 100644 --- a/daemon/src/to_sse_event.rs +++ b/daemon/src/to_sse_event.rs @@ -1,7 +1,7 @@ use crate::model::cfd::{ Dlc, OrderId, Payout, Role, SettlementKind, UpdateCfdProposal, UpdateCfdProposals, }; -use crate::model::{Leverage, Position, TradingPair, Usd}; +use crate::model::{Leverage, Position, Price, TradingPair, Usd}; use crate::{bitmex_price_feed, model}; use bdk::bitcoin::{Amount, Network, SignedAmount, Txid}; use rocket::request::FromParam; @@ -16,12 +16,12 @@ use tokio::sync::watch; #[derive(Debug, Clone, Serialize)] pub struct Cfd { pub order_id: OrderId, - pub initial_price: Usd, + pub initial_price: Price, pub leverage: Leverage, pub trading_pair: TradingPair, pub position: Position, - pub liquidation_price: Usd, + pub liquidation_price: Price, pub quantity_usd: Usd, @@ -163,13 +163,13 @@ pub struct CfdOrder { pub trading_pair: TradingPair, pub position: Position, - pub price: Usd, + pub price: Price, pub min_quantity: Usd, pub max_quantity: Usd, pub leverage: Leverage, - pub liquidation_price: Usd, + pub liquidation_price: Price, pub creation_timestamp: u64, pub term_in_secs: u64, @@ -184,7 +184,7 @@ pub trait ToSseEvent { /// by UI pub struct CfdsWithAuxData { pub cfds: Vec, - pub current_price: Usd, + pub current_price: Price, pub pending_proposals: UpdateCfdProposals, pub network: Network, } @@ -419,8 +419,8 @@ fn to_tx_url_list(state: model::cfd::CfdState, network: Network) -> Vec { #[derive(Debug, Clone, Serialize)] pub struct Quote { - bid: Usd, - ask: Usd, + bid: Price, + ask: Price, last_updated_at: u64, } diff --git a/daemon/src/wire.rs b/daemon/src/wire.rs index 774b8d0..3d89e2f 100644 --- a/daemon/src/wire.rs +++ b/daemon/src/wire.rs @@ -1,5 +1,5 @@ use crate::model::cfd::{Order, OrderId}; -use crate::model::{BitMexPriceEventId, Usd}; +use crate::model::{BitMexPriceEventId, Price, Usd}; use anyhow::{bail, Result}; use bdk::bitcoin::secp256k1::Signature; use bdk::bitcoin::util::psbt::PartiallySignedTransaction; @@ -31,7 +31,7 @@ pub enum TakerToMaker { taker: Amount, #[serde(with = "::bdk::bitcoin::util::amount::serde::as_btc")] maker: Amount, - price: Usd, + price: Price, }, InitiateSettlement { order_id: OrderId, diff --git a/daemon/tests/happy_path.rs b/daemon/tests/happy_path.rs index 51cb8c2..0fc3866 100644 --- a/daemon/tests/happy_path.rs +++ b/daemon/tests/happy_path.rs @@ -4,7 +4,7 @@ use bdk::bitcoin::{ecdsa, Txid}; use cfd_protocol::secp256k1_zkp::{schnorrsig, Secp256k1}; use cfd_protocol::PartyParams; use daemon::model::cfd::Order; -use daemon::model::{Usd, WalletInfo}; +use daemon::model::{Price, Usd, WalletInfo}; use daemon::{connection, db, logger, maker_cfd, maker_inc_connections, monitor, oracle, wallet}; use rand::thread_rng; use rust_decimal_macros::dec; @@ -36,7 +36,7 @@ async fn taker_receives_order_from_maker_on_publication() { fn new_dummy_order() -> maker_cfd::NewOrder { maker_cfd::NewOrder { - price: Usd::new(dec!(50_000)), + price: Price::new(dec!(50_000)).expect("unexpected failure"), min_quantity: Usd::new(dec!(10)), max_quantity: Usd::new(dec!(100)), }