You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
107 lines
3.2 KiB
107 lines
3.2 KiB
use crate::model::Usd;
|
|
use anyhow::Result;
|
|
use futures::{StreamExt, TryStreamExt};
|
|
use rust_decimal::Decimal;
|
|
use std::convert::TryFrom;
|
|
use std::future::Future;
|
|
use std::time::SystemTime;
|
|
use tokio::sync::watch;
|
|
use tokio_tungstenite::tungstenite;
|
|
|
|
/// Connects to the BitMex price feed, returning the polling task and a watch channel that will
|
|
/// always hold the last quote.
|
|
pub async fn new() -> Result<(impl Future<Output = ()>, watch::Receiver<Quote>)> {
|
|
let (connection, _) = tokio_tungstenite::connect_async(
|
|
"wss://www.bitmex.com/realtime?subscribe=quoteBin1m:XBTUSD",
|
|
)
|
|
.await?;
|
|
let mut quotes = connection
|
|
.inspect_err(|e| tracing::warn!("Error on websocket stream: {}", e))
|
|
.map(|msg| Quote::from_message(msg?))
|
|
.filter_map(|result| async move { result.transpose() })
|
|
.boxed()
|
|
.fuse();
|
|
|
|
tracing::info!("Connected to BitMex realtime API");
|
|
|
|
let first_quote = quotes.select_next_some().await?;
|
|
let (sender, receiver) = watch::channel(first_quote);
|
|
|
|
let task = async move {
|
|
while let Ok(Some(quote)) = quotes.try_next().await {
|
|
if sender.send(quote).is_err() {
|
|
break; // If the receiver dies, we can exit the loop.
|
|
}
|
|
}
|
|
|
|
tracing::warn!("Failed to read quote from websocket");
|
|
};
|
|
|
|
Ok((task, receiver))
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct Quote {
|
|
pub timestamp: SystemTime,
|
|
pub bid: Usd,
|
|
pub ask: Usd,
|
|
}
|
|
|
|
impl Quote {
|
|
fn from_message(message: tungstenite::Message) -> Result<Option<Self>> {
|
|
let text_message = match message {
|
|
tungstenite::Message::Text(text_message) => text_message,
|
|
_ => anyhow::bail!("Bad message type, only text is supported"),
|
|
};
|
|
|
|
let table_message = match serde_json::from_str::<wire::TableMessage>(&text_message) {
|
|
Ok(table_message) => table_message,
|
|
Err(_) => return Ok(None),
|
|
};
|
|
|
|
let [quote] = table_message.data;
|
|
|
|
Ok(Some(Self {
|
|
timestamp: SystemTime::now(),
|
|
bid: Usd::from(Decimal::try_from(quote.bid_price)?),
|
|
ask: Usd::from(Decimal::try_from(quote.ask_price)?),
|
|
}))
|
|
}
|
|
}
|
|
|
|
mod wire {
|
|
use serde::Deserialize;
|
|
|
|
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
|
pub struct TableMessage {
|
|
pub table: String,
|
|
// we always just expect a single quote, hence the use of an array instead of a vec
|
|
pub data: [QuoteData; 1],
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct QuoteData {
|
|
pub bid_size: u64,
|
|
pub ask_size: u64,
|
|
pub bid_price: f64,
|
|
pub ask_price: f64,
|
|
pub symbol: String,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use rust_decimal_macros::dec;
|
|
|
|
#[test]
|
|
fn can_deserialize_quote_message() {
|
|
let message = tungstenite::Message::Text(r#"{"table":"quoteBin1m","action":"insert","data":[{"timestamp":"2021-09-21T02:40:00.000Z","symbol":"XBTUSD","bidSize":50200,"bidPrice":42640.5,"askPrice":42641,"askSize":363600}]}"#.to_owned());
|
|
|
|
let quote = Quote::from_message(message).unwrap().unwrap();
|
|
|
|
assert_eq!(quote.bid, Usd(dec!(42640.5)));
|
|
assert_eq!(quote.ask, Usd(dec!(42641)));
|
|
}
|
|
}
|
|
|