Browse Source

Merge #744

744: Move Cfd types into the projection actor r=klochowicz a=klochowicz

This is the last stage of cleanup / moving things around, before I tackle the actual logic :)

Co-authored-by: Mariusz Klochowicz <mariusz@klochowicz.com>
chore/leaner-release-process
bors[bot] 3 years ago
committed by GitHub
parent
commit
03d34d4386
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 210
      daemon/src/projection.rs
  2. 4
      daemon/src/routes_maker.rs
  3. 15
      daemon/src/routes_taker.rs
  4. 158
      daemon/src/to_sse_event.rs
  5. 17
      daemon/src/tx.rs

210
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<Option<CfdOrder>>,
pub connected_takers: watch::Receiver<Vec<Identity>>,
// TODO: Convert items below here into projections
pub cfds: watch::Receiver<Vec<Cfd>>,
pub cfds: watch::Receiver<Vec<ModelCfd>>,
pub settlements: watch::Receiver<UpdateCfdProposals>,
}
impl Actor {
pub fn new(init_cfds: Vec<Cfd>, init_quote: bitmex_price_feed::Quote) -> (Self, Feeds) {
pub fn new(init_cfds: Vec<ModelCfd>, 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<Vec<Cfd>>,
pub cfds: watch::Sender<Vec<ModelCfd>>,
pub order: watch::Sender<Option<CfdOrder>>,
pub quote: watch::Sender<Quote>,
pub settlements: watch::Sender<UpdateCfdProposals>,
@ -73,7 +74,7 @@ impl Actor {
fn handle(&mut self, msg: Update<bitmex_price_feed::Quote>) {
let _ = self.tx.quote.send(msg.0.into());
}
fn handle(&mut self, msg: Update<Vec<Cfd>>) {
fn handle(&mut self, msg: Update<Vec<ModelCfd>>) {
let _ = self.tx.cfds.send(msg.0);
}
fn handle(&mut self, msg: Update<UpdateCfdProposals>) {
@ -184,28 +185,6 @@ impl From<Quote> 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<Amount>,
}
// 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<CfdAction> {
fn available_actions(state: CfdState, role: Role) -> Vec<CfdAction> {
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<CfdAction> {
_ => 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<CfdAction>,
pub state_transition_timestamp: i64,
pub details: CfdDetails,
#[serde(with = "::time::serde::timestamp")]
pub expiry_timestamp: OffsetDateTime,
}
impl From<&CfdsWithAuxData> for Vec<Cfd> {
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::<Vec<Cfd>>();
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<model::cfd::Cfd>,
pub current_price: model::Price,
pub pending_proposals: UpdateCfdProposals,
pub network: Network,
}
impl CfdsWithAuxData {
pub fn new(
rx_cfds: &watch::Receiver<Vec<model::cfd::Cfd>>,
rx_quote: &watch::Receiver<Quote>,
rx_updates: &watch::Receiver<UpdateCfdProposals>,
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\"");
}
}

4
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};

15
daemon/src/routes_taker.rs

@ -2,10 +2,10 @@ 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::{bitmex_price_feed, monitor, oracle, taker_cfd, wallet};
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};
use rocket::response::stream::EventStream;
@ -263,12 +263,5 @@ pub async fn post_withdraw_request(
.detail(e.to_string())
})?;
let url = match network.inner() {
Network::Bitcoin => format!("https://mempool.space/tx/{}", txid),
Network::Testnet => format!("https://mempool.space/testnet/tx/{}", txid),
Network::Signet => format!("https://mempool.space/signet/tx/{}", txid),
Network::Regtest => txid.to_string(),
};
Ok(url)
Ok(tx::to_mempool_url(txid, *network.inner()))
}

158
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<CfdAction>,
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<model::cfd::Cfd>,
pub current_price: model::Price,
pub pending_proposals: UpdateCfdProposals,
pub network: Network,
}
impl CfdsWithAuxData {
pub fn new(
rx_cfds: &watch::Receiver<Vec<model::cfd::Cfd>>,
rx_quote: &watch::Receiver<Quote>,
rx_updates: &watch::Receiver<UpdateCfdProposals>,
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::<Vec<Cfd>>();
let cfds: Vec<Cfd> = 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\"");
}
}

17
daemon/src/tx.rs

@ -9,16 +9,21 @@ pub struct TxUrl {
pub url: String,
}
/// Construct a mempool.space URL for a given txid
pub fn to_mempool_url(txid: Txid, network: Network) -> String {
match network {
Network::Bitcoin => format!("https://mempool.space/tx/{}", txid),
Network::Testnet => format!("https://mempool.space/testnet/tx/{}", txid),
Network::Signet => format!("https://mempool.space/signet/tx/{}", txid),
Network::Regtest => txid.to_string(),
}
}
impl TxUrl {
pub fn new(txid: Txid, network: Network, label: TxLabel) -> Self {
Self {
label,
url: match network {
Network::Bitcoin => format!("https://mempool.space/tx/{}", txid),
Network::Testnet => format!("https://mempool.space/testnet/tx/{}", txid),
Network::Signet => format!("https://mempool.space/signet/tx/{}", txid),
Network::Regtest => txid.to_string(),
},
url: to_mempool_url(txid, network),
}
}
}

Loading…
Cancel
Save