1use crate::{
2 UnindexedAccountEvent, UnindexedAccountSnapshot,
3 balance::AssetBalance,
4 client::{ExecutionClient, OrderStatusClient},
5 error::{
6 ConnectivityError, OrderError, StreamTerminationReason, UnindexedClientError,
7 UnindexedOrderError,
8 },
9 exchange::mock::request::MockExchangeRequest,
10 fee::{FeeModelConfig, SpotFeeAsset},
11 fill::SimFillConfig,
12 order::{
13 Order, OrderKey, UnindexedInactiveOrder, UnindexedOrderKey,
14 request::{OrderRequestCancel, OrderRequestOpen, UnindexedOrderResponseCancel},
15 state::{Open, OrderState, UnindexedOrderState},
16 },
17 slippage::SlippageConfig,
18 trade::TradesRead,
19};
20use chrono::{DateTime, Utc};
21use derive_more::Constructor;
22use futures::{StreamExt, stream::BoxStream};
23use rustrade_instrument::{
24 asset::name::AssetNameExchange,
25 exchange::ExchangeId,
26 instrument::{kind::InstrumentKindDiscriminant, name::InstrumentNameExchange},
27};
28use serde::{Deserialize, Serialize};
29use tokio::sync::{broadcast, mpsc, oneshot};
30use tokio_stream::wrappers::BroadcastStream;
31use tracing::error;
32
33#[derive(
34 Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Constructor,
35)]
36pub struct MockExecutionConfig {
37 pub mocked_exchange: ExchangeId,
38 pub initial_state: UnindexedAccountSnapshot,
39 pub latency_ms: u64,
40 #[serde(default)]
45 pub fee_model: FeeModelConfig,
46 #[serde(default)]
53 pub fill_model: SimFillConfig,
54 #[serde(default)]
59 pub spot_fee_asset: SpotFeeAsset,
60 #[serde(default)]
67 pub slippage: SlippageConfig,
68}
69
70#[derive(Debug, Constructor)]
71pub struct MockExecutionClientConfig<FnTime> {
72 pub mocked_exchange: ExchangeId,
73 pub clock: FnTime,
74 pub request_tx: mpsc::UnboundedSender<MockExchangeRequest>,
75 pub event_rx: broadcast::Receiver<UnindexedAccountEvent>,
76}
77
78impl<FnTime> Clone for MockExecutionClientConfig<FnTime>
79where
80 FnTime: Clone,
81{
82 fn clone(&self) -> Self {
83 Self {
84 mocked_exchange: self.mocked_exchange,
85 clock: self.clock.clone(),
86 request_tx: self.request_tx.clone(),
87 event_rx: self.event_rx.resubscribe(),
88 }
89 }
90}
91
92#[derive(Debug, Constructor)]
93pub struct MockExecution<FnTime> {
94 pub mocked_exchange: ExchangeId,
95 pub clock: FnTime,
96 pub request_tx: mpsc::UnboundedSender<MockExchangeRequest>,
97 pub event_rx: broadcast::Receiver<UnindexedAccountEvent>,
98}
99
100impl<FnTime> Clone for MockExecution<FnTime>
101where
102 FnTime: Clone,
103{
104 fn clone(&self) -> Self {
105 Self {
106 mocked_exchange: self.mocked_exchange,
107 clock: self.clock.clone(),
108 request_tx: self.request_tx.clone(),
109 event_rx: self.event_rx.resubscribe(),
110 }
111 }
112}
113
114impl<FnTime> MockExecution<FnTime>
115where
116 FnTime: Fn() -> DateTime<Utc>,
117{
118 pub fn time_request(&self) -> DateTime<Utc> {
119 (self.clock)()
120 }
121}
122
123impl<FnTime> ExecutionClient for MockExecution<FnTime>
124where
125 FnTime: Fn() -> DateTime<Utc> + Clone + Send + Sync,
126{
127 const EXCHANGE: ExchangeId = ExchangeId::Mock;
128
129 const SUPPORTED_KINDS: &'static [InstrumentKindDiscriminant] = &[
134 InstrumentKindDiscriminant::Spot,
135 InstrumentKindDiscriminant::Cfd,
136 ];
137 type Config = MockExecutionClientConfig<FnTime>;
138 type AccountStream = BoxStream<'static, UnindexedAccountEvent>;
139
140 fn new(config: Self::Config) -> Self {
141 Self {
142 mocked_exchange: config.mocked_exchange,
143 clock: config.clock,
144 request_tx: config.request_tx,
145 event_rx: config.event_rx,
146 }
147 }
148
149 async fn account_snapshot(
150 &self,
151 _: &[AssetNameExchange],
152 _: &[InstrumentNameExchange],
153 ) -> Result<UnindexedAccountSnapshot, UnindexedClientError> {
154 let (response_tx, response_rx) = oneshot::channel();
155
156 self.request_tx
157 .send(MockExchangeRequest::fetch_account_snapshot(
158 self.time_request(),
159 response_tx,
160 ))
161 .map_err(|_| {
162 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
163 self.mocked_exchange,
164 ))
165 })?;
166
167 response_rx.await.map_err(|_| {
168 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
169 self.mocked_exchange,
170 ))
171 })
172 }
173
174 async fn account_stream(
175 &self,
176 _: &[AssetNameExchange],
177 _: &[InstrumentNameExchange],
178 ) -> Result<Self::AccountStream, UnindexedClientError> {
179 let exchange = self.mocked_exchange;
183 Ok(BroadcastStream::new(self.event_rx.resubscribe())
184 .scan(false, move |done, result| {
185 let item = if *done {
187 None
189 } else {
190 match result {
191 Ok(event) => Some(event),
192 Err(error) => {
193 error!(
194 ?error,
195 "MockExchange Broadcast AccountStream lagged - terminating"
196 );
197 *done = true;
198 Some(UnindexedAccountEvent::stream_terminated(
199 exchange,
200 StreamTerminationReason::Error(
201 "mock broadcast stream lagged".to_string(),
202 ),
203 ))
204 }
205 }
206 };
207 futures::future::ready(item)
208 })
209 .boxed())
210 }
211
212 async fn cancel_order(
213 &self,
214 request: OrderRequestCancel<ExchangeId, &InstrumentNameExchange>,
215 ) -> UnindexedOrderResponseCancel {
216 let (response_tx, response_rx) = oneshot::channel();
217
218 let key = OrderKey {
219 exchange: request.key.exchange,
220 instrument: request.key.instrument.clone(),
221 strategy: request.key.strategy.clone(),
222 cid: request.key.cid.clone(),
223 };
224
225 if self
226 .request_tx
227 .send(MockExchangeRequest::cancel_order(
228 self.time_request(),
229 response_tx,
230 request.into_owned_instrument(),
231 ))
232 .is_err()
233 {
234 return UnindexedOrderResponseCancel {
235 key,
236 state: Err(UnindexedOrderError::Connectivity(
237 ConnectivityError::ExchangeOffline(self.mocked_exchange),
238 )),
239 };
240 }
241
242 match response_rx.await {
243 Ok(response) => response,
244 Err(_) => UnindexedOrderResponseCancel {
245 key,
246 state: Err(UnindexedOrderError::Connectivity(
247 ConnectivityError::ExchangeOffline(self.mocked_exchange),
248 )),
249 },
250 }
251 }
252
253 async fn open_order(
254 &self,
255 request: OrderRequestOpen<ExchangeId, &InstrumentNameExchange>,
256 ) -> Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState> {
257 let (response_tx, response_rx) = oneshot::channel();
258
259 let request = request.into_owned_instrument();
260
261 if self
262 .request_tx
263 .send(MockExchangeRequest::open_order(
264 self.time_request(),
265 response_tx,
266 request.clone(),
271 ))
272 .is_err()
273 {
274 return Order {
275 key: request.key,
276 side: request.state.side,
277 price: request.state.price,
278 quantity: request.state.quantity,
279 kind: request.state.kind,
280 time_in_force: request.state.time_in_force,
281 state: OrderState::inactive(OrderError::Connectivity(
282 ConnectivityError::ExchangeOffline(self.mocked_exchange),
283 )),
284 };
285 }
286
287 match response_rx.await {
288 Ok(response) => response,
289 Err(_) => Order {
290 key: request.key,
291 side: request.state.side,
292 price: request.state.price,
293 quantity: request.state.quantity,
294 kind: request.state.kind,
295 time_in_force: request.state.time_in_force,
296 state: OrderState::inactive(OrderError::Connectivity(
297 ConnectivityError::ExchangeOffline(self.mocked_exchange),
298 )),
299 },
300 }
301 }
302
303 async fn fetch_balances(
304 &self,
305 assets: &[AssetNameExchange],
306 ) -> Result<Vec<AssetBalance<AssetNameExchange>>, UnindexedClientError> {
307 let (response_tx, response_rx) = oneshot::channel();
308
309 self.request_tx
310 .send(MockExchangeRequest::fetch_balances(
311 self.time_request(),
312 assets.to_vec(),
313 response_tx,
314 ))
315 .map_err(|_| {
316 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
317 self.mocked_exchange,
318 ))
319 })?;
320
321 response_rx.await.map_err(|_| {
322 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
323 self.mocked_exchange,
324 ))
325 })
326 }
327
328 async fn fetch_open_orders(
329 &self,
330 instruments: &[InstrumentNameExchange],
331 ) -> Result<Vec<Order<ExchangeId, InstrumentNameExchange, Open>>, UnindexedClientError> {
332 let (response_tx, response_rx) = oneshot::channel();
333
334 self.request_tx
335 .send(MockExchangeRequest::fetch_orders_open(
336 self.time_request(),
337 instruments.to_vec(),
338 response_tx,
339 ))
340 .map_err(|_| {
341 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
342 self.mocked_exchange,
343 ))
344 })?;
345
346 response_rx.await.map_err(|_| {
347 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
348 self.mocked_exchange,
349 ))
350 })
351 }
352
353 async fn fetch_trades(
356 &self,
357 start: DateTime<Utc>,
358 end: DateTime<Utc>,
359 instruments: &[InstrumentNameExchange],
360 ) -> Result<TradesRead<AssetNameExchange, InstrumentNameExchange>, UnindexedClientError> {
361 let (response_tx, response_rx) = oneshot::channel();
362
363 self.request_tx
364 .send(MockExchangeRequest::fetch_trades(
365 self.time_request(),
366 response_tx,
367 start,
368 end,
369 ))
370 .map_err(|_| {
371 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
372 self.mocked_exchange,
373 ))
374 })?;
375
376 let mut trades = response_rx.await.map_err(|_| {
377 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
378 self.mocked_exchange,
379 ))
380 })?;
381 if !instruments.is_empty() {
382 trades.retain(|trade| instruments.contains(&trade.instrument));
383 }
384 Ok(TradesRead::complete(trades))
385 }
386}
387
388impl<FnTime> OrderStatusClient for MockExecution<FnTime>
392where
393 FnTime: Fn() -> DateTime<Utc> + Clone + Send + Sync,
394{
395 async fn fetch_ended_orders(
396 &self,
397 orders: &[UnindexedOrderKey],
398 ) -> Result<Vec<UnindexedInactiveOrder>, UnindexedClientError> {
399 let (response_tx, response_rx) = oneshot::channel();
400
401 self.request_tx
402 .send(MockExchangeRequest::fetch_orders_ended(
403 self.time_request(),
404 orders.to_vec(),
405 response_tx,
406 ))
407 .map_err(|_| {
408 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
409 self.mocked_exchange,
410 ))
411 })?;
412
413 response_rx.await.map_err(|_| {
414 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
415 self.mocked_exchange,
416 ))
417 })
418 }
419}
420
421#[cfg(test)]
422#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
424 use super::*;
425 use crate::{AccountEventKind, balance::Balance};
426 use rust_decimal::Decimal;
427 use rustrade_integration::collection::snapshot::Snapshot;
428
429 fn filler_event() -> UnindexedAccountEvent {
430 UnindexedAccountEvent::new(
433 ExchangeId::Mock,
434 AccountEventKind::BalanceSnapshot(Snapshot::new(AssetBalance {
435 asset: AssetNameExchange::new("btc"),
436 balance: Balance::new(Decimal::ONE, Decimal::ONE),
437 time_exchange: Utc::now(),
438 })),
439 )
440 }
441
442 #[tokio::test]
445 async fn fetch_trades_reads_the_span_for_the_requested_instruments() {
446 use crate::{
447 order::id::{OrderId, StrategyId},
448 trade::{AssetFees, Trade, TradeId},
449 };
450 use rustrade_instrument::Side;
451
452 let (_event_tx, event_rx) = broadcast::channel::<UnindexedAccountEvent>(1);
453 let (request_tx, mut request_rx) = mpsc::unbounded_channel();
454 let client = MockExecution::new(ExchangeId::Mock, Utc::now, request_tx, event_rx);
455 let start = Utc::now() - chrono::TimeDelta::hours(1);
456 let end = Utc::now();
457
458 let trade = move |instrument: &str| {
459 Trade::new(
460 TradeId::new(instrument),
461 OrderId::new("order"),
462 InstrumentNameExchange::new(instrument),
463 StrategyId::unknown(),
464 start,
465 Side::Buy,
466 Decimal::ONE,
467 Decimal::ONE,
468 None,
469 AssetFees::new(AssetNameExchange::new("usdt"), Decimal::ZERO, None),
470 )
471 };
472 let exchange = tokio::spawn(async move {
473 let request = request_rx.recv().await.expect("a request");
474 let crate::exchange::mock::request::MockExchangeRequestKind::FetchTrades {
475 response_tx,
476 start: asked_start,
477 end: asked_end,
478 } = request.kind
479 else {
480 panic!("expected FetchTrades, got {:?}", request.kind);
481 };
482 assert_eq!((asked_start, asked_end), (start, end));
483 response_tx
484 .send(vec![trade("btc_usdt"), trade("eth_usdt")])
485 .expect("the client awaits the answer");
486 });
487
488 let read = client
489 .fetch_trades(start, end, &[InstrumentNameExchange::new("eth_usdt")])
490 .await
491 .unwrap();
492 exchange.await.unwrap();
493 assert_eq!(read, TradesRead::complete(vec![trade("eth_usdt")]));
494 }
495
496 #[tokio::test]
499 async fn account_stream_emits_stream_terminated_on_broadcast_lag() {
500 let (event_tx, event_rx) = broadcast::channel::<UnindexedAccountEvent>(1);
502 let (request_tx, _request_rx) = mpsc::unbounded_channel();
503 let client = MockExecution::new(ExchangeId::Mock, Utc::now, request_tx, event_rx);
506
507 let mut stream = client.account_stream(&[], &[]).await.unwrap();
508
509 for _ in 0..3 {
511 event_tx.send(filler_event()).unwrap();
512 }
513
514 let event = stream.next().await.expect("expected a terminal event");
516 assert!(
517 matches!(
518 event.kind,
519 AccountEventKind::StreamTerminated(StreamTerminationReason::Error(_))
520 ),
521 "expected StreamTerminated(Error) on broadcast lag, got {:?}",
522 event.kind
523 );
524 assert_eq!(event.exchange, ExchangeId::Mock);
525
526 assert!(
528 stream.next().await.is_none(),
529 "stream must end after the terminal event"
530 );
531 }
532}