Skip to content
This repository has been archived by the owner on Sep 12, 2023. It is now read-only.

Feature: market close order #3244

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 1 addition & 44 deletions daemon/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
use crate::bitcoin::util::psbt::PartiallySignedTransaction;
use crate::bitcoin::Txid;
use crate::listen_protocols::TAKER_LISTEN_PROTOCOLS;
use anyhow::bail;
use anyhow::Context as _;
use anyhow::Result;
pub use bdk;
Expand All @@ -23,7 +22,6 @@ use model::Identity;
use model::Leverage;
use model::OfferId;
use model::OrderId;
use model::Price;
use model::Role;
use online_status::ConnectionStatus;
use parse_display::Display;
Expand All @@ -33,12 +31,10 @@ use seed::Identities;
use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;
use time::ext::NumericalDuration;
use tokio::sync::watch;
use tokio_extras::Tasks;
use tracing::instrument;
use xtra::prelude::*;
use xtra_bitmex_price_feed::QUOTE_INTERVAL_MINUTES;
use xtra_libp2p::dialer;
use xtra_libp2p::endpoint;
use xtra_libp2p::multiaddress_ext::MultiaddrExt;
Expand Down Expand Up @@ -387,40 +383,8 @@ where

#[instrument(skip(self), err)]
pub async fn propose_settlement(&self, order_id: OrderId) -> Result<()> {
let contract_symbol = self
.executor
.query(order_id, |cfd| Ok(cfd.contract_symbol()))
.await?;

let latest_quote = *self
.price_feed_actor
.send(xtra_bitmex_price_feed::GetLatestQuotes)
.await
.context("Price feed not available")?
.get(&into_price_feed_symbol(contract_symbol))
.context("No quote available")?;

let quote_timestamp = latest_quote
.timestamp
.format(&time::format_description::well_known::Rfc3339)
.context("Failed to format timestamp")?;

let threshold = QUOTE_INTERVAL_MINUTES.minutes() * 2;

if latest_quote.is_older_than(threshold) {
bail!(
"Latest quote is older than {} minutes. Refusing to settle with old price.",
threshold.whole_minutes()
)
}

self.cfd_actor
.send(taker_cfd::ProposeSettlement {
order_id,
bid: Price::new(latest_quote.bid())?,
ask: Price::new(latest_quote.ask())?,
quote_timestamp,
})
.send(taker_cfd::ProposeSettlement { order_id })
.await?
}

Expand Down Expand Up @@ -482,13 +446,6 @@ pub fn version() -> String {
VERSION.to_string()
}

fn into_price_feed_symbol(symbol: model::ContractSymbol) -> xtra_bitmex_price_feed::ContractSymbol {
match symbol {
model::ContractSymbol::BtcUsd => xtra_bitmex_price_feed::ContractSymbol::BtcUsd,
model::ContractSymbol::EthUsd => xtra_bitmex_price_feed::ContractSymbol::EthUsd,
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
47 changes: 32 additions & 15 deletions daemon/src/taker_cfd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,13 @@ use anyhow::Context;
use anyhow::Result;
use async_trait::async_trait;
use model::libp2p::PeerId;
use model::market_closing_price;
use model::Cfd;
use model::Contracts;
use model::Identity;
use model::Leverage;
use model::OfferId;
use model::OrderId;
use model::Price;
use model::Role;
use model::{ContractSymbol, Position};
use sqlite_db;
use std::collections::HashMap;
use time::OffsetDateTime;
Expand All @@ -29,12 +27,9 @@ pub struct PlaceOrder {
pub leverage: Leverage,
}

#[derive(Clone)]
#[derive(Clone, Copy)]
pub struct ProposeSettlement {
pub order_id: OrderId,
pub bid: Price,
pub ask: Price,
pub quote_timestamp: String,
}

pub struct Actor {
Expand All @@ -43,6 +38,7 @@ pub struct Actor {
collab_settlement_actor: xtra::Address<collab_settlement::taker::Actor>,
order_actor: xtra::Address<order::taker::Actor>,
offers: Offers,
latest_offers: HashMap<(ContractSymbol, Position), model::Offer>,
maker_identity: Identity,
maker_peer_id: PeerId,
}
Expand All @@ -64,6 +60,7 @@ impl Actor {
offers: Offers::default(),
maker_identity,
maker_peer_id,
latest_offers: HashMap::new(),
}
}
}
Expand All @@ -73,24 +70,44 @@ impl Actor {
async fn handle_latest_offers(&mut self, msg: offer::taker::LatestOffers) {
self.offers.insert(msg.0.clone());

for i in msg.0.iter() {
tracing::info!(
"Received new offer: {:?} with price {}",
i.position_maker.counter_position(),
i.price
);
self.latest_offers.insert(
(i.contract_symbol, i.position_maker.counter_position()),
i.clone(),
);
}

if let Err(e) = self.projection_actor.send(projection::Update(msg.0)).await {
tracing::warn!("Failed to send current offers to projection actor: {e:#}");
};
}

async fn handle_propose_settlement(&mut self, msg: ProposeSettlement) -> Result<()> {
let ProposeSettlement {
order_id,
bid,
ask,
quote_timestamp,
} = msg;
let ProposeSettlement { order_id } = msg;

let cfd = self.db.load_open_cfd::<Cfd>(order_id, ()).await?;

let proposal_closing_price = market_closing_price(bid, ask, Role::Taker, cfd.position());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ We should remove the function market_closing_price and also use the maker's price in the projection - otherwise what we show in the UI does not match what is actually used!

let offer = self
.latest_offers
.get(&(cfd.contract_symbol(), cfd.position().counter_position()))
.context("Cannot propose settlement without price")?;

if !offer.is_safe_to_take(OffsetDateTime::now_utc()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the frequent protocol failures the user might see some annoyance of "outdated offer" here when triggering collab settlement. This is unrelated to this fix, nothing to change here, but I wanted to point it out :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timeout is currently set to 10 minutes . That's plenty of time to hopefully get an offer :D

Copy link
Contributor

@da-kami da-kami Nov 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But that would mean that the only thing that protects us from using an outdated price is the automation's logic of "what is acceptable" as price. Is this fixing the problem of using an outdated price? Is the problem not that the price in the automation is potentially not up to date?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should be fine. The taker simply proposes the latest price what he got from the maker in here. The logic whether to accept or decline this price is outside of this scope.
We simply spare the roundtrip and potentially protect the user by sending a settlement request with an outdated price.

bail!("The maker's offer appears to be outdated, cannot close position");
}

let proposal_closing_price = offer.price;
let offer_timestamp = offer
.creation_timestamp_maker
.format()
.context("Failed to format timestamp")?;

tracing::debug!(%order_id, %proposal_closing_price, %bid, %ask, %quote_timestamp, "Proposing settlement of contract");
tracing::debug!(%order_id, %proposal_closing_price, %offer_timestamp, "Proposing settlement of contract");

// Wait for the response to check for invariants (ie. whether it is possible to settle)
self.collab_settlement_actor
Expand Down
7 changes: 7 additions & 0 deletions model/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,13 @@ impl Timestamp {
let out = self.0.try_into().context("Unable to convert i64 to u64")?;
Ok(out)
}

pub fn format(&self) -> Result<String> {
let formatted = OffsetDateTime::from_unix_timestamp(self.0)
.context("Could not parse timestamp")?
.format(&time::format_description::well_known::Rfc3339)?;
Ok(formatted)
}
}

/// Funding rate per SETTLEMENT_INTERVAL
Expand Down