Browse Source

Merge pull request #190 from comit-network/p_n_l_calculation

upload-correct-windows-binary
Philipp Hoenisch 3 years ago
committed by GitHub
parent
commit
485b8d1f1a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      daemon/src/model.rs
  2. 230
      daemon/src/model/cfd.rs
  3. 18
      daemon/src/to_sse_event.rs
  4. 2
      frontend/src/components/Types.tsx
  5. 9
      frontend/src/components/cfdtables/CfdTable.tsx

17
daemon/src/model.rs

@ -15,8 +15,6 @@ pub mod cfd;
pub struct Usd(pub Decimal); pub struct Usd(pub Decimal);
impl Usd { impl Usd {
pub const ZERO: Self = Self(Decimal::ZERO);
pub fn checked_add(&self, other: Usd) -> Result<Usd> { pub fn checked_add(&self, other: Usd) -> Result<Usd> {
let result = self.0.checked_add(other.0).context("addition error")?; let result = self.0.checked_add(other.0).context("addition error")?;
Ok(Usd(result)) Ok(Usd(result))
@ -57,6 +55,21 @@ impl From<Decimal> for Usd {
} }
} }
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
pub struct Percent(pub Decimal);
impl Display for Percent {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.0.round_dp(2).fmt(f)
}
}
impl From<Decimal> for Percent {
fn from(decimal: Decimal) -> Self {
Percent(decimal)
}
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
pub struct Leverage(pub u8); pub struct Leverage(pub u8);

230
daemon/src/model/cfd.rs

@ -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.

18
daemon/src/to_sse_event.rs

@ -1,9 +1,10 @@
use crate::model::cfd::{OrderId, Role}; use crate::model::cfd::{OrderId, Role};
use crate::model::{Leverage, Position, TradingPair, Usd}; use crate::model::{Leverage, Position, TradingPair, Usd};
use crate::{bitmex_price_feed, model}; use crate::{bitmex_price_feed, model};
use bdk::bitcoin::Amount; use bdk::bitcoin::{Amount, SignedAmount};
use rocket::request::FromParam; use rocket::request::FromParam;
use rocket::response::stream::Event; use rocket::response::stream::Event;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
@ -23,8 +24,8 @@ pub struct Cfd {
pub margin: Amount, pub margin: Amount,
#[serde(with = "::bdk::bitcoin::util::amount::serde::as_btc")] #[serde(with = "::bdk::bitcoin::util::amount::serde::as_btc")]
pub profit_btc: Amount, pub profit_btc: SignedAmount,
pub profit_usd: Usd, pub profit_in_percent: String,
pub state: CfdState, pub state: CfdState,
pub actions: Vec<CfdAction>, pub actions: Vec<CfdAction>,
@ -103,7 +104,14 @@ impl ToSseEvent for CfdsWithCurrentPrice {
.cfds .cfds
.iter() .iter()
.map(|cfd| { .map(|cfd| {
let (profit_btc, profit_usd) = cfd.profit(current_price).unwrap(); let (profit_btc, profit_in_percent) =
cfd.profit(current_price).unwrap_or_else(|error| {
tracing::warn!(
"Calculating profit/loss failed. Falling back to 0. {:#}",
error
);
(SignedAmount::ZERO, Decimal::ZERO.into())
});
Cfd { Cfd {
order_id: cfd.order.id, order_id: cfd.order.id,
@ -114,7 +122,7 @@ impl ToSseEvent for CfdsWithCurrentPrice {
liquidation_price: cfd.order.liquidation_price, liquidation_price: cfd.order.liquidation_price,
quantity_usd: cfd.quantity_usd, quantity_usd: cfd.quantity_usd,
profit_btc, profit_btc,
profit_usd, profit_in_percent: profit_in_percent.to_string(),
state: cfd.state.clone().into(), state: cfd.state.clone().into(),
actions: actions_for_state(cfd.state.clone(), cfd.role()), actions: actions_for_state(cfd.state.clone(), cfd.role()),
state_transition_timestamp: cfd state_transition_timestamp: cfd

2
frontend/src/components/Types.tsx

@ -43,7 +43,7 @@ export interface Cfd {
margin: number; margin: number;
profit_btc: number; profit_btc: number;
profit_usd: number; profit_in_percent: number;
state: State; state: State;
actions: Action[]; actions: Action[];

9
frontend/src/components/cfdtables/CfdTable.tsx

@ -131,9 +131,12 @@ export function CfdTable(
}, },
{ {
Header: "Unrealized P/L", Header: "Unrealized P/L",
accessor: ({ profit_usd }) => { accessor: "profit_btc",
return (<Dollars amount={profit_usd} />); isNumeric: true,
}, },
{
Header: "Unrealized P/L %",
accessor: "profit_in_percent",
isNumeric: true, isNumeric: true,
}, },
{ {

Loading…
Cancel
Save