1use crate::{
2 AccountEventKind, UnindexedAccountEvent, UnindexedAccountSnapshot,
3 balance::AssetBalance,
4 borrow_capacity::BorrowCapacity,
5 client::{BorrowCapacityClient, ExecutionClient, OrderStatusClient},
6 error::{
7 ClientError, ConnectivityError, NotPlacedReason, OrderError, StreamTerminationReason,
8 UnindexedClientError, UnindexedOrderError,
9 },
10 exchange::mock::request::MockExchangeRequest,
11 fee::{FeeAssetConfig, FeeModelConfig, SpotFeeAsset},
12 fill::{MarketOrderTiming, SimFillConfig, StaleMarketPolicy},
13 margin::SpotMarginConfig,
14 order::{
15 Order, OrderKey, UnindexedInactiveOrder, UnindexedOrderKey,
16 request::{OrderRequestCancel, OrderRequestOpen, UnindexedOrderResponseCancel},
17 state::{Open, OrderState, UnindexedOrderState},
18 },
19 slippage::SlippageConfig,
20 trade::TradesRead,
21};
22use chrono::{DateTime, Utc};
23use derive_more::Constructor;
24use futures::{StreamExt, stream::BoxStream};
25use rustrade_instrument::{
26 Side,
27 asset::name::AssetNameExchange,
28 exchange::ExchangeId,
29 instrument::{kind::InstrumentKindDiscriminant, name::InstrumentNameExchange},
30};
31use serde::{Deserialize, Serialize};
32use tokio::sync::{broadcast, mpsc, oneshot};
33use tokio_stream::wrappers::BroadcastStream;
34use tracing::error;
35
36#[derive(
37 Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Constructor,
38)]
39pub struct MockExecutionConfig {
40 pub mocked_exchange: ExchangeId,
41 pub initial_state: UnindexedAccountSnapshot,
42 pub latency_ms: u64,
43 #[serde(default)]
48 pub fee_model: FeeModelConfig,
49 #[serde(default)]
56 pub fill_model: SimFillConfig,
57 #[serde(default)]
62 pub spot_fee_asset: SpotFeeAsset,
63 #[serde(default)]
70 pub slippage: SlippageConfig,
71 #[serde(default)]
78 pub market_order_timing: MarketOrderTiming,
79 #[serde(default)]
86 pub stale_market: StaleMarketPolicy,
87 #[serde(default, skip_serializing_if = "Option::is_none")]
95 pub spot_margin: Option<SpotMarginConfig>,
96 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub fee_asset: Option<FeeAssetConfig>,
103}
104
105#[derive(Debug, Constructor)]
106pub struct MockExecutionClientConfig<FnTime> {
107 pub mocked_exchange: ExchangeId,
108 pub clock: FnTime,
109 pub request_tx: mpsc::UnboundedSender<MockExchangeRequest>,
110 pub event_rx: broadcast::Receiver<UnindexedAccountEvent>,
111}
112
113impl<FnTime> Clone for MockExecutionClientConfig<FnTime>
114where
115 FnTime: Clone,
116{
117 fn clone(&self) -> Self {
118 Self {
119 mocked_exchange: self.mocked_exchange,
120 clock: self.clock.clone(),
121 request_tx: self.request_tx.clone(),
122 event_rx: self.event_rx.resubscribe(),
123 }
124 }
125}
126
127#[derive(Debug, Constructor)]
128pub struct MockExecution<FnTime> {
129 pub mocked_exchange: ExchangeId,
130 pub clock: FnTime,
131 pub request_tx: mpsc::UnboundedSender<MockExchangeRequest>,
132 pub event_rx: broadcast::Receiver<UnindexedAccountEvent>,
133}
134
135impl<FnTime> Clone for MockExecution<FnTime>
136where
137 FnTime: Clone,
138{
139 fn clone(&self) -> Self {
140 Self {
141 mocked_exchange: self.mocked_exchange,
142 clock: self.clock.clone(),
143 request_tx: self.request_tx.clone(),
144 event_rx: self.event_rx.resubscribe(),
145 }
146 }
147}
148
149impl<FnTime> MockExecution<FnTime>
150where
151 FnTime: Fn() -> DateTime<Utc>,
152{
153 pub fn time_request(&self) -> DateTime<Utc> {
154 (self.clock)()
155 }
156}
157
158impl<FnTime> ExecutionClient for MockExecution<FnTime>
159where
160 FnTime: Fn() -> DateTime<Utc> + Clone + Send + Sync,
161{
162 const EXCHANGE: ExchangeId = ExchangeId::Mock;
163
164 const SUPPORTED_KINDS: &'static [InstrumentKindDiscriminant] = &[
169 InstrumentKindDiscriminant::Spot,
170 InstrumentKindDiscriminant::Cfd,
171 ];
172 type Config = MockExecutionClientConfig<FnTime>;
173 type AccountStream = BoxStream<'static, UnindexedAccountEvent>;
174
175 fn new(config: Self::Config) -> Self {
176 Self {
177 mocked_exchange: config.mocked_exchange,
178 clock: config.clock,
179 request_tx: config.request_tx,
180 event_rx: config.event_rx,
181 }
182 }
183
184 async fn account_snapshot(
185 &self,
186 _: &[AssetNameExchange],
187 _: &[InstrumentNameExchange],
188 ) -> Result<UnindexedAccountSnapshot, UnindexedClientError> {
189 let (response_tx, response_rx) = oneshot::channel();
190
191 self.request_tx
192 .send(MockExchangeRequest::fetch_account_snapshot(
193 self.time_request(),
194 response_tx,
195 ))
196 .map_err(|_| {
197 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
198 self.mocked_exchange,
199 ))
200 })?;
201
202 response_rx.await.map_err(|_| {
203 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
204 self.mocked_exchange,
205 ))
206 })
207 }
208
209 async fn account_stream(
210 &self,
211 _: &[AssetNameExchange],
212 _: &[InstrumentNameExchange],
213 ) -> Result<Self::AccountStream, UnindexedClientError> {
214 let exchange = self.mocked_exchange;
220 let events = BroadcastStream::new(self.event_rx.resubscribe()).map(move |result| {
221 result.unwrap_or_else(|error| {
222 error!(
223 ?error,
224 "MockExchange Broadcast AccountStream lagged - terminating"
225 );
226 UnindexedAccountEvent::stream_terminated(
227 exchange,
228 StreamTerminationReason::Error("mock broadcast stream lagged".to_string()),
229 )
230 })
231 });
232 Ok(
233 futures::stream::unfold((events, false), |(mut events, ended)| async move {
234 if ended {
235 return None;
236 }
237 let event = events.next().await?;
238 let ended = matches!(event.kind, AccountEventKind::StreamTerminated(_));
239 Some((event, (events, ended)))
240 })
241 .fuse()
243 .boxed(),
244 )
245 }
246
247 async fn cancel_order(
248 &self,
249 request: OrderRequestCancel<ExchangeId, &InstrumentNameExchange>,
250 ) -> UnindexedOrderResponseCancel {
251 let (response_tx, response_rx) = oneshot::channel();
252
253 let key = OrderKey {
254 exchange: request.key.exchange,
255 instrument: request.key.instrument.clone(),
256 strategy: request.key.strategy.clone(),
257 cid: request.key.cid.clone(),
258 };
259
260 if self
261 .request_tx
262 .send(MockExchangeRequest::cancel_order(
263 self.time_request(),
264 response_tx,
265 request.into_owned_instrument(),
266 ))
267 .is_err()
268 {
269 return UnindexedOrderResponseCancel {
270 key,
271 state: Err(UnindexedOrderError::Connectivity(
272 ConnectivityError::ExchangeOffline(self.mocked_exchange),
273 )),
274 };
275 }
276
277 match response_rx.await {
278 Ok(response) => response,
279 Err(_) => UnindexedOrderResponseCancel {
280 key,
281 state: Err(UnindexedOrderError::Connectivity(
282 ConnectivityError::ExchangeOffline(self.mocked_exchange),
283 )),
284 },
285 }
286 }
287
288 async fn open_order(
289 &self,
290 request: OrderRequestOpen<ExchangeId, &InstrumentNameExchange>,
291 ) -> Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState> {
292 let (response_tx, response_rx) = oneshot::channel();
293
294 let request = request.into_owned_instrument();
295
296 if self
297 .request_tx
298 .send(MockExchangeRequest::open_order(
299 self.time_request(),
300 response_tx,
301 request.clone(),
306 ))
307 .is_err()
308 {
309 return Order {
311 key: request.key,
312 side: request.state.side,
313 price: request.state.price,
314 quantity: request.state.quantity,
315 kind: request.state.kind,
316 time_in_force: request.state.time_in_force,
317 state: OrderState::inactive(OrderError::NotPlaced(NotPlacedReason::NotSent(
318 ConnectivityError::ExchangeOffline(self.mocked_exchange),
319 ))),
320 };
321 }
322
323 match response_rx.await {
324 Ok(response) => response,
325 Err(_) => Order {
326 key: request.key,
327 side: request.state.side,
328 price: request.state.price,
329 quantity: request.state.quantity,
330 kind: request.state.kind,
331 time_in_force: request.state.time_in_force,
332 state: OrderState::inactive(OrderError::Connectivity(
333 ConnectivityError::ExchangeOffline(self.mocked_exchange),
334 )),
335 },
336 }
337 }
338
339 async fn fetch_balances(
340 &self,
341 assets: &[AssetNameExchange],
342 ) -> Result<Vec<AssetBalance<AssetNameExchange>>, UnindexedClientError> {
343 let (response_tx, response_rx) = oneshot::channel();
344
345 self.request_tx
346 .send(MockExchangeRequest::fetch_balances(
347 self.time_request(),
348 assets.to_vec(),
349 response_tx,
350 ))
351 .map_err(|_| {
352 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
353 self.mocked_exchange,
354 ))
355 })?;
356
357 response_rx.await.map_err(|_| {
358 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
359 self.mocked_exchange,
360 ))
361 })
362 }
363
364 async fn fetch_open_orders(
365 &self,
366 instruments: &[InstrumentNameExchange],
367 ) -> Result<Vec<Order<ExchangeId, InstrumentNameExchange, Open>>, UnindexedClientError> {
368 let (response_tx, response_rx) = oneshot::channel();
369
370 self.request_tx
371 .send(MockExchangeRequest::fetch_orders_open(
372 self.time_request(),
373 instruments.to_vec(),
374 response_tx,
375 ))
376 .map_err(|_| {
377 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
378 self.mocked_exchange,
379 ))
380 })?;
381
382 response_rx.await.map_err(|_| {
383 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
384 self.mocked_exchange,
385 ))
386 })
387 }
388
389 async fn fetch_trades(
392 &self,
393 start: DateTime<Utc>,
394 end: DateTime<Utc>,
395 instruments: &[InstrumentNameExchange],
396 ) -> Result<TradesRead<AssetNameExchange, InstrumentNameExchange>, UnindexedClientError> {
397 let (response_tx, response_rx) = oneshot::channel();
398
399 self.request_tx
400 .send(MockExchangeRequest::fetch_trades(
401 self.time_request(),
402 response_tx,
403 start,
404 end,
405 ))
406 .map_err(|_| {
407 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
408 self.mocked_exchange,
409 ))
410 })?;
411
412 let mut trades = response_rx.await.map_err(|_| {
413 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
414 self.mocked_exchange,
415 ))
416 })?;
417 if !instruments.is_empty() {
418 trades.retain(|trade| instruments.contains(&trade.instrument));
419 }
420 Ok(TradesRead::complete(trades))
421 }
422}
423
424impl<FnTime> OrderStatusClient for MockExecution<FnTime>
428where
429 FnTime: Fn() -> DateTime<Utc> + Clone + Send + Sync,
430{
431 async fn fetch_ended_orders(
432 &self,
433 orders: &[UnindexedOrderKey],
434 ) -> Result<Vec<UnindexedInactiveOrder>, UnindexedClientError> {
435 let (response_tx, response_rx) = oneshot::channel();
436
437 self.request_tx
438 .send(MockExchangeRequest::fetch_orders_ended(
439 self.time_request(),
440 orders.to_vec(),
441 response_tx,
442 ))
443 .map_err(|_| {
444 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
445 self.mocked_exchange,
446 ))
447 })?;
448
449 response_rx.await.map_err(|_| {
450 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
451 self.mocked_exchange,
452 ))
453 })
454 }
455}
456
457impl<FnTime> BorrowCapacityClient for MockExecution<FnTime>
463where
464 FnTime: Fn() -> DateTime<Utc> + Clone + Send + Sync,
465{
466 async fn fetch_borrow_capacity(
467 &self,
468 instrument: &InstrumentNameExchange,
469 side: Side,
470 ) -> Result<BorrowCapacity, UnindexedClientError> {
471 request_borrow_capacity(
472 &self.request_tx,
473 self.mocked_exchange,
474 self.time_request(),
475 instrument,
476 side,
477 )
478 .await
479 }
480}
481
482pub async fn request_borrow_capacity(
497 request_tx: &mpsc::UnboundedSender<MockExchangeRequest>,
498 mocked_exchange: ExchangeId,
499 time_request: DateTime<Utc>,
500 instrument: &InstrumentNameExchange,
501 side: Side,
502) -> Result<BorrowCapacity, UnindexedClientError> {
503 let offline =
504 || UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(mocked_exchange));
505 let (response_tx, response_rx) = oneshot::channel();
506
507 request_tx
508 .send(MockExchangeRequest::fetch_borrow_capacity(
509 time_request,
510 instrument.clone(),
511 side,
512 response_tx,
513 ))
514 .map_err(|_| offline())?;
515
516 response_rx
517 .await
518 .map_err(|_| offline())?
519 .map_err(ClientError::Api)
520}
521
522pub async fn request_checkpoint(
537 request_tx: &mpsc::UnboundedSender<MockExchangeRequest>,
538 mocked_exchange: ExchangeId,
539 time_request: DateTime<Utc>,
540) -> Result<u64, UnindexedClientError> {
541 let offline =
542 || UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(mocked_exchange));
543 let (response_tx, response_rx) = oneshot::channel();
544 request_tx
545 .send(MockExchangeRequest::checkpoint(time_request, response_tx))
546 .map_err(|_| offline())?;
547 response_rx.await.map_err(|_| offline())
548}
549
550#[cfg(test)]
551#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
553 use super::*;
554 use crate::{AccountEventKind, balance::Balance};
555 use rust_decimal::Decimal;
556 use rustrade_integration::collection::snapshot::Snapshot;
557
558 fn filler_event() -> UnindexedAccountEvent {
559 UnindexedAccountEvent::new(
562 ExchangeId::Mock,
563 AccountEventKind::BalanceSnapshot(Snapshot::new(AssetBalance {
564 asset: AssetNameExchange::new("btc"),
565 balance: Balance::new(Decimal::ONE, Decimal::ONE),
566 time_exchange: Utc::now(),
567 })),
568 )
569 }
570
571 #[tokio::test]
574 async fn fetch_trades_reads_the_span_for_the_requested_instruments() {
575 use crate::{
576 order::id::{OrderId, StrategyId},
577 trade::{AssetFees, Trade, TradeId},
578 };
579 use rustrade_instrument::Side;
580
581 let (_event_tx, event_rx) = broadcast::channel::<UnindexedAccountEvent>(1);
582 let (request_tx, mut request_rx) = mpsc::unbounded_channel();
583 let client = MockExecution::new(ExchangeId::Mock, Utc::now, request_tx, event_rx);
584 let start = Utc::now() - chrono::TimeDelta::hours(1);
585 let end = Utc::now();
586
587 let trade = move |instrument: &str| {
588 Trade::new(
589 TradeId::new(instrument),
590 OrderId::new("order"),
591 InstrumentNameExchange::new(instrument),
592 StrategyId::unknown(),
593 start,
594 Side::Buy,
595 Decimal::ONE,
596 Decimal::ONE,
597 None,
598 AssetFees::new(AssetNameExchange::new("usdt"), Decimal::ZERO, None),
599 )
600 };
601 let exchange = tokio::spawn(async move {
602 let request = request_rx.recv().await.expect("a request");
603 let crate::exchange::mock::request::MockExchangeRequestKind::FetchTrades {
604 response_tx,
605 start: asked_start,
606 end: asked_end,
607 } = request.kind
608 else {
609 panic!("expected FetchTrades, got {:?}", request.kind);
610 };
611 assert_eq!((asked_start, asked_end), (start, end));
612 response_tx
613 .send(vec![trade("btc_usdt"), trade("eth_usdt")])
614 .expect("the client awaits the answer");
615 });
616
617 let read = client
618 .fetch_trades(start, end, &[InstrumentNameExchange::new("eth_usdt")])
619 .await
620 .unwrap();
621 exchange.await.unwrap();
622 assert_eq!(read, TradesRead::complete(vec![trade("eth_usdt")]));
623 }
624
625 #[tokio::test]
628 async fn request_checkpoint_answers_with_the_exchange_s_id() {
629 let (request_tx, mut request_rx) = mpsc::unbounded_channel::<MockExchangeRequest>();
630 let exchange = tokio::spawn(async move {
631 let request = request_rx.recv().await.expect("a request");
632 let crate::exchange::mock::request::MockExchangeRequestKind::Checkpoint { response_tx } =
633 request.kind
634 else {
635 panic!("expected Checkpoint, got {:?}", request.kind);
636 };
637 response_tx.send(7).expect("the caller awaits the answer");
638 });
639
640 let id = request_checkpoint(&request_tx, ExchangeId::Mock, Utc::now()).await;
641 assert_eq!(id.unwrap(), 7);
643 exchange.await.unwrap();
644
645 assert!(matches!(
647 request_checkpoint(&request_tx, ExchangeId::Mock, Utc::now()).await,
648 Err(UnindexedClientError::Connectivity(
649 ConnectivityError::ExchangeOffline(ExchangeId::Mock)
650 ))
651 ));
652 }
653
654 #[tokio::test]
657 async fn account_stream_emits_stream_terminated_on_broadcast_lag() {
658 let (event_tx, event_rx) = broadcast::channel::<UnindexedAccountEvent>(1);
660 let (request_tx, _request_rx) = mpsc::unbounded_channel();
661 let client = MockExecution::new(ExchangeId::Mock, Utc::now, request_tx, event_rx);
664
665 let mut stream = client.account_stream(&[], &[]).await.unwrap();
666
667 for _ in 0..3 {
669 event_tx.send(filler_event()).unwrap();
670 }
671
672 let event = stream.next().await.expect("expected a terminal event");
674 assert!(
675 matches!(
676 event.kind,
677 AccountEventKind::StreamTerminated(StreamTerminationReason::Error(_))
678 ),
679 "expected StreamTerminated(Error) on broadcast lag, got {:?}",
680 event.kind
681 );
682 assert_eq!(event.exchange, ExchangeId::Mock);
683
684 assert!(
686 stream.next().await.is_none(),
687 "stream must end after the terminal event"
688 );
689 }
690
691 #[tokio::test]
695 async fn account_stream_ends_right_after_a_terminal_event_from_the_venue() {
696 let (event_tx, event_rx) = broadcast::channel::<UnindexedAccountEvent>(16);
697 let (request_tx, _request_rx) = mpsc::unbounded_channel();
698 let client = MockExecution::new(ExchangeId::Mock, Utc::now, request_tx, event_rx);
699 let mut stream = client.account_stream(&[], &[]).await.unwrap();
700
701 event_tx.send(filler_event()).unwrap();
702 event_tx
703 .send(UnindexedAccountEvent::stream_terminated(
704 ExchangeId::Mock,
705 StreamTerminationReason::Error("market feed lagged".to_string()),
706 ))
707 .unwrap();
708
709 assert!(matches!(
710 stream.next().await.map(|event| event.kind),
711 Some(AccountEventKind::BalanceSnapshot(_))
712 ));
713 assert!(matches!(
714 stream.next().await.map(|event| event.kind),
715 Some(AccountEventKind::StreamTerminated(_))
716 ));
717 assert_eq!(
718 futures::FutureExt::now_or_never(stream.next()),
719 Some(None),
720 "the stream must end on the poll after its terminal event, with the sender still open"
721 );
722 assert_eq!(
723 futures::FutureExt::now_or_never(stream.next()),
724 Some(None),
725 "and stay ended however often it is polled"
726 );
727 drop(event_tx);
728 }
729}