Skip to content

Commit

Permalink
Merge #300
Browse files Browse the repository at this point in the history
300: Amend model to provide the prices from the orderbook r=klochowicz a=klochowicz

The orderbook can query the orders to provide the "best" orders for a given contract symbol.

I had to deal with the situation when there's no price (either bid or ask).

For the simplicity of wiring things together, I'm only piping through the BTCUSD
    best prices into Flutter for now.

PR for Flutter is incoming soon (excluded from this commit, as I'm still working on that part) - in the meantime it would be good to get some feedback on this backend code.

There was a bit of complexity that I had to tackle because:
- there might be no orders for particular direction (which means there's no bid or ask price from orderbook),
- initial misunderstandings on how orderbooks work (thanks `@da-kami` for clarification on that).

Co-authored-by: Mariusz Klochowicz <mariusz@klochowicz.com>
  • Loading branch information
bors[bot] and klochowicz authored Mar 22, 2023
2 parents e5f138d + 7249f3c commit 5daedc8
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 10 deletions.
23 changes: 14 additions & 9 deletions crates/orderbook-commons/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
mod price;

pub use crate::price::best_current_price;
pub use crate::price::Price;
pub use crate::price::Prices;
use rust_decimal::prelude::ToPrimitive;
use rust_decimal::Decimal;
use secp256k1::Message;
use secp256k1::PublicKey;
Expand Down Expand Up @@ -195,6 +201,11 @@ mod test {
use std::str::FromStr;
use time::OffsetDateTime;

fn dummy_public_key() -> PublicKey {
PublicKey::from_str("02bd998ebd176715fe92b7467cf6b1df8023950a4dd911db4c94dfc89cc9f5a655")
.unwrap()
}

#[test]
fn test_serialize_signature() {
let secret_key = SecretKey::from_slice(&[
Expand All @@ -221,7 +232,7 @@ mod test {
let serialized: Signature = serde_json::from_str(sig).unwrap();

let signature = Signature {
pubkey: PublicKey::from_str("02bd998ebd176715fe92b7467cf6b1df8023950a4dd911db4c94dfc89cc9f5a655").unwrap(),
pubkey: dummy_public_key(),
signature: "3045022100ddd8e15dea994a3dd98c481d901fb46b7f3624bb25b4210ea10f8a00779c6f0e0220222235da47b1ba293184fa4a91b39999911c08020e069c9f4afa2d81586b23e1".parse().unwrap(),
};

Expand All @@ -245,19 +256,13 @@ mod test {
Match {
order_id: Default::default(),
quantity: match_0_quantity,
pubkey: PublicKey::from_str(
"02bd998ebd176715fe92b7467cf6b1df8023950a4dd911db4c94dfc89cc9f5a655",
)
.unwrap(),
pubkey: dummy_public_key(),
execution_price: match_0_price,
},
Match {
order_id: Default::default(),
quantity: match_1_quantity,
pubkey: PublicKey::from_str(
"02bd998ebd176715fe92b7467cf6b1df8023950a4dd911db4c94dfc89cc9f5a655",
)
.unwrap(),
pubkey: dummy_public_key(),
execution_price: match_1_price,
},
],
Expand Down
137 changes: 137 additions & 0 deletions crates/orderbook-commons/src/price.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
use crate::Order;
use crate::ToPrimitive;
use rust_decimal::Decimal;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use trade::ContractSymbol;
use trade::Direction;

#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)]
pub struct Price {
pub bid: Option<Decimal>,
pub ask: Option<Decimal>,
}

pub type Prices = HashMap<ContractSymbol, Price>;

/// Best prices across all current orders for given ContractSymbol in the orderbook
/// Taken orders are not included in the average
pub fn best_current_price(current_orders: &[Order]) -> Prices {
let mut prices = HashMap::new();
let mut add_price_for_symbol = |symbol| {
prices.insert(
symbol,
Price {
bid: best_bid_price(current_orders, symbol),
ask: best_ask_price(current_orders, symbol),
},
);
};
add_price_for_symbol(ContractSymbol::BtcUsd);
prices
}

/// Best price (highest) of all long (buy) orders in the orderbook
fn best_bid_price(current_orders: &[Order], symbol: ContractSymbol) -> Option<Decimal> {
best_price_for(current_orders, Direction::Long, symbol)
}

/// Best price (lowest) of all short (sell) orders in the orderbook
fn best_ask_price(current_orders: &[Order], symbol: ContractSymbol) -> Option<Decimal> {
best_price_for(current_orders, Direction::Short, symbol)
}

fn best_price_for(
current_orders: &[Order],
direction: Direction,
symbol: ContractSymbol,
) -> Option<Decimal> {
assert_eq!(
symbol,
ContractSymbol::BtcUsd,
"only btcusd supported for now"
);
let use_max = direction == Direction::Long;
current_orders
.iter()
.filter(|order| !order.taken && order.direction == direction)
.map(|order| order.price.to_f64().expect("to represent decimal as f64"))
// get the best price
.fold(None, |acc, x| match acc {
Some(y) => Some(if use_max { x.max(y) } else { x.min(y) }),
None => Some(x),
})?
.try_into()
.ok()
}

#[cfg(test)]
mod test {
use crate::price::best_ask_price;
use crate::price::best_bid_price;
use crate::Order;
use crate::OrderType;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use secp256k1::PublicKey;
use std::str::FromStr;
use time::OffsetDateTime;
use trade::ContractSymbol;
use trade::Direction;
use uuid::Uuid;
use ContractSymbol::BtcUsd;

fn dummy_public_key() -> PublicKey {
PublicKey::from_str("02bd998ebd176715fe92b7467cf6b1df8023950a4dd911db4c94dfc89cc9f5a655")
.unwrap()
}

fn dummy_order(price: Decimal, direction: Direction, taken: bool) -> Order {
Order {
id: Uuid::new_v4(),
price,
trader_id: dummy_public_key(),
taken,
direction,
quantity: 100.into(),
order_type: OrderType::Market,
timestamp: OffsetDateTime::now_utc(),
}
}

#[test]
fn test_best_bid_price() {
let current_orders = vec![
dummy_order(dec!(10_000), Direction::Long, false),
dummy_order(dec!(30_000), Direction::Long, false),
dummy_order(dec!(500_000), Direction::Long, true), // taken
dummy_order(dec!(50_000), Direction::Short, false), // wrong direction
];
assert_eq!(best_bid_price(&current_orders, BtcUsd), Some(dec!(30_000)));
}

#[test]
fn test_best_ask_price() {
let current_orders = vec![
dummy_order(dec!(10_000), Direction::Short, false),
dummy_order(dec!(30_000), Direction::Short, false),
// ignored in the calculations - this order is taken
dummy_order(dec!(5_000), Direction::Short, true),
// ignored in the calculations - it's the bid price
dummy_order(dec!(50_000), Direction::Long, false),
];
assert_eq!(best_ask_price(&current_orders, BtcUsd), Some(dec!(10_000)));
}

#[test]
fn test_no_price() {
let all_orders_taken = vec![
dummy_order(dec!(10_000), Direction::Short, true),
dummy_order(dec!(30_000), Direction::Long, true),
];

assert_eq!(best_ask_price(&all_orders_taken, BtcUsd), None);
assert_eq!(best_bid_price(&all_orders_taken, BtcUsd), None);
}
}
2 changes: 1 addition & 1 deletion crates/trade/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::str::FromStr;

pub mod cfd;

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum ContractSymbol {
BtcUsd,
}
Expand Down
35 changes: 35 additions & 0 deletions mobile/native/src/event/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::trade::position::api::Position;
use core::convert::From;
use flutter_rust_bridge::frb;
use flutter_rust_bridge::StreamSink;
use rust_decimal::prelude::ToPrimitive;
use trade::ContractSymbol;

#[frb]
Expand All @@ -18,6 +19,7 @@ pub enum Event {
WalletInfoUpdateNotification(WalletInfo),
PositionUpdateNotification(Position),
PositionClosedNotification(PositionClosed),
PriceUpdateNotification(BestPrice),
}

impl From<EventInternal> for Event {
Expand All @@ -40,6 +42,14 @@ impl From<EventInternal> for Event {
EventInternal::PositionCloseNotification(contract_symbol) => {
Event::PositionClosedNotification(PositionClosed { contract_symbol })
}
EventInternal::PriceUpdateNotification(prices) => {
let best_price = prices
.get(&ContractSymbol::BtcUsd)
.cloned()
.unwrap_or_default()
.into();
Event::PriceUpdateNotification(best_price)
}
}
}
}
Expand Down Expand Up @@ -72,6 +82,7 @@ impl Subscriber for FlutterSubscriber {
EventType::OrderUpdateNotification,
EventType::PositionUpdateNotification,
EventType::PositionClosedNotification,
EventType::PriceUpdateNotification,
]
}
}
Expand All @@ -81,3 +92,27 @@ impl FlutterSubscriber {
FlutterSubscriber { stream }
}
}

/// The best bid and ask price for a contract.
///
/// Best prices come from an orderbook. Contrary to the `Price` struct, we can have no price
/// available, due to no orders in the orderbook.
#[frb]
#[derive(Clone, Debug, Default)]
pub struct BestPrice {
pub bid: Option<f64>,
pub ask: Option<f64>,
}

impl From<orderbook_commons::Price> for BestPrice {
fn from(value: orderbook_commons::Price) -> Self {
BestPrice {
bid: value
.bid
.map(|bid| bid.to_f64().expect("price bid to fit into f64")),
ask: value
.ask
.map(|ask| ask.to_f64().expect("price ask to fit into f64")),
}
}
}
4 changes: 4 additions & 0 deletions mobile/native/src/event/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod subscriber;

use crate::api::WalletInfo;
use coordinator_commons::TradeParams;
use orderbook_commons::Prices;
use std::hash::Hash;
use trade::ContractSymbol;

Expand All @@ -29,6 +30,7 @@ pub enum EventInternal {
OrderFilledWith(Box<TradeParams>),
PositionUpdateNotification(Position),
PositionCloseNotification(ContractSymbol),
PriceUpdateNotification(Prices),
}

impl From<EventInternal> for EventType {
Expand All @@ -43,6 +45,7 @@ impl From<EventInternal> for EventType {
EventInternal::OrderFilledWith(_) => EventType::OrderFilledWith,
EventInternal::PositionUpdateNotification(_) => EventType::PositionUpdateNotification,
EventInternal::PositionCloseNotification(_) => EventType::PositionClosedNotification,
EventInternal::PriceUpdateNotification(_) => EventType::PriceUpdateNotification,
}
}
}
Expand All @@ -56,4 +59,5 @@ pub enum EventType {
OrderFilledWith,
PositionUpdateNotification,
PositionClosedNotification,
PriceUpdateNotification,
}
55 changes: 55 additions & 0 deletions mobile/native/src/orderbook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use anyhow::Result;
use bdk::bitcoin::secp256k1::SecretKey;
use bdk::bitcoin::secp256k1::SECP256K1;
use futures::TryStreamExt;
use orderbook_commons::best_current_price;
use orderbook_commons::OrderbookMsg;
use orderbook_commons::Signature;
use state::Storage;
Expand Down Expand Up @@ -38,6 +39,9 @@ pub fn subscribe(secret_key: SecretKey) -> Result<()> {
Signature { pubkey, signature }
};

// Consider using a HashMap instead to optimize the lookup for removal/update
let mut orders = Vec::new();

loop {
let mut stream =
orderbook_client::subscribe_with_authentication(url.clone(), &authenticate);
Expand Down Expand Up @@ -65,6 +69,57 @@ pub fn subscribe(secret_key: SecretKey) -> Result<()> {
tracing::error!("Trade request sent to coordinator failed. Error: {e:#}");
}
},
OrderbookMsg::AllOrders(initial_orders) => {
if !orders.is_empty() {
tracing::warn!("Received all orders from orderbook, but we already have some orders. This should not happen");
}
else {
tracing::debug!(?orders, "Received all orders from orderbook");
}
orders = initial_orders;
if let Err(e) = position::handler::price_update(best_current_price(&orders)) {
tracing::error!("Price update from the orderbook failed. Error: {e:#}");
}
},
OrderbookMsg::NewOrder(order) => {
orders.push(order);
if let Err(e) = position::handler::price_update(best_current_price(&orders)) {
tracing::error!("Price update from the orderbook failed. Error: {e:#}");
}
}
OrderbookMsg::DeleteOrder(order_id) => {
let mut found = false;
for (index, element) in orders.iter().enumerate() {
if element.id == order_id {
found = true;
orders.remove(index);
break;
}
}
if !found {
tracing::warn!(%order_id, "Could not remove non-existing order");
}
if let Err(e) = position::handler::price_update(best_current_price(&orders)) {
tracing::error!("Price update from the orderbook failed. Error: {e:#}");
}
},
OrderbookMsg::Update(updated_order) => {
let mut found = false;
for (index, element) in orders.iter().enumerate() {
if element.id == updated_order.id {
orders.remove(index);
found = true;
break;
}
}
if !found {
tracing::warn!(?updated_order, "Update without prior knowledge of order");
}
orders.push(updated_order);
if let Err(e) = position::handler::price_update(best_current_price(&orders)) {
tracing::error!("Price update from the orderbook failed. Error: {e:#}");
}
},
_ => tracing::debug!(?msg, "Skipping message from orderbook"),
}
}
Expand Down
6 changes: 6 additions & 0 deletions mobile/native/src/trade/position/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use anyhow::Context;
use anyhow::Result;
use coordinator_commons::TradeParams;
use orderbook_commons::FilledWith;
use orderbook_commons::Prices;
use rust_decimal::prelude::ToPrimitive;
use trade::ContractSymbol;
use trade::Direction;
Expand Down Expand Up @@ -116,3 +117,8 @@ pub fn update_position_after_order_filled(filled_order: Order, collateral: u64)

Ok(())
}

pub fn price_update(prices: Prices) -> Result<()> {
event::publish(&EventInternal::PriceUpdateNotification(prices));
Ok(())
}

0 comments on commit 5daedc8

Please sign in to comment.