Skip to content

Commit

Permalink
Add tests for coordinator pnl calculation
Browse files Browse the repository at this point in the history
Additionally adds more tests to `trade::cfd` that reflect the pnl calculation on the trader side.
This reflects that the traders losses are the coordinator's profits and vice versa.
  • Loading branch information
da-kami committed Jul 25, 2023
1 parent c9925c3 commit cf3f06f
Show file tree
Hide file tree
Showing 4 changed files with 290 additions and 33 deletions.
2 changes: 1 addition & 1 deletion coordinator/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ pub mod storage;
pub mod unrealized_pnl;

/// The leverage used by the coordinator for all trades.
const COORDINATOR_LEVERAGE: f32 = 1.0;
pub const COORDINATOR_LEVERAGE: f32 = 1.0;

#[derive(Debug, Clone)]
pub struct NodeSettings {
Expand Down
33 changes: 1 addition & 32 deletions coordinator/src/node/unrealized_pnl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,9 @@ use anyhow::Result;
use diesel::r2d2::ConnectionManager;
use diesel::r2d2::PooledConnection;
use diesel::PgConnection;
use rust_decimal::Decimal;
use time::OffsetDateTime;
use trade::bitmex_client::BitmexClient;
use trade::bitmex_client::Quote;
use trade::cfd::calculate_pnl;
use trade::cfd::closing_price;
use trade::Direction;

pub async fn sync(node: Node) -> Result<()> {
let mut conn = node.pool.get()?;
Expand All @@ -36,34 +32,7 @@ fn sync_position(
position: &Position,
quote: Quote,
) -> Result<()> {
let closing_price = match position.closing_price {
None => closing_price(position.direction, quote),
Some(closing_price) => {
Decimal::try_from(closing_price).expect("f32 closing price to fit into decimal")
}
};

let average_entry_price = Decimal::try_from(position.average_entry_price)
.context("Failed to convert average entry price to Decimal")?;

let (long_leverage, short_leverage) = match position.direction {
Direction::Long => (position.leverage, 1.0_f32),
Direction::Short => (1.0_f32, position.leverage),
};

// the position in the database is the trader's position, our direction is opposite
let direction = position.direction.opposite();

let pnl = calculate_pnl(
average_entry_price,
closing_price,
position.quantity,
long_leverage,
short_leverage,
direction,
)
.context("Failed to calculate pnl for position")?;

let pnl = position.calculate_coordinator_pnl(quote)?;
db::positions::Position::update_unrealized_pnl(conn, position.id, pnl)
.context("Failed to update unrealized pnl in db")?;

Expand Down
200 changes: 200 additions & 0 deletions coordinator/src/position/models.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
use crate::node::COORDINATOR_LEVERAGE;
use anyhow::Context;
use anyhow::Result;
use bitcoin::secp256k1::PublicKey;
use dlc_manager::ContractId;
use rust_decimal::Decimal;
use time::OffsetDateTime;
use trade::bitmex_client::Quote;
use trade::cfd::calculate_pnl;
use trade::cfd::closing_price;
use trade::ContractSymbol;
use trade::Direction;

Expand Down Expand Up @@ -51,3 +58,196 @@ pub struct Position {
pub temporary_contract_id: Option<ContractId>,
pub closing_price: Option<f32>,
}

impl Position {
/// Calculates the profit and loss for the coordinator in satoshis
///
/// The position stored represents the values of the trader.
pub fn calculate_coordinator_pnl(&self, quote: Quote) -> Result<i64> {
let closing_price = match self.closing_price {
None => closing_price(self.direction, quote),
Some(closing_price) => {
Decimal::try_from(closing_price).expect("f32 closing price to fit into decimal")
}
};

let average_entry_price = Decimal::try_from(self.average_entry_price)
.context("Failed to convert average entry price to Decimal")?;

let (long_leverage, short_leverage) = match self.direction {
Direction::Long => (self.leverage, COORDINATOR_LEVERAGE),
Direction::Short => (COORDINATOR_LEVERAGE, self.leverage),
};

// the position in the database is the trader's position, our direction is opposite
let direction = self.direction.opposite();

let pnl = calculate_pnl(
average_entry_price,
closing_price,
self.quantity,
long_leverage,
short_leverage,
direction,
)
.context("Failed to calculate pnl for position")?;

Ok(pnl)
}
}

#[cfg(test)]
pub mod tests {
use super::*;
use std::str::FromStr;

#[test]
fn given_trader_long_position_when_no_bid_price_change_then_zero_pnl() {
let position = Position::dummy()
.with_leverage(2.0)
.with_quantity(1.0)
.with_average_entry_price(1000.0)
.with_direction(Direction::Long);

let quote = dummy_quote(1000, 0);

let coordinator_pnl = position.calculate_coordinator_pnl(quote).unwrap();

assert_eq!(coordinator_pnl, 0);
}

#[test]
fn given_trader_short_position_when_no_ask_price_change_then_zero_pnl() {
let position = Position::dummy()
.with_leverage(2.0)
.with_quantity(1.0)
.with_average_entry_price(1000.0)
.with_direction(Direction::Short);

let quote = dummy_quote(0, 1000);

let coordinator_pnl = position.calculate_coordinator_pnl(quote).unwrap();

assert_eq!(coordinator_pnl, 0);
}

/// See also: `given_long_position_when_price_10_pc_up_then_18pc_profit` test in `trade::cfd`
#[test]
fn given_trader_long_position_when_bid_price_10pc_up_then_coordinator_18pc_loss() {
let position = Position::dummy()
.with_leverage(2.0)
.with_quantity(20000.0)
.with_average_entry_price(20000.0)
.with_direction(Direction::Long);

let quote = dummy_quote(22000, 0);

let coordinator_pnl = position.calculate_coordinator_pnl(quote).unwrap();

assert_eq!(coordinator_pnl, -9_090_909);
}

/// See also: `given_short_position_when_price_10_pc_up_then_18pc_loss` test in `trade::cfd`
#[test]
fn given_trader_short_position_when_bid_price_10pc_up_then_coordinator_18pc_profit() {
let position = Position::dummy()
.with_leverage(2.0)
.with_quantity(20000.0)
.with_average_entry_price(20000.0)
.with_direction(Direction::Short);

let quote = dummy_quote(0, 22000);

let coordinator_pnl = position.calculate_coordinator_pnl(quote).unwrap();

assert_eq!(coordinator_pnl, 9_090_909);
}

/// See also: `given_long_position_when_price_10_pc_down_then_22pc_loss` test in `trade::cfd`
#[test]
fn given_trader_long_position_when_bid_price_10pc_down_then_coordinator_22pc_profit() {
let position = Position::dummy()
.with_leverage(2.0)
.with_quantity(20000.0)
.with_average_entry_price(20000.0)
.with_direction(Direction::Long);

let quote = dummy_quote(18000, 0);

let coordinator_pnl = position.calculate_coordinator_pnl(quote).unwrap();

assert_eq!(coordinator_pnl, 11_111_111);
}

/// See also: `given_short_position_when_price_10_pc_down_then_22pc_profit` test in `trade::cfd`
#[test]
fn given_trader_short_position_when_bid_price_10pc_down_then_coordinator_22pc_loss() {
let position = Position::dummy()
.with_leverage(2.0)
.with_quantity(20000.0)
.with_average_entry_price(20000.0)
.with_direction(Direction::Short);

let quote = dummy_quote(0, 18000);

let coordinator_pnl = position.calculate_coordinator_pnl(quote).unwrap();

assert_eq!(coordinator_pnl, -11_111_111);
}

fn dummy_quote(bid: u64, ask: u64) -> Quote {
Quote {
bid_size: 0,
ask_size: 0,
bid_price: Decimal::from(bid),
ask_price: Decimal::from(ask),
symbol: "".to_string(),
timestamp: OffsetDateTime::now_utc(),
}
}

impl Position {
fn dummy() -> Self {
Position {
id: 0,
contract_symbol: ContractSymbol::BtcUsd,
leverage: 2.0,
quantity: 100.0,
direction: Direction::Long,
average_entry_price: 10000.0,
liquidation_price: 0.0,
position_state: PositionState::Open,
collateral: 1000,
creation_timestamp: OffsetDateTime::now_utc(),
expiry_timestamp: OffsetDateTime::now_utc(),
update_timestamp: OffsetDateTime::now_utc(),
trader: PublicKey::from_str(
"02bd998ebd176715fe92b7467cf6b1df8023950a4dd911db4c94dfc89cc9f5a655",
)
.unwrap(),
temporary_contract_id: None,
closing_price: None,
}
}

fn with_quantity(mut self, quantity: f32) -> Self {
self.quantity = quantity;
self
}

fn with_average_entry_price(mut self, average_entry_price: f32) -> Self {
self.average_entry_price = average_entry_price;
self
}

fn with_leverage(mut self, leverage: f32) -> Self {
self.leverage = leverage;
self
}

fn with_direction(mut self, direction: Direction) -> Self {
self.direction = direction;
self
}
}
}
88 changes: 88 additions & 0 deletions crates/trade/src/cfd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,4 +270,92 @@ pub mod tests {
let closing_price = closing_price(Direction::Short, quote.clone());
assert_eq!(closing_price, quote.ask_price)
}

#[test]
fn given_long_position_when_price_10_pc_up_then_18pc_profit() {
let opening_price = Decimal::from(20000);
let closing_price = Decimal::from(22000);
let quantity = 20000.0;
let long_leverage = 2.0;
let short_leverage = 1.0;

let pnl_long = calculate_pnl(
opening_price,
closing_price,
quantity,
long_leverage,
short_leverage,
Direction::Long,
)
.unwrap();

// Value taken from our CFD hedging model sheet
assert_eq!(pnl_long, 9_090_909);
}

#[test]
fn given_short_position_when_price_10_pc_up_then_18pc_loss() {
let opening_price = Decimal::from(20000);
let closing_price = Decimal::from(22000);
let quantity = 20000.0;
let long_leverage = 2.0;
let short_leverage = 1.0;

let pnl_long = calculate_pnl(
opening_price,
closing_price,
quantity,
long_leverage,
short_leverage,
Direction::Short,
)
.unwrap();

// Value taken from our CFD hedging model sheet
assert_eq!(pnl_long, -9_090_909);
}

#[test]
fn given_long_position_when_price_10_pc_down_then_22pc_loss() {
let opening_price = Decimal::from(20000);
let closing_price = Decimal::from(18000);
let quantity = 20000.0;
let long_leverage = 2.0;
let short_leverage = 1.0;

let pnl_long = calculate_pnl(
opening_price,
closing_price,
quantity,
long_leverage,
short_leverage,
Direction::Long,
)
.unwrap();

// Value taken from our CFD hedging model sheet
assert_eq!(pnl_long, -11_111_111);
}

#[test]
fn given_short_position_when_price_10_pc_down_then_22pc_profit() {
let opening_price = Decimal::from(20000);
let closing_price = Decimal::from(18000);
let quantity = 20000.0;
let long_leverage = 2.0;
let short_leverage = 1.0;

let pnl_long = calculate_pnl(
opening_price,
closing_price,
quantity,
long_leverage,
short_leverage,
Direction::Short,
)
.unwrap();

// Value taken from our CFD hedging model sheet
assert_eq!(pnl_long, 11_111_111);
}
}

0 comments on commit cf3f06f

Please sign in to comment.