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 expiry::LongOptionExpiry,
12 fee::{FeeAssetConfig, FeeModelConfig, SpotFeeAsset},
13 fill::{MarketOrderTiming, SimFillConfig, StaleMarketPolicy},
14 margin::SpotMarginConfig,
15 order::{
16 Order, OrderKey, UnindexedInactiveOrder, UnindexedOrderKey,
17 request::{OrderRequestCancel, OrderRequestOpen, UnindexedOrderResponseCancel},
18 state::{Open, OrderState, UnindexedOrderState},
19 },
20 short_option::ShortPutCollateral,
21 slippage::SlippageConfig,
22 trade::TradesRead,
23};
24use chrono::{DateTime, Utc};
25use derive_more::Constructor;
26use futures::{StreamExt, stream::BoxStream};
27use rustrade_instrument::{
28 Side,
29 asset::name::AssetNameExchange,
30 exchange::ExchangeId,
31 instrument::{kind::InstrumentKindDiscriminant, name::InstrumentNameExchange},
32};
33use serde::{Deserialize, Serialize};
34use tokio::sync::{broadcast, mpsc, oneshot};
35use tokio_stream::wrappers::BroadcastStream;
36use tracing::error;
37
38#[derive(
39 Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Constructor,
40)]
41pub struct MockExecutionConfig {
42 pub mocked_exchange: ExchangeId,
43 pub initial_state: UnindexedAccountSnapshot,
44 pub latency_ms: u64,
45 #[serde(default)]
50 pub fee_model: FeeModelConfig,
51 #[serde(default)]
58 pub fill_model: SimFillConfig,
59 #[serde(default)]
64 pub spot_fee_asset: SpotFeeAsset,
65 #[serde(default)]
72 pub slippage: SlippageConfig,
73 #[serde(default)]
80 pub market_order_timing: MarketOrderTiming,
81 #[serde(default)]
88 pub stale_market: StaleMarketPolicy,
89 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub spot_margin: Option<SpotMarginConfig>,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub fee_asset: Option<FeeAssetConfig>,
105 #[serde(default)]
114 pub long_option_expiry: LongOptionExpiry,
115 #[serde(default)]
118 pub short_put_collateral: ShortPutCollateral,
119}
120
121#[derive(Debug, Constructor)]
122pub struct MockExecutionClientConfig<FnTime> {
123 pub mocked_exchange: ExchangeId,
124 pub clock: FnTime,
125 pub request_tx: mpsc::UnboundedSender<MockExchangeRequest>,
126 pub event_rx: broadcast::Receiver<UnindexedAccountEvent>,
127}
128
129impl<FnTime> Clone for MockExecutionClientConfig<FnTime>
130where
131 FnTime: Clone,
132{
133 fn clone(&self) -> Self {
134 Self {
135 mocked_exchange: self.mocked_exchange,
136 clock: self.clock.clone(),
137 request_tx: self.request_tx.clone(),
138 event_rx: self.event_rx.resubscribe(),
139 }
140 }
141}
142
143#[derive(Debug, Constructor)]
144pub struct MockExecution<FnTime> {
145 pub mocked_exchange: ExchangeId,
146 pub clock: FnTime,
147 pub request_tx: mpsc::UnboundedSender<MockExchangeRequest>,
148 pub event_rx: broadcast::Receiver<UnindexedAccountEvent>,
149}
150
151impl<FnTime> Clone for MockExecution<FnTime>
152where
153 FnTime: Clone,
154{
155 fn clone(&self) -> Self {
156 Self {
157 mocked_exchange: self.mocked_exchange,
158 clock: self.clock.clone(),
159 request_tx: self.request_tx.clone(),
160 event_rx: self.event_rx.resubscribe(),
161 }
162 }
163}
164
165impl<FnTime> MockExecution<FnTime>
166where
167 FnTime: Fn() -> DateTime<Utc>,
168{
169 pub fn time_request(&self) -> DateTime<Utc> {
170 (self.clock)()
171 }
172}
173
174impl<FnTime> ExecutionClient for MockExecution<FnTime>
175where
176 FnTime: Fn() -> DateTime<Utc> + Clone + Send + Sync,
177{
178 const EXCHANGE: ExchangeId = ExchangeId::Mock;
179
180 const SUPPORTED_KINDS: &'static [InstrumentKindDiscriminant] = &[
185 InstrumentKindDiscriminant::Spot,
186 InstrumentKindDiscriminant::Cfd,
187 InstrumentKindDiscriminant::Option,
188 ];
189 type Config = MockExecutionClientConfig<FnTime>;
190 type AccountStream = BoxStream<'static, UnindexedAccountEvent>;
191
192 fn new(config: Self::Config) -> Self {
193 Self {
194 mocked_exchange: config.mocked_exchange,
195 clock: config.clock,
196 request_tx: config.request_tx,
197 event_rx: config.event_rx,
198 }
199 }
200
201 async fn account_snapshot(
202 &self,
203 _: &[AssetNameExchange],
204 _: &[InstrumentNameExchange],
205 ) -> Result<UnindexedAccountSnapshot, UnindexedClientError> {
206 let (response_tx, response_rx) = oneshot::channel();
207
208 self.request_tx
209 .send(MockExchangeRequest::fetch_account_snapshot(
210 self.time_request(),
211 response_tx,
212 ))
213 .map_err(|_| {
214 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
215 self.mocked_exchange,
216 ))
217 })?;
218
219 response_rx.await.map_err(|_| {
220 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
221 self.mocked_exchange,
222 ))
223 })
224 }
225
226 async fn account_stream(
227 &self,
228 _: &[AssetNameExchange],
229 _: &[InstrumentNameExchange],
230 ) -> Result<Self::AccountStream, UnindexedClientError> {
231 let exchange = self.mocked_exchange;
237 let events = BroadcastStream::new(self.event_rx.resubscribe()).map(move |result| {
238 result.unwrap_or_else(|error| {
239 error!(
240 ?error,
241 "MockExchange Broadcast AccountStream lagged - terminating"
242 );
243 UnindexedAccountEvent::stream_terminated(
244 exchange,
245 StreamTerminationReason::Error("mock broadcast stream lagged".to_string()),
246 )
247 })
248 });
249 Ok(
250 futures::stream::unfold((events, false), |(mut events, ended)| async move {
251 if ended {
252 return None;
253 }
254 let event = events.next().await?;
255 let ended = matches!(event.kind, AccountEventKind::StreamTerminated(_));
256 Some((event, (events, ended)))
257 })
258 .fuse()
260 .boxed(),
261 )
262 }
263
264 async fn cancel_order(
265 &self,
266 request: OrderRequestCancel<ExchangeId, &InstrumentNameExchange>,
267 ) -> UnindexedOrderResponseCancel {
268 let (response_tx, response_rx) = oneshot::channel();
269
270 let key = OrderKey {
271 exchange: request.key.exchange,
272 instrument: request.key.instrument.clone(),
273 strategy: request.key.strategy.clone(),
274 cid: request.key.cid.clone(),
275 };
276
277 if self
278 .request_tx
279 .send(MockExchangeRequest::cancel_order(
280 self.time_request(),
281 response_tx,
282 request.into_owned_instrument(),
283 ))
284 .is_err()
285 {
286 return UnindexedOrderResponseCancel {
287 key,
288 state: Err(UnindexedOrderError::Connectivity(
289 ConnectivityError::ExchangeOffline(self.mocked_exchange),
290 )),
291 };
292 }
293
294 match response_rx.await {
295 Ok(response) => response,
296 Err(_) => UnindexedOrderResponseCancel {
297 key,
298 state: Err(UnindexedOrderError::Connectivity(
299 ConnectivityError::ExchangeOffline(self.mocked_exchange),
300 )),
301 },
302 }
303 }
304
305 async fn open_order(
306 &self,
307 request: OrderRequestOpen<ExchangeId, &InstrumentNameExchange>,
308 ) -> Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState> {
309 let (response_tx, response_rx) = oneshot::channel();
310
311 let request = request.into_owned_instrument();
312
313 if self
314 .request_tx
315 .send(MockExchangeRequest::open_order(
316 self.time_request(),
317 response_tx,
318 request.clone(),
323 ))
324 .is_err()
325 {
326 return Order {
328 key: request.key,
329 side: request.state.side,
330 price: request.state.price,
331 quantity: request.state.quantity,
332 kind: request.state.kind,
333 time_in_force: request.state.time_in_force,
334 state: OrderState::inactive(OrderError::NotPlaced(NotPlacedReason::NotSent(
335 ConnectivityError::ExchangeOffline(self.mocked_exchange),
336 ))),
337 };
338 }
339
340 match response_rx.await {
341 Ok(response) => response,
342 Err(_) => Order {
343 key: request.key,
344 side: request.state.side,
345 price: request.state.price,
346 quantity: request.state.quantity,
347 kind: request.state.kind,
348 time_in_force: request.state.time_in_force,
349 state: OrderState::inactive(OrderError::Connectivity(
350 ConnectivityError::ExchangeOffline(self.mocked_exchange),
351 )),
352 },
353 }
354 }
355
356 async fn fetch_balances(
357 &self,
358 assets: &[AssetNameExchange],
359 ) -> Result<Vec<AssetBalance<AssetNameExchange>>, UnindexedClientError> {
360 let (response_tx, response_rx) = oneshot::channel();
361
362 self.request_tx
363 .send(MockExchangeRequest::fetch_balances(
364 self.time_request(),
365 assets.to_vec(),
366 response_tx,
367 ))
368 .map_err(|_| {
369 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
370 self.mocked_exchange,
371 ))
372 })?;
373
374 response_rx.await.map_err(|_| {
375 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
376 self.mocked_exchange,
377 ))
378 })
379 }
380
381 async fn fetch_open_orders(
382 &self,
383 instruments: &[InstrumentNameExchange],
384 ) -> Result<Vec<Order<ExchangeId, InstrumentNameExchange, Open>>, UnindexedClientError> {
385 let (response_tx, response_rx) = oneshot::channel();
386
387 self.request_tx
388 .send(MockExchangeRequest::fetch_orders_open(
389 self.time_request(),
390 instruments.to_vec(),
391 response_tx,
392 ))
393 .map_err(|_| {
394 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
395 self.mocked_exchange,
396 ))
397 })?;
398
399 response_rx.await.map_err(|_| {
400 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
401 self.mocked_exchange,
402 ))
403 })
404 }
405
406 async fn fetch_trades(
409 &self,
410 start: DateTime<Utc>,
411 end: DateTime<Utc>,
412 instruments: &[InstrumentNameExchange],
413 ) -> Result<TradesRead<AssetNameExchange, InstrumentNameExchange>, UnindexedClientError> {
414 let (response_tx, response_rx) = oneshot::channel();
415
416 self.request_tx
417 .send(MockExchangeRequest::fetch_trades(
418 self.time_request(),
419 response_tx,
420 start,
421 end,
422 ))
423 .map_err(|_| {
424 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
425 self.mocked_exchange,
426 ))
427 })?;
428
429 let mut trades = response_rx.await.map_err(|_| {
430 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
431 self.mocked_exchange,
432 ))
433 })?;
434 if !instruments.is_empty() {
435 trades.retain(|trade| instruments.contains(&trade.instrument));
436 }
437 Ok(TradesRead::complete(trades))
438 }
439}
440
441impl<FnTime> OrderStatusClient for MockExecution<FnTime>
445where
446 FnTime: Fn() -> DateTime<Utc> + Clone + Send + Sync,
447{
448 async fn fetch_ended_orders(
449 &self,
450 orders: &[UnindexedOrderKey],
451 ) -> Result<Vec<UnindexedInactiveOrder>, UnindexedClientError> {
452 let (response_tx, response_rx) = oneshot::channel();
453
454 self.request_tx
455 .send(MockExchangeRequest::fetch_orders_ended(
456 self.time_request(),
457 orders.to_vec(),
458 response_tx,
459 ))
460 .map_err(|_| {
461 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
462 self.mocked_exchange,
463 ))
464 })?;
465
466 response_rx.await.map_err(|_| {
467 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
468 self.mocked_exchange,
469 ))
470 })
471 }
472}
473
474impl<FnTime> BorrowCapacityClient for MockExecution<FnTime>
480where
481 FnTime: Fn() -> DateTime<Utc> + Clone + Send + Sync,
482{
483 async fn fetch_borrow_capacity(
484 &self,
485 instrument: &InstrumentNameExchange,
486 side: Side,
487 ) -> Result<BorrowCapacity, UnindexedClientError> {
488 request_borrow_capacity(
489 &self.request_tx,
490 self.mocked_exchange,
491 self.time_request(),
492 instrument,
493 side,
494 )
495 .await
496 }
497}
498
499pub async fn request_borrow_capacity(
514 request_tx: &mpsc::UnboundedSender<MockExchangeRequest>,
515 mocked_exchange: ExchangeId,
516 time_request: DateTime<Utc>,
517 instrument: &InstrumentNameExchange,
518 side: Side,
519) -> Result<BorrowCapacity, UnindexedClientError> {
520 let offline =
521 || UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(mocked_exchange));
522 let (response_tx, response_rx) = oneshot::channel();
523
524 request_tx
525 .send(MockExchangeRequest::fetch_borrow_capacity(
526 time_request,
527 instrument.clone(),
528 side,
529 response_tx,
530 ))
531 .map_err(|_| offline())?;
532
533 response_rx
534 .await
535 .map_err(|_| offline())?
536 .map_err(ClientError::Api)
537}
538
539pub async fn request_checkpoint(
554 request_tx: &mpsc::UnboundedSender<MockExchangeRequest>,
555 mocked_exchange: ExchangeId,
556 time_request: DateTime<Utc>,
557) -> Result<u64, UnindexedClientError> {
558 let offline =
559 || UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(mocked_exchange));
560 let (response_tx, response_rx) = oneshot::channel();
561 request_tx
562 .send(MockExchangeRequest::checkpoint(time_request, response_tx))
563 .map_err(|_| offline())?;
564 response_rx.await.map_err(|_| offline())
565}
566
567#[cfg(test)]
568#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
570 use super::*;
571 use crate::{AccountEventKind, balance::Balance};
572 use rust_decimal::Decimal;
573 use rustrade_integration::collection::snapshot::Snapshot;
574
575 fn filler_event() -> UnindexedAccountEvent {
576 UnindexedAccountEvent::new(
579 ExchangeId::Mock,
580 AccountEventKind::BalanceSnapshot(Snapshot::new(AssetBalance {
581 asset: AssetNameExchange::new("btc"),
582 balance: Balance::new(Decimal::ONE, Decimal::ONE),
583 time_exchange: Utc::now(),
584 })),
585 )
586 }
587
588 #[tokio::test]
591 async fn fetch_trades_reads_the_span_for_the_requested_instruments() {
592 use crate::{
593 order::id::{OrderId, StrategyId},
594 trade::{AssetFees, Trade, TradeId},
595 };
596 use rustrade_instrument::Side;
597
598 let (_event_tx, event_rx) = broadcast::channel::<UnindexedAccountEvent>(1);
599 let (request_tx, mut request_rx) = mpsc::unbounded_channel();
600 let client = MockExecution::new(ExchangeId::Mock, Utc::now, request_tx, event_rx);
601 let start = Utc::now() - chrono::TimeDelta::hours(1);
602 let end = Utc::now();
603
604 let trade = move |instrument: &str| {
605 Trade::new(
606 TradeId::new(instrument),
607 OrderId::new("order"),
608 InstrumentNameExchange::new(instrument),
609 StrategyId::unknown(),
610 start,
611 Side::Buy,
612 Decimal::ONE,
613 Decimal::ONE,
614 None,
615 AssetFees::new(AssetNameExchange::new("usdt"), Decimal::ZERO, None),
616 )
617 };
618 let exchange = tokio::spawn(async move {
619 let request = request_rx.recv().await.expect("a request");
620 let crate::exchange::mock::request::MockExchangeRequestKind::FetchTrades {
621 response_tx,
622 start: asked_start,
623 end: asked_end,
624 } = request.kind
625 else {
626 panic!("expected FetchTrades, got {:?}", request.kind);
627 };
628 assert_eq!((asked_start, asked_end), (start, end));
629 response_tx
630 .send(vec![trade("btc_usdt"), trade("eth_usdt")])
631 .expect("the client awaits the answer");
632 });
633
634 let read = client
635 .fetch_trades(start, end, &[InstrumentNameExchange::new("eth_usdt")])
636 .await
637 .unwrap();
638 exchange.await.unwrap();
639 assert_eq!(read, TradesRead::complete(vec![trade("eth_usdt")]));
640 }
641
642 #[tokio::test]
645 async fn request_checkpoint_answers_with_the_exchange_s_id() {
646 let (request_tx, mut request_rx) = mpsc::unbounded_channel::<MockExchangeRequest>();
647 let exchange = tokio::spawn(async move {
648 let request = request_rx.recv().await.expect("a request");
649 let crate::exchange::mock::request::MockExchangeRequestKind::Checkpoint { response_tx } =
650 request.kind
651 else {
652 panic!("expected Checkpoint, got {:?}", request.kind);
653 };
654 response_tx.send(7).expect("the caller awaits the answer");
655 });
656
657 let id = request_checkpoint(&request_tx, ExchangeId::Mock, Utc::now()).await;
658 assert_eq!(id.unwrap(), 7);
660 exchange.await.unwrap();
661
662 assert!(matches!(
664 request_checkpoint(&request_tx, ExchangeId::Mock, Utc::now()).await,
665 Err(UnindexedClientError::Connectivity(
666 ConnectivityError::ExchangeOffline(ExchangeId::Mock)
667 ))
668 ));
669 }
670
671 #[tokio::test]
674 async fn account_stream_emits_stream_terminated_on_broadcast_lag() {
675 let (event_tx, event_rx) = broadcast::channel::<UnindexedAccountEvent>(1);
677 let (request_tx, _request_rx) = mpsc::unbounded_channel();
678 let client = MockExecution::new(ExchangeId::Mock, Utc::now, request_tx, event_rx);
681
682 let mut stream = client.account_stream(&[], &[]).await.unwrap();
683
684 for _ in 0..3 {
686 event_tx.send(filler_event()).unwrap();
687 }
688
689 let event = stream.next().await.expect("expected a terminal event");
691 assert!(
692 matches!(
693 event.kind,
694 AccountEventKind::StreamTerminated(StreamTerminationReason::Error(_))
695 ),
696 "expected StreamTerminated(Error) on broadcast lag, got {:?}",
697 event.kind
698 );
699 assert_eq!(event.exchange, ExchangeId::Mock);
700
701 assert!(
703 stream.next().await.is_none(),
704 "stream must end after the terminal event"
705 );
706 }
707
708 #[tokio::test]
712 async fn account_stream_ends_right_after_a_terminal_event_from_the_venue() {
713 let (event_tx, event_rx) = broadcast::channel::<UnindexedAccountEvent>(16);
714 let (request_tx, _request_rx) = mpsc::unbounded_channel();
715 let client = MockExecution::new(ExchangeId::Mock, Utc::now, request_tx, event_rx);
716 let mut stream = client.account_stream(&[], &[]).await.unwrap();
717
718 event_tx.send(filler_event()).unwrap();
719 event_tx
720 .send(UnindexedAccountEvent::stream_terminated(
721 ExchangeId::Mock,
722 StreamTerminationReason::Error("market feed lagged".to_string()),
723 ))
724 .unwrap();
725
726 assert!(matches!(
727 stream.next().await.map(|event| event.kind),
728 Some(AccountEventKind::BalanceSnapshot(_))
729 ));
730 assert!(matches!(
731 stream.next().await.map(|event| event.kind),
732 Some(AccountEventKind::StreamTerminated(_))
733 ));
734 assert_eq!(
735 futures::FutureExt::now_or_never(stream.next()),
736 Some(None),
737 "the stream must end on the poll after its terminal event, with the sender still open"
738 );
739 assert_eq!(
740 futures::FutureExt::now_or_never(stream.next()),
741 Some(None),
742 "and stay ended however often it is polled"
743 );
744 drop(event_tx);
745 }
746}