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 let (response_tx, response_rx) = oneshot::channel();
472
473 self.request_tx
474 .send(MockExchangeRequest::fetch_borrow_capacity(
475 self.time_request(),
476 instrument.clone(),
477 side,
478 response_tx,
479 ))
480 .map_err(|_| {
481 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
482 self.mocked_exchange,
483 ))
484 })?;
485
486 response_rx
487 .await
488 .map_err(|_| {
489 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
490 self.mocked_exchange,
491 ))
492 })?
493 .map_err(ClientError::Api)
494 }
495}
496
497#[cfg(test)]
498#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
500 use super::*;
501 use crate::{AccountEventKind, balance::Balance};
502 use rust_decimal::Decimal;
503 use rustrade_integration::collection::snapshot::Snapshot;
504
505 fn filler_event() -> UnindexedAccountEvent {
506 UnindexedAccountEvent::new(
509 ExchangeId::Mock,
510 AccountEventKind::BalanceSnapshot(Snapshot::new(AssetBalance {
511 asset: AssetNameExchange::new("btc"),
512 balance: Balance::new(Decimal::ONE, Decimal::ONE),
513 time_exchange: Utc::now(),
514 })),
515 )
516 }
517
518 #[tokio::test]
521 async fn fetch_trades_reads_the_span_for_the_requested_instruments() {
522 use crate::{
523 order::id::{OrderId, StrategyId},
524 trade::{AssetFees, Trade, TradeId},
525 };
526 use rustrade_instrument::Side;
527
528 let (_event_tx, event_rx) = broadcast::channel::<UnindexedAccountEvent>(1);
529 let (request_tx, mut request_rx) = mpsc::unbounded_channel();
530 let client = MockExecution::new(ExchangeId::Mock, Utc::now, request_tx, event_rx);
531 let start = Utc::now() - chrono::TimeDelta::hours(1);
532 let end = Utc::now();
533
534 let trade = move |instrument: &str| {
535 Trade::new(
536 TradeId::new(instrument),
537 OrderId::new("order"),
538 InstrumentNameExchange::new(instrument),
539 StrategyId::unknown(),
540 start,
541 Side::Buy,
542 Decimal::ONE,
543 Decimal::ONE,
544 None,
545 AssetFees::new(AssetNameExchange::new("usdt"), Decimal::ZERO, None),
546 )
547 };
548 let exchange = tokio::spawn(async move {
549 let request = request_rx.recv().await.expect("a request");
550 let crate::exchange::mock::request::MockExchangeRequestKind::FetchTrades {
551 response_tx,
552 start: asked_start,
553 end: asked_end,
554 } = request.kind
555 else {
556 panic!("expected FetchTrades, got {:?}", request.kind);
557 };
558 assert_eq!((asked_start, asked_end), (start, end));
559 response_tx
560 .send(vec![trade("btc_usdt"), trade("eth_usdt")])
561 .expect("the client awaits the answer");
562 });
563
564 let read = client
565 .fetch_trades(start, end, &[InstrumentNameExchange::new("eth_usdt")])
566 .await
567 .unwrap();
568 exchange.await.unwrap();
569 assert_eq!(read, TradesRead::complete(vec![trade("eth_usdt")]));
570 }
571
572 #[tokio::test]
575 async fn account_stream_emits_stream_terminated_on_broadcast_lag() {
576 let (event_tx, event_rx) = broadcast::channel::<UnindexedAccountEvent>(1);
578 let (request_tx, _request_rx) = mpsc::unbounded_channel();
579 let client = MockExecution::new(ExchangeId::Mock, Utc::now, request_tx, event_rx);
582
583 let mut stream = client.account_stream(&[], &[]).await.unwrap();
584
585 for _ in 0..3 {
587 event_tx.send(filler_event()).unwrap();
588 }
589
590 let event = stream.next().await.expect("expected a terminal event");
592 assert!(
593 matches!(
594 event.kind,
595 AccountEventKind::StreamTerminated(StreamTerminationReason::Error(_))
596 ),
597 "expected StreamTerminated(Error) on broadcast lag, got {:?}",
598 event.kind
599 );
600 assert_eq!(event.exchange, ExchangeId::Mock);
601
602 assert!(
604 stream.next().await.is_none(),
605 "stream must end after the terminal event"
606 );
607 }
608
609 #[tokio::test]
613 async fn account_stream_ends_right_after_a_terminal_event_from_the_venue() {
614 let (event_tx, event_rx) = broadcast::channel::<UnindexedAccountEvent>(16);
615 let (request_tx, _request_rx) = mpsc::unbounded_channel();
616 let client = MockExecution::new(ExchangeId::Mock, Utc::now, request_tx, event_rx);
617 let mut stream = client.account_stream(&[], &[]).await.unwrap();
618
619 event_tx.send(filler_event()).unwrap();
620 event_tx
621 .send(UnindexedAccountEvent::stream_terminated(
622 ExchangeId::Mock,
623 StreamTerminationReason::Error("market feed lagged".to_string()),
624 ))
625 .unwrap();
626
627 assert!(matches!(
628 stream.next().await.map(|event| event.kind),
629 Some(AccountEventKind::BalanceSnapshot(_))
630 ));
631 assert!(matches!(
632 stream.next().await.map(|event| event.kind),
633 Some(AccountEventKind::StreamTerminated(_))
634 ));
635 assert_eq!(
636 futures::FutureExt::now_or_never(stream.next()),
637 Some(None),
638 "the stream must end on the poll after its terminal event, with the sender still open"
639 );
640 assert_eq!(
641 futures::FutureExt::now_or_never(stream.next()),
642 Some(None),
643 "and stay ended however often it is polled"
644 );
645 drop(event_tx);
646 }
647}