use crate::{
AccountEventKind, UnindexedAccountEvent, UnindexedAccountSnapshot,
balance::AssetBalance,
client::{ExecutionClient, OrderStatusClient},
error::{
ConnectivityError, OrderError, StreamTerminationReason, UnindexedClientError,
UnindexedOrderError,
},
exchange::mock::request::MockExchangeRequest,
fee::{FeeAssetConfig, FeeModelConfig, SpotFeeAsset},
fill::{MarketOrderTiming, SimFillConfig, StaleMarketPolicy},
margin::SpotMarginConfig,
order::{
Order, OrderKey, UnindexedInactiveOrder, UnindexedOrderKey,
request::{OrderRequestCancel, OrderRequestOpen, UnindexedOrderResponseCancel},
state::{Open, OrderState, UnindexedOrderState},
},
slippage::SlippageConfig,
trade::TradesRead,
};
use chrono::{DateTime, Utc};
use derive_more::Constructor;
use futures::{StreamExt, stream::BoxStream};
use rustrade_instrument::{
asset::name::AssetNameExchange,
exchange::ExchangeId,
instrument::{kind::InstrumentKindDiscriminant, name::InstrumentNameExchange},
};
use serde::{Deserialize, Serialize};
use tokio::sync::{broadcast, mpsc, oneshot};
use tokio_stream::wrappers::BroadcastStream;
use tracing::error;
#[derive(
Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Constructor,
)]
pub struct MockExecutionConfig {
pub mocked_exchange: ExchangeId,
pub initial_state: UnindexedAccountSnapshot,
pub latency_ms: u64,
#[serde(default)]
pub fee_model: FeeModelConfig,
#[serde(default)]
pub fill_model: SimFillConfig,
#[serde(default)]
pub spot_fee_asset: SpotFeeAsset,
#[serde(default)]
pub slippage: SlippageConfig,
#[serde(default)]
pub market_order_timing: MarketOrderTiming,
#[serde(default)]
pub stale_market: StaleMarketPolicy,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub spot_margin: Option<SpotMarginConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fee_asset: Option<FeeAssetConfig>,
}
#[derive(Debug, Constructor)]
pub struct MockExecutionClientConfig<FnTime> {
pub mocked_exchange: ExchangeId,
pub clock: FnTime,
pub request_tx: mpsc::UnboundedSender<MockExchangeRequest>,
pub event_rx: broadcast::Receiver<UnindexedAccountEvent>,
}
impl<FnTime> Clone for MockExecutionClientConfig<FnTime>
where
FnTime: Clone,
{
fn clone(&self) -> Self {
Self {
mocked_exchange: self.mocked_exchange,
clock: self.clock.clone(),
request_tx: self.request_tx.clone(),
event_rx: self.event_rx.resubscribe(),
}
}
}
#[derive(Debug, Constructor)]
pub struct MockExecution<FnTime> {
pub mocked_exchange: ExchangeId,
pub clock: FnTime,
pub request_tx: mpsc::UnboundedSender<MockExchangeRequest>,
pub event_rx: broadcast::Receiver<UnindexedAccountEvent>,
}
impl<FnTime> Clone for MockExecution<FnTime>
where
FnTime: Clone,
{
fn clone(&self) -> Self {
Self {
mocked_exchange: self.mocked_exchange,
clock: self.clock.clone(),
request_tx: self.request_tx.clone(),
event_rx: self.event_rx.resubscribe(),
}
}
}
impl<FnTime> MockExecution<FnTime>
where
FnTime: Fn() -> DateTime<Utc>,
{
pub fn time_request(&self) -> DateTime<Utc> {
(self.clock)()
}
}
impl<FnTime> ExecutionClient for MockExecution<FnTime>
where
FnTime: Fn() -> DateTime<Utc> + Clone + Send + Sync,
{
const EXCHANGE: ExchangeId = ExchangeId::Mock;
const SUPPORTED_KINDS: &'static [InstrumentKindDiscriminant] = &[
InstrumentKindDiscriminant::Spot,
InstrumentKindDiscriminant::Cfd,
];
type Config = MockExecutionClientConfig<FnTime>;
type AccountStream = BoxStream<'static, UnindexedAccountEvent>;
fn new(config: Self::Config) -> Self {
Self {
mocked_exchange: config.mocked_exchange,
clock: config.clock,
request_tx: config.request_tx,
event_rx: config.event_rx,
}
}
async fn account_snapshot(
&self,
_: &[AssetNameExchange],
_: &[InstrumentNameExchange],
) -> Result<UnindexedAccountSnapshot, UnindexedClientError> {
let (response_tx, response_rx) = oneshot::channel();
self.request_tx
.send(MockExchangeRequest::fetch_account_snapshot(
self.time_request(),
response_tx,
))
.map_err(|_| {
UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
self.mocked_exchange,
))
})?;
response_rx.await.map_err(|_| {
UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
self.mocked_exchange,
))
})
}
async fn account_stream(
&self,
_: &[AssetNameExchange],
_: &[InstrumentNameExchange],
) -> Result<Self::AccountStream, UnindexedClientError> {
let exchange = self.mocked_exchange;
let events = BroadcastStream::new(self.event_rx.resubscribe()).map(move |result| {
result.unwrap_or_else(|error| {
error!(
?error,
"MockExchange Broadcast AccountStream lagged - terminating"
);
UnindexedAccountEvent::stream_terminated(
exchange,
StreamTerminationReason::Error("mock broadcast stream lagged".to_string()),
)
})
});
Ok(
futures::stream::unfold((events, false), |(mut events, ended)| async move {
if ended {
return None;
}
let event = events.next().await?;
let ended = matches!(event.kind, AccountEventKind::StreamTerminated(_));
Some((event, (events, ended)))
})
.fuse()
.boxed(),
)
}
async fn cancel_order(
&self,
request: OrderRequestCancel<ExchangeId, &InstrumentNameExchange>,
) -> UnindexedOrderResponseCancel {
let (response_tx, response_rx) = oneshot::channel();
let key = OrderKey {
exchange: request.key.exchange,
instrument: request.key.instrument.clone(),
strategy: request.key.strategy.clone(),
cid: request.key.cid.clone(),
};
if self
.request_tx
.send(MockExchangeRequest::cancel_order(
self.time_request(),
response_tx,
request.into_owned_instrument(),
))
.is_err()
{
return UnindexedOrderResponseCancel {
key,
state: Err(UnindexedOrderError::Connectivity(
ConnectivityError::ExchangeOffline(self.mocked_exchange),
)),
};
}
match response_rx.await {
Ok(response) => response,
Err(_) => UnindexedOrderResponseCancel {
key,
state: Err(UnindexedOrderError::Connectivity(
ConnectivityError::ExchangeOffline(self.mocked_exchange),
)),
},
}
}
async fn open_order(
&self,
request: OrderRequestOpen<ExchangeId, &InstrumentNameExchange>,
) -> Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState> {
let (response_tx, response_rx) = oneshot::channel();
let request = request.into_owned_instrument();
if self
.request_tx
.send(MockExchangeRequest::open_order(
self.time_request(),
response_tx,
request.clone(),
))
.is_err()
{
return Order {
key: request.key,
side: request.state.side,
price: request.state.price,
quantity: request.state.quantity,
kind: request.state.kind,
time_in_force: request.state.time_in_force,
state: OrderState::inactive(OrderError::Connectivity(
ConnectivityError::ExchangeOffline(self.mocked_exchange),
)),
};
}
match response_rx.await {
Ok(response) => response,
Err(_) => Order {
key: request.key,
side: request.state.side,
price: request.state.price,
quantity: request.state.quantity,
kind: request.state.kind,
time_in_force: request.state.time_in_force,
state: OrderState::inactive(OrderError::Connectivity(
ConnectivityError::ExchangeOffline(self.mocked_exchange),
)),
},
}
}
async fn fetch_balances(
&self,
assets: &[AssetNameExchange],
) -> Result<Vec<AssetBalance<AssetNameExchange>>, UnindexedClientError> {
let (response_tx, response_rx) = oneshot::channel();
self.request_tx
.send(MockExchangeRequest::fetch_balances(
self.time_request(),
assets.to_vec(),
response_tx,
))
.map_err(|_| {
UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
self.mocked_exchange,
))
})?;
response_rx.await.map_err(|_| {
UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
self.mocked_exchange,
))
})
}
async fn fetch_open_orders(
&self,
instruments: &[InstrumentNameExchange],
) -> Result<Vec<Order<ExchangeId, InstrumentNameExchange, Open>>, UnindexedClientError> {
let (response_tx, response_rx) = oneshot::channel();
self.request_tx
.send(MockExchangeRequest::fetch_orders_open(
self.time_request(),
instruments.to_vec(),
response_tx,
))
.map_err(|_| {
UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
self.mocked_exchange,
))
})?;
response_rx.await.map_err(|_| {
UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
self.mocked_exchange,
))
})
}
async fn fetch_trades(
&self,
start: DateTime<Utc>,
end: DateTime<Utc>,
instruments: &[InstrumentNameExchange],
) -> Result<TradesRead<AssetNameExchange, InstrumentNameExchange>, UnindexedClientError> {
let (response_tx, response_rx) = oneshot::channel();
self.request_tx
.send(MockExchangeRequest::fetch_trades(
self.time_request(),
response_tx,
start,
end,
))
.map_err(|_| {
UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
self.mocked_exchange,
))
})?;
let mut trades = response_rx.await.map_err(|_| {
UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
self.mocked_exchange,
))
})?;
if !instruments.is_empty() {
trades.retain(|trade| instruments.contains(&trade.instrument));
}
Ok(TradesRead::complete(trades))
}
}
impl<FnTime> OrderStatusClient for MockExecution<FnTime>
where
FnTime: Fn() -> DateTime<Utc> + Clone + Send + Sync,
{
async fn fetch_ended_orders(
&self,
orders: &[UnindexedOrderKey],
) -> Result<Vec<UnindexedInactiveOrder>, UnindexedClientError> {
let (response_tx, response_rx) = oneshot::channel();
self.request_tx
.send(MockExchangeRequest::fetch_orders_ended(
self.time_request(),
orders.to_vec(),
response_tx,
))
.map_err(|_| {
UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
self.mocked_exchange,
))
})?;
response_rx.await.map_err(|_| {
UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
self.mocked_exchange,
))
})
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
use super::*;
use crate::{AccountEventKind, balance::Balance};
use rust_decimal::Decimal;
use rustrade_integration::collection::snapshot::Snapshot;
fn filler_event() -> UnindexedAccountEvent {
UnindexedAccountEvent::new(
ExchangeId::Mock,
AccountEventKind::BalanceSnapshot(Snapshot::new(AssetBalance {
asset: AssetNameExchange::new("btc"),
balance: Balance::new(Decimal::ONE, Decimal::ONE),
time_exchange: Utc::now(),
})),
)
}
#[tokio::test]
async fn fetch_trades_reads_the_span_for_the_requested_instruments() {
use crate::{
order::id::{OrderId, StrategyId},
trade::{AssetFees, Trade, TradeId},
};
use rustrade_instrument::Side;
let (_event_tx, event_rx) = broadcast::channel::<UnindexedAccountEvent>(1);
let (request_tx, mut request_rx) = mpsc::unbounded_channel();
let client = MockExecution::new(ExchangeId::Mock, Utc::now, request_tx, event_rx);
let start = Utc::now() - chrono::TimeDelta::hours(1);
let end = Utc::now();
let trade = move |instrument: &str| {
Trade::new(
TradeId::new(instrument),
OrderId::new("order"),
InstrumentNameExchange::new(instrument),
StrategyId::unknown(),
start,
Side::Buy,
Decimal::ONE,
Decimal::ONE,
None,
AssetFees::new(AssetNameExchange::new("usdt"), Decimal::ZERO, None),
)
};
let exchange = tokio::spawn(async move {
let request = request_rx.recv().await.expect("a request");
let crate::exchange::mock::request::MockExchangeRequestKind::FetchTrades {
response_tx,
start: asked_start,
end: asked_end,
} = request.kind
else {
panic!("expected FetchTrades, got {:?}", request.kind);
};
assert_eq!((asked_start, asked_end), (start, end));
response_tx
.send(vec![trade("btc_usdt"), trade("eth_usdt")])
.expect("the client awaits the answer");
});
let read = client
.fetch_trades(start, end, &[InstrumentNameExchange::new("eth_usdt")])
.await
.unwrap();
exchange.await.unwrap();
assert_eq!(read, TradesRead::complete(vec![trade("eth_usdt")]));
}
#[tokio::test]
async fn account_stream_emits_stream_terminated_on_broadcast_lag() {
let (event_tx, event_rx) = broadcast::channel::<UnindexedAccountEvent>(1);
let (request_tx, _request_rx) = mpsc::unbounded_channel();
let client = MockExecution::new(ExchangeId::Mock, Utc::now, request_tx, event_rx);
let mut stream = client.account_stream(&[], &[]).await.unwrap();
for _ in 0..3 {
event_tx.send(filler_event()).unwrap();
}
let event = stream.next().await.expect("expected a terminal event");
assert!(
matches!(
event.kind,
AccountEventKind::StreamTerminated(StreamTerminationReason::Error(_))
),
"expected StreamTerminated(Error) on broadcast lag, got {:?}",
event.kind
);
assert_eq!(event.exchange, ExchangeId::Mock);
assert!(
stream.next().await.is_none(),
"stream must end after the terminal event"
);
}
#[tokio::test]
async fn account_stream_ends_right_after_a_terminal_event_from_the_venue() {
let (event_tx, event_rx) = broadcast::channel::<UnindexedAccountEvent>(16);
let (request_tx, _request_rx) = mpsc::unbounded_channel();
let client = MockExecution::new(ExchangeId::Mock, Utc::now, request_tx, event_rx);
let mut stream = client.account_stream(&[], &[]).await.unwrap();
event_tx.send(filler_event()).unwrap();
event_tx
.send(UnindexedAccountEvent::stream_terminated(
ExchangeId::Mock,
StreamTerminationReason::Error("market feed lagged".to_string()),
))
.unwrap();
assert!(matches!(
stream.next().await.map(|event| event.kind),
Some(AccountEventKind::BalanceSnapshot(_))
));
assert!(matches!(
stream.next().await.map(|event| event.kind),
Some(AccountEventKind::StreamTerminated(_))
));
assert_eq!(
futures::FutureExt::now_or_never(stream.next()),
Some(None),
"the stream must end on the poll after its terminal event, with the sender still open"
);
assert_eq!(
futures::FutureExt::now_or_never(stream.next()),
Some(None),
"and stay ended however often it is polled"
);
drop(event_tx);
}
}