You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
560 lines
18 KiB
560 lines
18 KiB
use crate::model::{Leverage, Position, TradingPair, Usd};
|
|
use anyhow::Result;
|
|
use bdk::bitcoin::secp256k1::{SecretKey, Signature};
|
|
use bdk::bitcoin::util::psbt::PartiallySignedTransaction;
|
|
use bdk::bitcoin::{Amount, Transaction};
|
|
use cfd_protocol::EcdsaAdaptorSignature;
|
|
use rust_decimal::Decimal;
|
|
use rust_decimal_macros::dec;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::fmt::{Display, Formatter};
|
|
use std::time::{Duration, SystemTime};
|
|
use uuid::Uuid;
|
|
|
|
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
|
pub struct OrderId(Uuid);
|
|
|
|
impl Default for OrderId {
|
|
fn default() -> Self {
|
|
Self(Uuid::new_v4())
|
|
}
|
|
}
|
|
|
|
impl Display for OrderId {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
self.0.fmt(f)
|
|
}
|
|
}
|
|
|
|
/// A concrete order created by a maker for a taker
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
pub struct Order {
|
|
pub id: OrderId,
|
|
|
|
pub trading_pair: TradingPair,
|
|
pub position: Position,
|
|
|
|
pub price: Usd,
|
|
|
|
// TODO: [post-MVP] Representation of the contract size; at the moment the contract size is
|
|
// always 1 USD
|
|
pub min_quantity: Usd,
|
|
pub max_quantity: Usd,
|
|
|
|
// TODO: [post-MVP] Allow different values
|
|
pub leverage: Leverage,
|
|
pub liquidation_price: Usd,
|
|
|
|
pub creation_timestamp: SystemTime,
|
|
|
|
/// The duration that will be used for calculating the settlement timestamp
|
|
pub term: Duration,
|
|
}
|
|
|
|
#[allow(dead_code)] // Only one binary and the tests use this.
|
|
impl Order {
|
|
pub fn from_default_with_price(price: Usd) -> Result<Self> {
|
|
let leverage = Leverage(5);
|
|
let maintenance_margin_rate = dec!(0.005);
|
|
let liquidation_price =
|
|
calculate_liquidation_price(&leverage, &price, &maintenance_margin_rate)?;
|
|
|
|
Ok(Order {
|
|
id: OrderId::default(),
|
|
price,
|
|
min_quantity: Usd(dec!(1000)),
|
|
max_quantity: Usd(dec!(10000)),
|
|
leverage,
|
|
trading_pair: TradingPair::BtcUsd,
|
|
liquidation_price,
|
|
position: Position::Sell,
|
|
creation_timestamp: SystemTime::now(),
|
|
term: Duration::from_secs(60 * 60 * 8), // 8 hours
|
|
})
|
|
}
|
|
pub fn with_min_quantity(mut self, min_quantity: Usd) -> Order {
|
|
self.min_quantity = min_quantity;
|
|
self
|
|
}
|
|
|
|
pub fn with_max_quantity(mut self, max_quantity: Usd) -> Order {
|
|
self.max_quantity = max_quantity;
|
|
self
|
|
}
|
|
}
|
|
|
|
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 {
|
|
// TODO
|
|
ConnectionLost,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CfdStateError {
|
|
last_successful_state: CfdState,
|
|
error: Error,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
|
pub struct CfdStateCommon {
|
|
pub transition_timestamp: SystemTime,
|
|
}
|
|
|
|
// Note: De-/Serialize with type tag to make handling on UI easier
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
|
#[serde(tag = "type", content = "payload")]
|
|
pub enum CfdState {
|
|
/// The taker has requested to take a CFD, but has not messaged the maker yet.
|
|
///
|
|
/// This state only applies to the taker.
|
|
TakeRequested { common: CfdStateCommon },
|
|
/// The taker sent an open request to the maker to open the CFD but don't have a response yet.
|
|
///
|
|
/// This state applies to taker and maker.
|
|
/// Initial state for the maker.
|
|
PendingTakeRequest { common: CfdStateCommon },
|
|
/// The maker has accepted the CFD take request, but the contract is not set up on chain yet.
|
|
///
|
|
/// This state applies to taker and maker.
|
|
Accepted { common: CfdStateCommon },
|
|
|
|
/// The maker rejected the CFD take request.
|
|
///
|
|
/// This state applies to taker and maker.
|
|
Rejected { common: CfdStateCommon },
|
|
|
|
/// State used during contract setup.
|
|
///
|
|
/// This state applies to taker and maker.
|
|
/// All contract setup messages between taker and maker are expected to be sent in on scope.
|
|
ContractSetup { common: CfdStateCommon },
|
|
|
|
/// The CFD contract is set up on chain.
|
|
///
|
|
/// This state applies to taker and maker.
|
|
Open {
|
|
common: CfdStateCommon,
|
|
settlement_timestamp: SystemTime,
|
|
#[serde(with = "::bdk::bitcoin::util::amount::serde::as_sat")]
|
|
margin: Amount,
|
|
},
|
|
|
|
/// Requested close the position, but we have not passed that on to the blockchain yet.
|
|
///
|
|
/// This state applies to taker and maker.
|
|
CloseRequested { common: CfdStateCommon },
|
|
/// The close transaction (CET) was published on the Bitcoin blockchain but we don't have a
|
|
/// confirmation yet.
|
|
///
|
|
/// This state applies to taker and maker.
|
|
PendingClose { common: CfdStateCommon },
|
|
/// The close transaction is confirmed with at least one block.
|
|
///
|
|
/// This state applies to taker and maker.
|
|
Closed { common: CfdStateCommon },
|
|
/// Error state
|
|
///
|
|
/// This state applies to taker and maker.
|
|
Error { common: CfdStateCommon },
|
|
}
|
|
|
|
impl CfdState {
|
|
fn get_common(&self) -> CfdStateCommon {
|
|
let common = match self {
|
|
CfdState::TakeRequested { common } => common,
|
|
CfdState::PendingTakeRequest { common } => common,
|
|
CfdState::Accepted { common } => common,
|
|
CfdState::Rejected { common } => common,
|
|
CfdState::ContractSetup { common } => common,
|
|
CfdState::Open { common, .. } => common,
|
|
CfdState::CloseRequested { common } => common,
|
|
CfdState::PendingClose { common } => common,
|
|
CfdState::Closed { common } => common,
|
|
CfdState::Error { common } => common,
|
|
};
|
|
|
|
*common
|
|
}
|
|
|
|
pub fn get_transition_timestamp(&self) -> SystemTime {
|
|
self.get_common().transition_timestamp
|
|
}
|
|
}
|
|
|
|
impl Display for CfdState {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
CfdState::TakeRequested { .. } => {
|
|
write!(f, "Take Requested")
|
|
}
|
|
CfdState::PendingTakeRequest { .. } => {
|
|
write!(f, "Pending Take Request")
|
|
}
|
|
CfdState::Accepted { .. } => {
|
|
write!(f, "Accepted")
|
|
}
|
|
CfdState::Rejected { .. } => {
|
|
write!(f, "Rejected")
|
|
}
|
|
CfdState::ContractSetup { .. } => {
|
|
write!(f, "Contract Setup")
|
|
}
|
|
CfdState::Open { .. } => {
|
|
write!(f, "Open")
|
|
}
|
|
CfdState::CloseRequested { .. } => {
|
|
write!(f, "Close Requested")
|
|
}
|
|
CfdState::PendingClose { .. } => {
|
|
write!(f, "Pending Close")
|
|
}
|
|
CfdState::Closed { .. } => {
|
|
write!(f, "Closed")
|
|
}
|
|
CfdState::Error { .. } => {
|
|
write!(f, "Error")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Represents a cfd (including state)
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
pub struct Cfd {
|
|
pub order_id: OrderId,
|
|
pub initial_price: Usd,
|
|
|
|
pub leverage: Leverage,
|
|
pub trading_pair: TradingPair,
|
|
pub position: Position,
|
|
pub liquidation_price: Usd,
|
|
|
|
pub quantity_usd: Usd,
|
|
|
|
pub state: CfdState,
|
|
}
|
|
|
|
impl Cfd {
|
|
pub fn new(cfd_order: Order, quantity: Usd, state: CfdState, position: Position) -> Self {
|
|
Cfd {
|
|
order_id: cfd_order.id,
|
|
initial_price: cfd_order.price,
|
|
leverage: cfd_order.leverage,
|
|
trading_pair: cfd_order.trading_pair,
|
|
position,
|
|
liquidation_price: cfd_order.liquidation_price,
|
|
quantity_usd: quantity,
|
|
state,
|
|
}
|
|
}
|
|
|
|
pub fn calc_margin(&self) -> Result<Amount> {
|
|
let margin = match self.position {
|
|
Position::Buy => {
|
|
calculate_buy_margin(self.initial_price, self.quantity_usd, self.leverage)?
|
|
}
|
|
Position::Sell => calculate_sell_margin(self.initial_price, self.quantity_usd)?,
|
|
};
|
|
|
|
Ok(margin)
|
|
}
|
|
|
|
pub fn calc_profit(&self, current_price: Usd) -> Result<(Amount, Usd)> {
|
|
let profit = calculate_profit(
|
|
self.initial_price,
|
|
current_price,
|
|
dec!(0.005),
|
|
Usd(dec!(0.1)),
|
|
)?;
|
|
Ok(profit)
|
|
}
|
|
}
|
|
|
|
fn calculate_profit(
|
|
_intial_price: Usd,
|
|
_current_price: Usd,
|
|
_interest_per_day: Decimal,
|
|
_fee: Usd,
|
|
) -> Result<(Amount, Usd)> {
|
|
// TODO: profit calculation
|
|
Ok((Amount::ZERO, Usd::ZERO))
|
|
}
|
|
|
|
pub trait AsBlocks {
|
|
/// Calculates the duration in Bitcoin blocks.
|
|
///
|
|
/// On Bitcoin there is a block every 10 minutes/600 seconds on average.
|
|
/// It's the caller's responsibility to round the resulting floating point number.
|
|
fn as_blocks(&self) -> f32;
|
|
}
|
|
|
|
impl AsBlocks for Duration {
|
|
fn as_blocks(&self) -> f32 {
|
|
self.as_secs_f32() / 60.0 / 10.0
|
|
}
|
|
}
|
|
|
|
/// Calculates the buyer's margin in BTC
|
|
///
|
|
/// 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)
|
|
}
|
|
|
|
/// Calculates the seller's margin in BTC
|
|
///
|
|
/// 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)?;
|
|
|
|
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)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use rust_decimal_macros::dec;
|
|
use std::time::UNIX_EPOCH;
|
|
|
|
#[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 liquidation_price =
|
|
calculate_liquidation_price(&leverage, &price, &maintenance_margin_rate).unwrap();
|
|
|
|
assert_eq!(liquidation_price, Usd(dec!(41004.184100418410041841004184)));
|
|
}
|
|
|
|
#[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 buy_margin = calculate_buy_margin(price, quantity, leverage).unwrap();
|
|
|
|
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 buy_margin = calculate_buy_margin(price, quantity, leverage).unwrap();
|
|
|
|
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 sell_margin = calculate_sell_margin(price, quantity).unwrap();
|
|
|
|
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 sell_margin = calculate_sell_margin(price, quantity).unwrap();
|
|
|
|
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 sell_margin = calculate_sell_margin(price, quantity).unwrap();
|
|
|
|
assert_eq!(sell_margin, Amount::from_btc(2.0).unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn serialize_cfd_state_snapshot() {
|
|
// This test is to prevent us from breaking the CfdState API against the database.
|
|
// We serialize the state into the database, so changes to the enum result in breaking
|
|
// program version changes.
|
|
|
|
let fixed_timestamp = UNIX_EPOCH;
|
|
|
|
let cfd_state = CfdState::TakeRequested {
|
|
common: CfdStateCommon {
|
|
transition_timestamp: fixed_timestamp,
|
|
},
|
|
};
|
|
let json = serde_json::to_string(&cfd_state).unwrap();
|
|
assert_eq!(
|
|
json,
|
|
r#"{"type":"TakeRequested","payload":{"common":{"transition_timestamp":{"secs_since_epoch":0,"nanos_since_epoch":0}}}}"#
|
|
);
|
|
|
|
let cfd_state = CfdState::PendingTakeRequest {
|
|
common: CfdStateCommon {
|
|
transition_timestamp: fixed_timestamp,
|
|
},
|
|
};
|
|
let json = serde_json::to_string(&cfd_state).unwrap();
|
|
assert_eq!(
|
|
json,
|
|
r#"{"type":"PendingTakeRequest","payload":{"common":{"transition_timestamp":{"secs_since_epoch":0,"nanos_since_epoch":0}}}}"#
|
|
);
|
|
|
|
let cfd_state = CfdState::Accepted {
|
|
common: CfdStateCommon {
|
|
transition_timestamp: fixed_timestamp,
|
|
},
|
|
};
|
|
let json = serde_json::to_string(&cfd_state).unwrap();
|
|
assert_eq!(
|
|
json,
|
|
r#"{"type":"Accepted","payload":{"common":{"transition_timestamp":{"secs_since_epoch":0,"nanos_since_epoch":0}}}}"#
|
|
);
|
|
|
|
let cfd_state = CfdState::ContractSetup {
|
|
common: CfdStateCommon {
|
|
transition_timestamp: fixed_timestamp,
|
|
},
|
|
};
|
|
let json = serde_json::to_string(&cfd_state).unwrap();
|
|
assert_eq!(
|
|
json,
|
|
r#"{"type":"ContractSetup","payload":{"common":{"transition_timestamp":{"secs_since_epoch":0,"nanos_since_epoch":0}}}}"#
|
|
);
|
|
|
|
let cfd_state = CfdState::Open {
|
|
common: CfdStateCommon {
|
|
transition_timestamp: fixed_timestamp,
|
|
},
|
|
settlement_timestamp: fixed_timestamp,
|
|
margin: Amount::from_btc(0.5).unwrap(),
|
|
};
|
|
let json = serde_json::to_string(&cfd_state).unwrap();
|
|
assert_eq!(
|
|
json,
|
|
r#"{"type":"Open","payload":{"common":{"transition_timestamp":{"secs_since_epoch":0,"nanos_since_epoch":0}},"settlement_timestamp":{"secs_since_epoch":0,"nanos_since_epoch":0},"margin":50000000}}"#
|
|
);
|
|
|
|
let cfd_state = CfdState::CloseRequested {
|
|
common: CfdStateCommon {
|
|
transition_timestamp: fixed_timestamp,
|
|
},
|
|
};
|
|
let json = serde_json::to_string(&cfd_state).unwrap();
|
|
assert_eq!(
|
|
json,
|
|
r#"{"type":"CloseRequested","payload":{"common":{"transition_timestamp":{"secs_since_epoch":0,"nanos_since_epoch":0}}}}"#
|
|
);
|
|
|
|
let cfd_state = CfdState::PendingClose {
|
|
common: CfdStateCommon {
|
|
transition_timestamp: fixed_timestamp,
|
|
},
|
|
};
|
|
let json = serde_json::to_string(&cfd_state).unwrap();
|
|
assert_eq!(
|
|
json,
|
|
r#"{"type":"PendingClose","payload":{"common":{"transition_timestamp":{"secs_since_epoch":0,"nanos_since_epoch":0}}}}"#
|
|
);
|
|
|
|
let cfd_state = CfdState::Closed {
|
|
common: CfdStateCommon {
|
|
transition_timestamp: fixed_timestamp,
|
|
},
|
|
};
|
|
let json = serde_json::to_string(&cfd_state).unwrap();
|
|
assert_eq!(
|
|
json,
|
|
r#"{"type":"Closed","payload":{"common":{"transition_timestamp":{"secs_since_epoch":0,"nanos_since_epoch":0}}}}"#
|
|
);
|
|
|
|
let cfd_state = CfdState::Error {
|
|
common: CfdStateCommon {
|
|
transition_timestamp: fixed_timestamp,
|
|
},
|
|
};
|
|
let json = serde_json::to_string(&cfd_state).unwrap();
|
|
assert_eq!(
|
|
json,
|
|
r#"{"type":"Error","payload":{"common":{"transition_timestamp":{"secs_since_epoch":0,"nanos_since_epoch":0}}}}"#
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_secs_into_blocks() {
|
|
let error_margin = f32::EPSILON;
|
|
|
|
let duration = Duration::from_secs(600);
|
|
let blocks = duration.as_blocks();
|
|
assert!(blocks - error_margin < 1.0 && blocks + error_margin > 1.0);
|
|
|
|
let duration = Duration::from_secs(0);
|
|
let blocks = duration.as_blocks();
|
|
assert!(blocks - error_margin < 0.0 && blocks + error_margin > 0.0);
|
|
|
|
let duration = Duration::from_secs(60);
|
|
let blocks = duration.as_blocks();
|
|
assert!(blocks - error_margin < 0.1 && blocks + error_margin > 0.1);
|
|
}
|
|
}
|
|
|
|
/// Contains all data we've assembled about the CFD through the setup protocol.
|
|
///
|
|
/// All contained signatures are the signatures of THE OTHER PARTY.
|
|
/// To use any of these transactions, we need to re-sign them with the correct secret key.
|
|
#[derive(Debug)]
|
|
pub struct FinalizedCfd {
|
|
pub identity: SecretKey,
|
|
pub revocation: SecretKey,
|
|
pub publish: SecretKey,
|
|
|
|
pub lock: PartiallySignedTransaction,
|
|
pub commit: (Transaction, EcdsaAdaptorSignature),
|
|
pub cets: Vec<(Transaction, EcdsaAdaptorSignature, Vec<u8>)>,
|
|
pub refund: (Transaction, Signature),
|
|
}
|
|
|