@ -1,44 +1,102 @@
use crate ::model ::{ Price , Timestamp } ;
use crate ::projection ;
use crate ::{ projection , Tasks } ;
use anyhow ::Result ;
use futures ::{ StreamExt , TryStreamExt } ;
use rust_decimal ::Decimal ;
use std ::convert ::TryFrom ;
use std ::future ::Future ;
use tokio_tungstenite ::tungstenite ;
use xtra ::prelude ::MessageChannel ;
use xtra_productivity ::xtra_productivity ;
/// 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 (
msg_channel : impl MessageChannel < projection ::Update < Quote > > ,
) -> Result < ( impl Future < Output = ( ) > , 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 task = async move {
while let Ok ( Some ( quote ) ) = quotes . try_next ( ) . await {
if msg_channel . send ( projection ::Update ( quote ) ) . await . is_err ( ) {
break ; // If the receiver dies, we can exit the loop.
const URL : & str = "wss://www.bitmex.com/realtime?subscribe=quoteBin1m:XBTUSD" ;
pub struct Actor {
tasks : Tasks ,
receiver : Box < dyn MessageChannel < projection ::Update < Quote > > > ,
}
impl Actor {
pub fn new ( receiver : impl MessageChannel < projection ::Update < Quote > > + 'static ) -> Self {
Self {
tasks : Tasks ::default ( ) ,
receiver : Box ::new ( receiver ) ,
}
}
}
impl xtra ::Actor for Actor { }
#[ xtra_productivity ]
impl Actor {
async fn handle ( & mut self , msg : NotifyNoConnection , ctx : & mut xtra ::Context < Self > ) {
match msg {
NotifyNoConnection ::Failed { error } = > {
tracing ::warn ! ( "Connection to BitMex realtime API failed: {:#}" , error )
}
NotifyNoConnection ::StreamEnded = > {
tracing ::warn ! ( "Connection to BitMex realtime API closed" )
}
}
tracing ::warn ! ( "Failed to read quote from websocket" ) ;
} ;
let this = ctx . address ( ) . expect ( "we are alive" ) ;
self . tasks . add ( connect_until_successful ( this ) ) ;
}
Ok ( ( task , first_quote ) )
async fn handle ( & mut self , _ : Connect , ctx : & mut xtra ::Context < Self > ) -> Result < Quote > {
tracing ::debug ! ( "Connecting to BitMex realtime API" ) ;
let ( connection , _ ) = tokio_tungstenite ::connect_async ( URL ) . await ? ;
let mut quotes = connection
. map ( | msg | Quote ::from_message ( msg ? ) )
. filter_map ( | result | async move { result . transpose ( ) } )
. boxed ( )
. fuse ( ) ;
tracing ::info ! ( "Connected to BitMex realtime API" ) ;
let initial_quote = quotes . select_next_some ( ) . await ? ;
let this = ctx . address ( ) . expect ( "we are alive" ) ;
self . tasks . add ( {
let receiver = self . receiver . clone_channel ( ) ;
async move {
let no_connection = loop {
match quotes . try_next ( ) . await {
Ok ( Some ( quote ) ) = > {
if receiver . send ( projection ::Update ( quote ) ) . await . is_err ( ) {
return ; // if the receiver dies, our job is done
}
}
Ok ( None ) = > break NotifyNoConnection ::StreamEnded ,
Err ( e ) = > break NotifyNoConnection ::Failed { error : e } ,
}
} ;
let _ = this . send ( no_connection ) . await ;
}
} ) ;
Ok ( initial_quote )
}
}
async fn connect_until_successful ( this : xtra ::Address < Actor > ) {
while let Err ( e ) = this
. send ( Connect )
. await
. expect ( "always connected to ourselves" )
{
tracing ::warn ! ( "Failed to connect to BitMex realtime API: {:#}" , e ) ;
}
}
pub struct Connect ;
enum NotifyNoConnection {
Failed { error : anyhow ::Error } ,
StreamEnded ,
}
#[ derive(Clone, Debug) ]
@ -57,7 +115,10 @@ impl Quote {
let table_message = match serde_json ::from_str ::< wire ::TableMessage > ( & text_message ) {
Ok ( table_message ) = > table_message ,
Err ( _ ) = > return Ok ( None ) ,
Err ( _ ) = > {
tracing ::trace ! ( % text_message , "Not a 'table' message, skipping..." ) ;
return Ok ( None ) ;
}
} ;
let [ quote ] = table_message . data ;