|
@ -1,17 +1,18 @@ |
|
|
use crate::model::{Leverage, Position, TakerId, TradingPair, Usd}; |
|
|
use crate::model::{Leverage, Percent, Position, TakerId, TradingPair, Usd}; |
|
|
use crate::monitor; |
|
|
use crate::monitor; |
|
|
use anyhow::{bail, Result}; |
|
|
use anyhow::{bail, Context, Result}; |
|
|
use bdk::bitcoin::secp256k1::{SecretKey, Signature}; |
|
|
use bdk::bitcoin::secp256k1::{SecretKey, Signature}; |
|
|
use bdk::bitcoin::{Address, Amount, PublicKey, Transaction}; |
|
|
use bdk::bitcoin::{Address, Amount, PublicKey, SignedAmount, Transaction}; |
|
|
use bdk::descriptor::Descriptor; |
|
|
use bdk::descriptor::Descriptor; |
|
|
use cfd_protocol::secp256k1_zkp::{EcdsaAdaptorSignature, SECP256K1}; |
|
|
use cfd_protocol::secp256k1_zkp::{EcdsaAdaptorSignature, SECP256K1}; |
|
|
use cfd_protocol::{finalize_spend_transaction, spending_tx_sighash}; |
|
|
use cfd_protocol::{finalize_spend_transaction, spending_tx_sighash}; |
|
|
use rocket::request::FromParam; |
|
|
use rocket::request::FromParam; |
|
|
|
|
|
use rust_decimal::prelude::{FromPrimitive, ToPrimitive}; |
|
|
use rust_decimal::Decimal; |
|
|
use rust_decimal::Decimal; |
|
|
use rust_decimal_macros::dec; |
|
|
use rust_decimal_macros::dec; |
|
|
use serde::{Deserialize, Serialize}; |
|
|
use serde::{Deserialize, Serialize}; |
|
|
use std::fmt; |
|
|
use std::fmt; |
|
|
use std::ops::RangeInclusive; |
|
|
use std::ops::{Neg, RangeInclusive}; |
|
|
use std::time::{Duration, SystemTime}; |
|
|
use std::time::{Duration, SystemTime}; |
|
|
use uuid::Uuid; |
|
|
use uuid::Uuid; |
|
|
|
|
|
|
|
@ -390,10 +391,16 @@ impl Cfd { |
|
|
Ok(margin) |
|
|
Ok(margin) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
pub fn profit(&self, current_price: Usd) -> Result<(Amount, Usd)> { |
|
|
pub fn profit(&self, current_price: Usd) -> Result<(SignedAmount, Percent)> { |
|
|
let profit = |
|
|
let (p_n_l, p_n_l_percent) = calculate_profit( |
|
|
calculate_profit(self.order.price, current_price, dec!(0.005), Usd(dec!(0.1)))?; |
|
|
self.order.price, |
|
|
Ok(profit) |
|
|
current_price, |
|
|
|
|
|
self.quantity_usd, |
|
|
|
|
|
self.margin()?, |
|
|
|
|
|
self.position(), |
|
|
|
|
|
)?; |
|
|
|
|
|
|
|
|
|
|
|
Ok((p_n_l, p_n_l_percent)) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
#[allow(dead_code)] // Not used by all binaries.
|
|
|
#[allow(dead_code)] // Not used by all binaries.
|
|
@ -702,14 +709,90 @@ pub enum CfdStateChangeEvent { |
|
|
CommitTxSent, |
|
|
CommitTxSent, |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// Returns the Profit/Loss (P/L) as Bitcoin. Losses are capped by the provided margin
|
|
|
fn calculate_profit( |
|
|
fn calculate_profit( |
|
|
_intial_price: Usd, |
|
|
initial_price: Usd, |
|
|
_current_price: Usd, |
|
|
current_price: Usd, |
|
|
_interest_per_day: Decimal, |
|
|
quantity: Usd, |
|
|
_fee: Usd, |
|
|
margin: Amount, |
|
|
) -> Result<(Amount, Usd)> { |
|
|
position: Position, |
|
|
// TODO: profit calculation
|
|
|
) -> Result<(SignedAmount, Percent)> { |
|
|
Ok((Amount::ZERO, Usd::ZERO)) |
|
|
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(current_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 { |
|
|
pub trait AsBlocks { |
|
@ -843,6 +926,123 @@ mod tests { |
|
|
let blocks = duration.as_blocks(); |
|
|
let blocks = duration.as_blocks(); |
|
|
assert!(blocks - error_margin < 0.1 && blocks + error_margin > 0.1); |
|
|
assert!(blocks - error_margin < 0.1 && blocks + error_margin > 0.1); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
#[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, |
|
|
|
|
|
Position::Buy, |
|
|
|
|
|
SignedAmount::ZERO, |
|
|
|
|
|
Decimal::ZERO.into(), |
|
|
|
|
|
"No price increase means no profit", |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
assert_profit_loss_values( |
|
|
|
|
|
Usd::from(dec!(10_000)), |
|
|
|
|
|
Usd::from(dec!(20_000)), |
|
|
|
|
|
Usd::from(dec!(10_000)), |
|
|
|
|
|
Amount::ONE_BTC, |
|
|
|
|
|
Position::Buy, |
|
|
|
|
|
SignedAmount::from_sat(50_000_000), |
|
|
|
|
|
dec!(50).into(), |
|
|
|
|
|
"A 100% price increase should be 50% profit", |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
assert_profit_loss_values( |
|
|
|
|
|
Usd::from(dec!(10_000)), |
|
|
|
|
|
Usd::from(dec!(5_000)), |
|
|
|
|
|
Usd::from(dec!(10_000)), |
|
|
|
|
|
Amount::ONE_BTC, |
|
|
|
|
|
Position::Buy, |
|
|
|
|
|
SignedAmount::from_sat(-100_000_000), |
|
|
|
|
|
dec!(-100).into(), |
|
|
|
|
|
"A 50% drop should result in 100% loss", |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
assert_profit_loss_values( |
|
|
|
|
|
Usd::from(dec!(10_000)), |
|
|
|
|
|
Usd::from(dec!(2_500)), |
|
|
|
|
|
Usd::from(dec!(10_000)), |
|
|
|
|
|
Amount::ONE_BTC, |
|
|
|
|
|
Position::Buy, |
|
|
|
|
|
SignedAmount::from_sat(-100_000_000), |
|
|
|
|
|
dec!(-100).into(), |
|
|
|
|
|
"A loss should be capped by 100%", |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
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(), |
|
|
|
|
|
Position::Buy, |
|
|
|
|
|
SignedAmount::from_sat(3_174_603), |
|
|
|
|
|
dec!(160.01024065540194572452620968).into(), |
|
|
|
|
|
"buy 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, |
|
|
|
|
|
Position::Sell, |
|
|
|
|
|
SignedAmount::from_sat(-37_500_000), |
|
|
|
|
|
dec!(-37.5).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, |
|
|
|
|
|
quantity: Usd, |
|
|
|
|
|
margin: Amount, |
|
|
|
|
|
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(); |
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(profit, should_profit, "{}", msg); |
|
|
|
|
|
assert_eq!(in_percent, should_profit_in_percent, "{}", msg); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
#[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 (profit, profit_in_percent) = calculate_profit( |
|
|
|
|
|
initial_price, |
|
|
|
|
|
closing_price, |
|
|
|
|
|
quantity, |
|
|
|
|
|
margin, |
|
|
|
|
|
Position::Buy, |
|
|
|
|
|
) |
|
|
|
|
|
.unwrap(); |
|
|
|
|
|
let (loss, loss_in_percent) = calculate_profit( |
|
|
|
|
|
initial_price, |
|
|
|
|
|
closing_price, |
|
|
|
|
|
quantity, |
|
|
|
|
|
margin, |
|
|
|
|
|
Position::Sell, |
|
|
|
|
|
) |
|
|
|
|
|
.unwrap(); |
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(profit.checked_add(loss).unwrap(), SignedAmount::ZERO); |
|
|
|
|
|
assert_eq!( |
|
|
|
|
|
profit_in_percent.0.checked_add(loss_in_percent.0).unwrap(), |
|
|
|
|
|
Decimal::ZERO |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/// Contains all data we've assembled about the CFD through the setup protocol.
|
|
|
/// Contains all data we've assembled about the CFD through the setup protocol.
|
|
|