Browse Source

Merge #405

405: Fix price type r=da-kami a=DeliciousHair

Replacement of structs with public data to ones with private data

Addresses #357 and #365. Although not a very large change, this PR ends up touching rather a lot of code.

* Converted types `Usd`, `Leverage` and `Percent` to something that is appropriate to this application
* Created new types `Price` and `InversePrice` to use for BTC/USD exchange rate with appropriate algebraic ops implemented as well.
* Added new positive tests
* The function `daemon::model::calculate_profit()` has been changed substantially as the updated types make the existing workflow needlessly complex
* Some tests (mostly in `cfd.rs` required updating) in order to make use of the new types.
* Minor edit to `.gitignore` to avoid accidental pushing of DB to repository--should have been it's own item, added here to fix a problem that arose during this work.

NOTE:
* There may be an excess of algebraic ops implemented, some pruning may be appropriate.

Co-authored-by: DelicioiusHair <mshepit@gmail.com>
contact-taker-before-changing-cfd-state
bors[bot] 3 years ago
committed by GitHub
parent
commit
9ed55c859e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .gitignore
  2. 27
      daemon/src/bitmex_price_feed.rs
  3. 40
      daemon/src/db.rs
  4. 6
      daemon/src/maker_cfd.rs
  5. 438
      daemon/src/model.rs
  6. 430
      daemon/src/model/cfd.rs
  7. 22
      daemon/src/payout_curve.rs
  8. 4
      daemon/src/routes_maker.rs
  9. 7
      daemon/src/routes_taker.rs
  10. 6
      daemon/src/taker_cfd.rs
  11. 16
      daemon/src/to_sse_event.rs
  12. 4
      daemon/src/wire.rs
  13. 4
      daemon/tests/happy_path.rs

2
.gitignore

@ -17,7 +17,7 @@
.pnp.*
# Artifacts from running the daemons
/*.sqlite*
*.sqlite*
/maker_seed
/taker_seed
/mainnet

27
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<Output = ()>, watch::Receiver<Quote>)>
#[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<Usd> {
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());
}
}

40
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<Sqlite>) -> 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::<Price>()?,
min_quantity: row.min_quantity.parse::<Usd>()?,
max_quantity: row.max_quantity.parse::<Usd>()?,
leverage: row.leverage,
liquidation_price: Usd::new(Decimal::from_str(&row.liquidation_price)?),
liquidation_price: row.liquidation_price.parse::<Price>()?,
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::<Price>()?,
min_quantity: row.min_quantity.parse::<Usd>()?,
max_quantity: row.max_quantity.parse::<Usd>()?,
leverage: row.leverage,
liquidation_price: Usd::new(Decimal::from_str(&row.liquidation_price)?),
liquidation_price: row.liquidation_price.parse::<Price>()?,
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<Sqlite>) -> 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::<Price>()?,
min_quantity: row.min_quantity.parse::<Usd>()?,
max_quantity: row.max_quantity.parse::<Usd>()?,
leverage: row.leverage,
liquidation_price: Usd::new(Decimal::from_str(&row.liquidation_price)?),
liquidation_price: row.liquidation_price.parse::<Price>()?,
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::<Price>()?,
min_quantity: row.min_quantity.parse::<Usd>()?,
max_quantity: row.max_quantity.parse::<Usd>()?,
leverage: row.leverage,
liquidation_price: Usd::new(Decimal::from_str(&row.liquidation_price)?),
liquidation_price: row.liquidation_price.parse::<Price>()?,
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::<Usd>()?,
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,

6
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<()> {

438
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<Usd> {
let result = self.0.checked_add(other.0).context("addition error")?;
Ok(Usd(result))
pub fn try_into_u64(&self) -> Result<u64> {
self.0.to_u64().context("could not fit decimal into u64")
}
pub fn checked_sub(&self, other: Usd) -> Result<Usd> {
let result = self.0.checked_sub(other.0).context("subtraction error")?;
Ok(Usd(result))
pub fn try_into_f64(&self) -> Result<f64> {
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<Usd> {
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<Usd> {
let result = self.0.checked_div(other.0).context("division error")?;
Ok(Usd(result))
impl Serialize for Usd {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
<Decimal as Serialize>::serialize(&self.0.round_dp(2), serializer)
}
}
pub fn try_into_u64(&self) -> Result<u64> {
self.0.to_u64().context("could not fit decimal into u64")
impl<'de> Deserialize<'de> for Usd {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let dec = <Decimal as Deserialize>::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<Self, Self::Err> {
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<Self, Error> {
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<u64> {
self.0.to_u64().context("Could not fit decimal into u64")
}
pub fn try_into_f64(&self) -> Result<f64> {
self.0.to_f64().context("Could not fit decimal into f64")
}
}
impl Serialize for Price {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let dec = <Decimal as Deserialize>::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<Decimal> for Usd {
fn from(decimal: Decimal) -> Self {
Usd(decimal)
impl str::FromStr for Price {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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<Self, Error> {
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<u64> {
self.0.to_u64().context("Could not fit decimal into u64")
}
pub fn try_into_f64(&self) -> Result<f64> {
self.0.to_f64().context("Could not fit decimal into f64")
}
}
impl Serialize for InversePrice {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
<Decimal as Serialize>::serialize(&self.0.round_dp(2), serializer)
}
}
impl<'de> Deserialize<'de> for InversePrice {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let dec = <Decimal as Deserialize>::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<Self> {
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<Leverage> for Usd {
type Output = Usd;
fn mul(self, rhs: Leverage) -> Self::Output {
let value = self.0 * Decimal::from(rhs.0);
Self(value)
}
}
impl Div<Leverage> for Usd {
type Output = Usd;
fn div(self, rhs: Leverage) -> Self::Output {
Self(self.0 / Decimal::from(rhs.0))
}
}
impl Mul<Usd> for Leverage {
type Output = Usd;
fn mul(self, rhs: Usd) -> Self::Output {
let value = Decimal::from(self.0) * rhs.0;
Usd(value)
}
}
impl Mul<u8> for Usd {
type Output = Usd;
fn mul(self, rhs: u8) -> Self::Output {
let value = self.0 * Decimal::from(rhs);
Self(value)
}
}
impl Div<u8> for Usd {
type Output = Usd;
fn div(self, rhs: u8) -> Self::Output {
let value = self.0 / Decimal::from(rhs);
Self(value)
}
}
impl Div<u8> for Price {
type Output = Price;
fn div(self, rhs: u8) -> Self::Output {
let value = self.0 / Decimal::from(rhs);
Self(value)
}
}
impl Add<Usd> for Usd {
type Output = Usd;
fn add(self, rhs: Usd) -> Self::Output {
let value = self.0 + rhs.0;
Self(value)
}
}
impl Sub<Usd> for Usd {
type Output = Usd;
fn sub(self, rhs: Usd) -> Self::Output {
let value = self.0 - rhs.0;
Self(value)
}
}
impl Div<Price> 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<Leverage> for Price {
type Output = Price;
fn mul(self, rhs: Leverage) -> Self::Output {
let value = self.0 * Decimal::from(rhs.0);
Self(value)
}
}
impl Mul<Price> for Leverage {
type Output = Price;
fn mul(self, rhs: Price) -> Self::Output {
let value = Decimal::from(self.0) * rhs.0;
Price(value)
}
}
impl Div<Leverage> for Price {
type Output = Price;
fn div(self, rhs: Leverage) -> Self::Output {
let value = self.0 / Decimal::from(rhs.0);
Self(value)
}
}
impl Mul<InversePrice> 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<Leverage> for InversePrice {
type Output = InversePrice;
fn mul(self, rhs: Leverage) -> Self::Output {
let value = self.0 * Decimal::from(rhs.0);
Self(value)
}
}
impl Mul<InversePrice> for Leverage {
type Output = InversePrice;
fn mul(self, rhs: InversePrice) -> Self::Output {
let value = Decimal::from(self.0) * rhs.0;
InversePrice(value)
}
}
impl Div<Leverage> for InversePrice {
type Output = InversePrice;
fn div(self, rhs: Leverage) -> Self::Output {
let value = self.0 / Decimal::from(rhs.0);
Self(value)
}
}
impl Add<Price> for Price {
type Output = Price;
fn add(self, rhs: Price) -> Self::Output {
Self(self.0 + rhs.0)
}
}
impl Sub<Price> for Price {
type Output = Price;
fn sub(self, rhs: Price) -> Self::Output {
Self(self.0 - rhs.0)
}
}
impl Add<InversePrice> for InversePrice {
type Output = InversePrice;
fn add(self, rhs: InversePrice) -> Self::Output {
Self(self.0 + rhs.0)
}
}
impl Sub<InversePrice> for InversePrice {
type Output = InversePrice;
fn sub(self, rhs: InversePrice) -> Self::Output {
Self(self.0 - rhs.0)
}
}
impl Add<u8> for Leverage {
type Output = Leverage;
fn add(self, rhs: u8) -> Self::Output {
Self(self.0 + rhs)
}
}
impl Add<Leverage> for u8 {
type Output = Leverage;
fn add(self, rhs: Leverage) -> Self::Output {
Leverage(self + rhs.0)
}
}
impl Div<Leverage> 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<Decimal> 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);
}
}

430
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<Self> {
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<Usd> {
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<Price> {
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<Amount> {
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<Amount> {
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<SettlementProposal> {
pub fn calculate_settlement(&self, current_price: Price) -> Result<SettlementProposal> {
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<Amount> {
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<Amount> {
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

22
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<Vec<Payout>> {
pub fn calculate(price: Price, quantity: Usd, leverage: Leverage) -> Result<Vec<Payout>> {
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<Vec<PayoutParameter>> {
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();

4
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,

7
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 }))))
}

6
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?;

16
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<model::cfd::Cfd>,
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<TxUrl> {
#[derive(Debug, Clone, Serialize)]
pub struct Quote {
bid: Usd,
ask: Usd,
bid: Price,
ask: Price,
last_updated_at: u64,
}

4
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,

4
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)),
}

Loading…
Cancel
Save