From 7464ababc89fef6678d5e8488a9c01f0f96d0283 Mon Sep 17 00:00:00 2001 From: Mariusz Klochowicz Date: Mon, 29 Nov 2021 15:35:29 +1030 Subject: [PATCH] Move Cfd types into the projection actor Move the logic of creating Cfd representation suitable for UI into the projection actor For the time being, keep the logic as free functions that are still used in the "old" way (inside ToSseEvent) --- daemon/src/projection.rs | 210 +++++++++++++++++++++++++++++++------ daemon/src/routes_maker.rs | 4 +- daemon/src/routes_taker.rs | 4 +- daemon/src/to_sse_event.rs | 158 +--------------------------- 4 files changed, 187 insertions(+), 189 deletions(-) diff --git a/daemon/src/projection.rs b/daemon/src/projection.rs index 1bf5533..33147ae 100644 --- a/daemon/src/projection.rs +++ b/daemon/src/projection.rs @@ -1,12 +1,13 @@ use std::collections::HashMap; -use crate::model::cfd::{OrderId, Role, SettlementKind, UpdateCfdProposal}; +use crate::model::cfd::{Cfd as ModelCfd, OrderId, Role, SettlementKind, UpdateCfdProposal}; use crate::model::{Leverage, Position, Timestamp, TradingPair}; -use crate::{bitmex_price_feed, model, tx, Cfd, Order, UpdateCfdProposals}; -use bdk::bitcoin::{Amount, Network}; +use crate::{bitmex_price_feed, model, tx, Order, UpdateCfdProposals}; +use bdk::bitcoin::{Amount, Network, SignedAmount}; use itertools::Itertools; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; use tokio::sync::watch; use xtra_productivity::xtra_productivity; @@ -19,12 +20,12 @@ pub struct Feeds { pub order: watch::Receiver>, pub connected_takers: watch::Receiver>, // TODO: Convert items below here into projections - pub cfds: watch::Receiver>, + pub cfds: watch::Receiver>, pub settlements: watch::Receiver, } impl Actor { - pub fn new(init_cfds: Vec, init_quote: bitmex_price_feed::Quote) -> (Self, Feeds) { + pub fn new(init_cfds: Vec, init_quote: bitmex_price_feed::Quote) -> (Self, Feeds) { let (tx_cfds, rx_cfds) = watch::channel(init_cfds); let (tx_order, rx_order) = watch::channel(None); let (tx_update_cfd_feed, rx_update_cfd_feed) = watch::channel(HashMap::new()); @@ -54,7 +55,7 @@ impl Actor { /// Internal struct to keep all the senders around in one place struct Tx { - pub cfds: watch::Sender>, + pub cfds: watch::Sender>, pub order: watch::Sender>, pub quote: watch::Sender, pub settlements: watch::Sender, @@ -73,7 +74,7 @@ impl Actor { fn handle(&mut self, msg: Update) { let _ = self.tx.quote.send(msg.0.into()); } - fn handle(&mut self, msg: Update>) { + fn handle(&mut self, msg: Update>) { let _ = self.tx.cfds.send(msg.0); } fn handle(&mut self, msg: Update) { @@ -184,28 +185,6 @@ impl From for bitmex_price_feed::Quote { } } -#[cfg(test)] -mod tests { - use super::*; - - use rust_decimal_macros::dec; - use serde_test::{assert_ser_tokens, Token}; - - #[test] - fn usd_serializes_with_only_cents() { - let usd = Usd::new(model::Usd::new(dec!(1000.12345))); - - assert_ser_tokens(&usd, &[Token::Str("1000.12")]); - } - - #[test] - fn price_serializes_with_only_cents() { - let price = Price::new(model::Price::new(dec!(1000.12345)).unwrap()); - - assert_ser_tokens(&price, &[Token::Str("1000.12")]); - } -} - #[derive(Debug, Clone, PartialEq, Serialize)] pub struct CfdOrder { pub id: OrderId, @@ -337,8 +316,7 @@ pub struct CfdDetails { payout: Option, } -// TODO: don't expose -pub fn to_cfd_details(cfd: &model::cfd::Cfd, network: Network) -> CfdDetails { +fn to_cfd_details(cfd: &model::cfd::Cfd, network: Network) -> CfdDetails { CfdDetails { tx_url_list: tx::to_tx_url_list(cfd.state.clone(), network), payout: cfd.payout(), @@ -359,7 +337,7 @@ pub enum CfdAction { RejectRollOver, } -pub fn available_actions(state: CfdState, role: Role) -> Vec { +fn available_actions(state: CfdState, role: Role) -> Vec { match (state, role) { (CfdState::IncomingOrderRequest { .. }, Role::Maker) => { vec![CfdAction::AcceptOrder, CfdAction::RejectOrder] @@ -386,3 +364,171 @@ pub fn available_actions(state: CfdState, role: Role) -> Vec { _ => vec![], } } + +#[derive(Debug, Clone, Serialize)] +pub struct Cfd { + pub order_id: OrderId, + pub initial_price: Price, + + pub leverage: Leverage, + pub trading_pair: TradingPair, + pub position: Position, + pub liquidation_price: Price, + + pub quantity_usd: Usd, + + #[serde(with = "::bdk::bitcoin::util::amount::serde::as_btc")] + pub margin: Amount, + #[serde(with = "::bdk::bitcoin::util::amount::serde::as_btc")] + pub margin_counterparty: Amount, + + #[serde(with = "::bdk::bitcoin::util::amount::serde::as_btc")] + pub profit_btc: SignedAmount, + pub profit_in_percent: String, + + pub state: CfdState, + pub actions: Vec, + pub state_transition_timestamp: i64, + + pub details: CfdDetails, + + #[serde(with = "::time::serde::timestamp")] + pub expiry_timestamp: OffsetDateTime, +} + +impl From<&CfdsWithAuxData> for Vec { + fn from(input: &CfdsWithAuxData) -> Self { + let current_price = input.current_price; + let network = input.network; + + let cfds = input + .cfds + .iter() + .map(|cfd| { + 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()) + }); + + let pending_proposal = input.pending_proposals.get(&cfd.order.id); + let state = to_cfd_state(&cfd.state, pending_proposal); + + Cfd { + order_id: cfd.order.id, + initial_price: cfd.order.price.into(), + leverage: cfd.order.leverage, + trading_pair: cfd.order.trading_pair.clone(), + position: cfd.position(), + liquidation_price: cfd.order.liquidation_price.into(), + quantity_usd: cfd.quantity_usd.into(), + profit_btc, + profit_in_percent: profit_in_percent.round_dp(1).to_string(), + state: state.clone(), + actions: available_actions(state, cfd.role()), + state_transition_timestamp: cfd.state.get_transition_timestamp().seconds(), + + // TODO: Depending on the state the margin might be set (i.e. in Open we save it + // in the DB internally) and does not have to be calculated + margin: cfd.margin().expect("margin to be available"), + margin_counterparty: cfd.counterparty_margin().expect("margin to be available"), + details: to_cfd_details(cfd, network), + expiry_timestamp: match cfd.expiry_timestamp() { + None => cfd.order.oracle_event_id.timestamp(), + Some(timestamp) => timestamp, + }, + } + }) + .collect::>(); + cfds + } +} + +/// Intermediate struct to able to piggy-back additional information along with +/// cfds, so we can avoid a 1:1 mapping between the states in the model and seen +/// by UI +// TODO: Remove this struct out of existence +pub struct CfdsWithAuxData { + pub cfds: Vec, + pub current_price: model::Price, + pub pending_proposals: UpdateCfdProposals, + pub network: Network, +} + +impl CfdsWithAuxData { + pub fn new( + rx_cfds: &watch::Receiver>, + rx_quote: &watch::Receiver, + rx_updates: &watch::Receiver, + role: Role, + network: Network, + ) -> Self { + let quote: bitmex_price_feed::Quote = rx_quote.borrow().clone().into(); + let current_price = match role { + Role::Maker => quote.for_maker(), + Role::Taker => quote.for_taker(), + }; + + let pending_proposals = rx_updates.borrow().clone(); + + CfdsWithAuxData { + cfds: rx_cfds.borrow().clone(), + current_price, + pending_proposals, + network, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use rust_decimal_macros::dec; + use serde_test::{assert_ser_tokens, Token}; + + #[test] + fn usd_serializes_with_only_cents() { + let usd = Usd::new(model::Usd::new(dec!(1000.12345))); + + assert_ser_tokens(&usd, &[Token::Str("1000.12")]); + } + + #[test] + fn price_serializes_with_only_cents() { + let price = Price::new(model::Price::new(dec!(1000.12345)).unwrap()); + + assert_ser_tokens(&price, &[Token::Str("1000.12")]); + } + + #[test] + fn state_snapshot_test() { + // Make sure to update the UI after changing this test! + + let json = serde_json::to_string(&CfdState::OutgoingOrderRequest).unwrap(); + assert_eq!(json, "\"OutgoingOrderRequest\""); + let json = serde_json::to_string(&CfdState::IncomingOrderRequest).unwrap(); + assert_eq!(json, "\"IncomingOrderRequest\""); + let json = serde_json::to_string(&CfdState::Accepted).unwrap(); + assert_eq!(json, "\"Accepted\""); + let json = serde_json::to_string(&CfdState::Rejected).unwrap(); + assert_eq!(json, "\"Rejected\""); + let json = serde_json::to_string(&CfdState::ContractSetup).unwrap(); + assert_eq!(json, "\"ContractSetup\""); + let json = serde_json::to_string(&CfdState::PendingOpen).unwrap(); + assert_eq!(json, "\"PendingOpen\""); + let json = serde_json::to_string(&CfdState::Open).unwrap(); + assert_eq!(json, "\"Open\""); + let json = serde_json::to_string(&CfdState::OpenCommitted).unwrap(); + assert_eq!(json, "\"OpenCommitted\""); + let json = serde_json::to_string(&CfdState::PendingRefund).unwrap(); + assert_eq!(json, "\"PendingRefund\""); + let json = serde_json::to_string(&CfdState::Refunded).unwrap(); + assert_eq!(json, "\"Refunded\""); + let json = serde_json::to_string(&CfdState::SetupFailed).unwrap(); + assert_eq!(json, "\"SetupFailed\""); + } +} diff --git a/daemon/src/routes_maker.rs b/daemon/src/routes_maker.rs index 588a411..865a6af 100644 --- a/daemon/src/routes_maker.rs +++ b/daemon/src/routes_maker.rs @@ -3,9 +3,9 @@ use bdk::bitcoin::Network; use daemon::auth::Authenticated; use daemon::model::cfd::{OrderId, Role}; use daemon::model::{Price, Usd, WalletInfo}; -use daemon::projection::{CfdAction, Feeds}; +use daemon::projection::{CfdAction, CfdsWithAuxData, Feeds}; use daemon::routes::EmbeddedFileExt; -use daemon::to_sse_event::{CfdsWithAuxData, ToSseEvent}; +use daemon::to_sse_event::ToSseEvent; use daemon::{maker_cfd, maker_inc_connections, monitor, oracle, wallet}; use http_api_problem::{HttpApiProblem, StatusCode}; use rocket::http::{ContentType, Header, Status}; diff --git a/daemon/src/routes_taker.rs b/daemon/src/routes_taker.rs index 475b91c..59812b5 100644 --- a/daemon/src/routes_taker.rs +++ b/daemon/src/routes_taker.rs @@ -2,9 +2,9 @@ use bdk::bitcoin::{Amount, Network}; use daemon::connection::ConnectionStatus; use daemon::model::cfd::{calculate_long_margin, OrderId, Role}; use daemon::model::{Leverage, Price, Usd, WalletInfo}; -use daemon::projection::{CfdAction, Feeds}; +use daemon::projection::{CfdAction, CfdsWithAuxData, Feeds}; use daemon::routes::EmbeddedFileExt; -use daemon::to_sse_event::{CfdsWithAuxData, ToSseEvent}; +use daemon::to_sse_event::ToSseEvent; use daemon::{bitmex_price_feed, monitor, oracle, taker_cfd, tx, wallet}; use http_api_problem::{HttpApiProblem, StatusCode}; use rocket::http::{ContentType, Status}; diff --git a/daemon/src/to_sse_event.rs b/daemon/src/to_sse_event.rs index 526fdbb..2d183c5 100644 --- a/daemon/src/to_sse_event.rs +++ b/daemon/src/to_sse_event.rs @@ -1,46 +1,11 @@ use crate::connection::ConnectionStatus; -use crate::model::cfd::{OrderId, Role, UpdateCfdProposals}; -use crate::model::{Leverage, Position, Timestamp, TradingPair}; -use crate::projection::{self, CfdAction, CfdOrder, CfdState, Identity, Price, Quote, Usd}; -use crate::{bitmex_price_feed, model}; -use bdk::bitcoin::{Amount, Network, SignedAmount}; +use crate::model; +use crate::model::Timestamp; +use crate::projection::{Cfd, CfdAction, CfdOrder, CfdsWithAuxData, Identity, Quote}; +use bdk::bitcoin::Amount; use rocket::request::FromParam; use rocket::response::stream::Event; -use rust_decimal::Decimal; use serde::Serialize; -use time::OffsetDateTime; -use tokio::sync::watch; - -#[derive(Debug, Clone, Serialize)] -pub struct Cfd { - pub order_id: OrderId, - pub initial_price: Price, - - pub leverage: Leverage, - pub trading_pair: TradingPair, - pub position: Position, - pub liquidation_price: Price, - - pub quantity_usd: Usd, - - #[serde(with = "::bdk::bitcoin::util::amount::serde::as_btc")] - pub margin: Amount, - #[serde(with = "::bdk::bitcoin::util::amount::serde::as_btc")] - pub margin_counterparty: Amount, - - #[serde(with = "::bdk::bitcoin::util::amount::serde::as_btc")] - pub profit_btc: SignedAmount, - pub profit_in_percent: String, - - pub state: CfdState, - pub actions: Vec, - pub state_transition_timestamp: i64, - - pub details: projection::CfdDetails, - - #[serde(with = "::time::serde::timestamp")] - pub expiry_timestamp: OffsetDateTime, -} impl<'v> FromParam<'v> for CfdAction { type Error = serde_plain::Error; @@ -55,90 +20,10 @@ pub trait ToSseEvent { fn to_sse_event(&self) -> Event; } -/// Intermediate struct to able to piggy-back additional information along with -/// cfds, so we can avoid a 1:1 mapping between the states in the model and seen -/// by UI -pub struct CfdsWithAuxData { - pub cfds: Vec, - pub current_price: model::Price, - pub pending_proposals: UpdateCfdProposals, - pub network: Network, -} - -impl CfdsWithAuxData { - pub fn new( - rx_cfds: &watch::Receiver>, - rx_quote: &watch::Receiver, - rx_updates: &watch::Receiver, - role: Role, - network: Network, - ) -> Self { - let quote: bitmex_price_feed::Quote = rx_quote.borrow().clone().into(); - let current_price = match role { - Role::Maker => quote.for_maker(), - Role::Taker => quote.for_taker(), - }; - - let pending_proposals = rx_updates.borrow().clone(); - - CfdsWithAuxData { - cfds: rx_cfds.borrow().clone(), - current_price, - pending_proposals, - network, - } - } -} - impl ToSseEvent for CfdsWithAuxData { // TODO: This conversion can fail, we might want to change the API fn to_sse_event(&self) -> Event { - let current_price = self.current_price; - let network = self.network; - - let cfds = self - .cfds - .iter() - .map(|cfd| { - 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()) - }); - - let pending_proposal = self.pending_proposals.get(&cfd.order.id); - let state = projection::to_cfd_state(&cfd.state, pending_proposal); - - Cfd { - order_id: cfd.order.id, - initial_price: cfd.order.price.into(), - leverage: cfd.order.leverage, - trading_pair: cfd.order.trading_pair.clone(), - position: cfd.position(), - liquidation_price: cfd.order.liquidation_price.into(), - quantity_usd: cfd.quantity_usd.into(), - profit_btc, - profit_in_percent: profit_in_percent.round_dp(1).to_string(), - state: state.clone(), - actions: projection::available_actions(state, cfd.role()), - state_transition_timestamp: cfd.state.get_transition_timestamp().seconds(), - - // TODO: Depending on the state the margin might be set (i.e. in Open we save it - // in the DB internally) and does not have to be calculated - margin: cfd.margin().expect("margin to be available"), - margin_counterparty: cfd.counterparty_margin().expect("margin to be available"), - details: projection::to_cfd_details(cfd, network), - expiry_timestamp: match cfd.expiry_timestamp() { - None => cfd.order.oracle_event_id.timestamp(), - Some(timestamp) => timestamp, - }, - } - }) - .collect::>(); - + let cfds: Vec = self.into(); Event::json(&cfds).event("cfds") } } @@ -191,36 +76,3 @@ impl ToSseEvent for Quote { Event::json(self).event("quote") } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn state_snapshot_test() { - // Make sure to update the UI after changing this test! - - let json = serde_json::to_string(&CfdState::OutgoingOrderRequest).unwrap(); - assert_eq!(json, "\"OutgoingOrderRequest\""); - let json = serde_json::to_string(&CfdState::IncomingOrderRequest).unwrap(); - assert_eq!(json, "\"IncomingOrderRequest\""); - let json = serde_json::to_string(&CfdState::Accepted).unwrap(); - assert_eq!(json, "\"Accepted\""); - let json = serde_json::to_string(&CfdState::Rejected).unwrap(); - assert_eq!(json, "\"Rejected\""); - let json = serde_json::to_string(&CfdState::ContractSetup).unwrap(); - assert_eq!(json, "\"ContractSetup\""); - let json = serde_json::to_string(&CfdState::PendingOpen).unwrap(); - assert_eq!(json, "\"PendingOpen\""); - let json = serde_json::to_string(&CfdState::Open).unwrap(); - assert_eq!(json, "\"Open\""); - let json = serde_json::to_string(&CfdState::OpenCommitted).unwrap(); - assert_eq!(json, "\"OpenCommitted\""); - let json = serde_json::to_string(&CfdState::PendingRefund).unwrap(); - assert_eq!(json, "\"PendingRefund\""); - let json = serde_json::to_string(&CfdState::Refunded).unwrap(); - assert_eq!(json, "\"Refunded\""); - let json = serde_json::to_string(&CfdState::SetupFailed).unwrap(); - assert_eq!(json, "\"SetupFailed\""); - } -}