1use crate::{
2 AccountEventKind, UnindexedAccountEvent, UnindexedAccountSnapshot,
3 balance::AssetBalance,
4 client::{ExecutionClient, OrderStatusClient},
5 error::{
6 ConnectivityError, NotPlacedReason, OrderError, StreamTerminationReason,
7 UnindexedClientError, UnindexedOrderError,
8 },
9 exchange::mock::request::MockExchangeRequest,
10 fee::{FeeAssetConfig, FeeModelConfig, SpotFeeAsset},
11 fill::{MarketOrderTiming, SimFillConfig, StaleMarketPolicy},
12 margin::SpotMarginConfig,
13 order::{
14 Order, OrderKey, UnindexedInactiveOrder, UnindexedOrderKey,
15 request::{OrderRequestCancel, OrderRequestOpen, UnindexedOrderResponseCancel},
16 state::{Open, OrderState, UnindexedOrderState},
17 },
18 slippage::SlippageConfig,
19 trade::TradesRead,
20};
21use chrono::{DateTime, Utc};
22use derive_more::Constructor;
23use futures::{StreamExt, stream::BoxStream};
24use rustrade_instrument::{
25 asset::name::AssetNameExchange,
26 exchange::ExchangeId,
27 instrument::{kind::InstrumentKindDiscriminant, name::InstrumentNameExchange},
28};
29use serde::{Deserialize, Serialize};
30use tokio::sync::{broadcast, mpsc, oneshot};
31use tokio_stream::wrappers::BroadcastStream;
32use tracing::error;
33
34#[derive(
35 Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Constructor,
36)]
37pub struct MockExecutionConfig {
38 pub mocked_exchange: ExchangeId,
39 pub initial_state: UnindexedAccountSnapshot,
40 pub latency_ms: u64,
41 #[serde(default)]
46 pub fee_model: FeeModelConfig,
47 #[serde(default)]
54 pub fill_model: SimFillConfig,
55 #[serde(default)]
60 pub spot_fee_asset: SpotFeeAsset,
61 #[serde(default)]
68 pub slippage: SlippageConfig,
69 #[serde(default)]
76 pub market_order_timing: MarketOrderTiming,
77 #[serde(default)]
84 pub stale_market: StaleMarketPolicy,
85 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub spot_margin: Option<SpotMarginConfig>,
94 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub fee_asset: Option<FeeAssetConfig>,
101}
102
103#[derive(Debug, Constructor)]
104pub struct MockExecutionClientConfig<FnTime> {
105 pub mocked_exchange: ExchangeId,
106 pub clock: FnTime,
107 pub request_tx: mpsc::UnboundedSender<MockExchangeRequest>,
108 pub event_rx: broadcast::Receiver<UnindexedAccountEvent>,
109}
110
111impl<FnTime> Clone for MockExecutionClientConfig<FnTime>
112where
113 FnTime: Clone,
114{
115 fn clone(&self) -> Self {
116 Self {
117 mocked_exchange: self.mocked_exchange,
118 clock: self.clock.clone(),
119 request_tx: self.request_tx.clone(),
120 event_rx: self.event_rx.resubscribe(),
121 }
122 }
123}
124
125#[derive(Debug, Constructor)]
126pub struct MockExecution<FnTime> {
127 pub mocked_exchange: ExchangeId,
128 pub clock: FnTime,
129 pub request_tx: mpsc::UnboundedSender<MockExchangeRequest>,
130 pub event_rx: broadcast::Receiver<UnindexedAccountEvent>,
131}
132
133impl<FnTime> Clone for MockExecution<FnTime>
134where
135 FnTime: Clone,
136{
137 fn clone(&self) -> Self {
138 Self {
139 mocked_exchange: self.mocked_exchange,
140 clock: self.clock.clone(),
141 request_tx: self.request_tx.clone(),
142 event_rx: self.event_rx.resubscribe(),
143 }
144 }
145}
146
147impl<FnTime> MockExecution<FnTime>
148where
149 FnTime: Fn() -> DateTime<Utc>,
150{
151 pub fn time_request(&self) -> DateTime<Utc> {
152 (self.clock)()
153 }
154}
155
156impl<FnTime> ExecutionClient for MockExecution<FnTime>
157where
158 FnTime: Fn() -> DateTime<Utc> + Clone + Send + Sync,
159{
160 const EXCHANGE: ExchangeId = ExchangeId::Mock;
161
162 const SUPPORTED_KINDS: &'static [InstrumentKindDiscriminant] = &[
167 InstrumentKindDiscriminant::Spot,
168 InstrumentKindDiscriminant::Cfd,
169 ];
170 type Config = MockExecutionClientConfig<FnTime>;
171 type AccountStream = BoxStream<'static, UnindexedAccountEvent>;
172
173 fn new(config: Self::Config) -> Self {
174 Self {
175 mocked_exchange: config.mocked_exchange,
176 clock: config.clock,
177 request_tx: config.request_tx,
178 event_rx: config.event_rx,
179 }
180 }
181
182 async fn account_snapshot(
183 &self,
184 _: &[AssetNameExchange],
185 _: &[InstrumentNameExchange],
186 ) -> Result<UnindexedAccountSnapshot, UnindexedClientError> {
187 let (response_tx, response_rx) = oneshot::channel();
188
189 self.request_tx
190 .send(MockExchangeRequest::fetch_account_snapshot(
191 self.time_request(),
192 response_tx,
193 ))
194 .map_err(|_| {
195 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
196 self.mocked_exchange,
197 ))
198 })?;
199
200 response_rx.await.map_err(|_| {
201 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
202 self.mocked_exchange,
203 ))
204 })
205 }
206
207 async fn account_stream(
208 &self,
209 _: &[AssetNameExchange],
210 _: &[InstrumentNameExchange],
211 ) -> Result<Self::AccountStream, UnindexedClientError> {
212 let exchange = self.mocked_exchange;
218 let events = BroadcastStream::new(self.event_rx.resubscribe()).map(move |result| {
219 result.unwrap_or_else(|error| {
220 error!(
221 ?error,
222 "MockExchange Broadcast AccountStream lagged - terminating"
223 );
224 UnindexedAccountEvent::stream_terminated(
225 exchange,
226 StreamTerminationReason::Error("mock broadcast stream lagged".to_string()),
227 )
228 })
229 });
230 Ok(
231 futures::stream::unfold((events, false), |(mut events, ended)| async move {
232 if ended {
233 return None;
234 }
235 let event = events.next().await?;
236 let ended = matches!(event.kind, AccountEventKind::StreamTerminated(_));
237 Some((event, (events, ended)))
238 })
239 .fuse()
241 .boxed(),
242 )
243 }
244
245 async fn cancel_order(
246 &self,
247 request: OrderRequestCancel<ExchangeId, &InstrumentNameExchange>,
248 ) -> UnindexedOrderResponseCancel {
249 let (response_tx, response_rx) = oneshot::channel();
250
251 let key = OrderKey {
252 exchange: request.key.exchange,
253 instrument: request.key.instrument.clone(),
254 strategy: request.key.strategy.clone(),
255 cid: request.key.cid.clone(),
256 };
257
258 if self
259 .request_tx
260 .send(MockExchangeRequest::cancel_order(
261 self.time_request(),
262 response_tx,
263 request.into_owned_instrument(),
264 ))
265 .is_err()
266 {
267 return UnindexedOrderResponseCancel {
268 key,
269 state: Err(UnindexedOrderError::Connectivity(
270 ConnectivityError::ExchangeOffline(self.mocked_exchange),
271 )),
272 };
273 }
274
275 match response_rx.await {
276 Ok(response) => response,
277 Err(_) => UnindexedOrderResponseCancel {
278 key,
279 state: Err(UnindexedOrderError::Connectivity(
280 ConnectivityError::ExchangeOffline(self.mocked_exchange),
281 )),
282 },
283 }
284 }
285
286 async fn open_order(
287 &self,
288 request: OrderRequestOpen<ExchangeId, &InstrumentNameExchange>,
289 ) -> Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState> {
290 let (response_tx, response_rx) = oneshot::channel();
291
292 let request = request.into_owned_instrument();
293
294 if self
295 .request_tx
296 .send(MockExchangeRequest::open_order(
297 self.time_request(),
298 response_tx,
299 request.clone(),
304 ))
305 .is_err()
306 {
307 return Order {
309 key: request.key,
310 side: request.state.side,
311 price: request.state.price,
312 quantity: request.state.quantity,
313 kind: request.state.kind,
314 time_in_force: request.state.time_in_force,
315 state: OrderState::inactive(OrderError::NotPlaced(NotPlacedReason::NotSent(
316 ConnectivityError::ExchangeOffline(self.mocked_exchange),
317 ))),
318 };
319 }
320
321 match response_rx.await {
322 Ok(response) => response,
323 Err(_) => Order {
324 key: request.key,
325 side: request.state.side,
326 price: request.state.price,
327 quantity: request.state.quantity,
328 kind: request.state.kind,
329 time_in_force: request.state.time_in_force,
330 state: OrderState::inactive(OrderError::Connectivity(
331 ConnectivityError::ExchangeOffline(self.mocked_exchange),
332 )),
333 },
334 }
335 }
336
337 async fn fetch_balances(
338 &self,
339 assets: &[AssetNameExchange],
340 ) -> Result<Vec<AssetBalance<AssetNameExchange>>, UnindexedClientError> {
341 let (response_tx, response_rx) = oneshot::channel();
342
343 self.request_tx
344 .send(MockExchangeRequest::fetch_balances(
345 self.time_request(),
346 assets.to_vec(),
347 response_tx,
348 ))
349 .map_err(|_| {
350 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
351 self.mocked_exchange,
352 ))
353 })?;
354
355 response_rx.await.map_err(|_| {
356 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
357 self.mocked_exchange,
358 ))
359 })
360 }
361
362 async fn fetch_open_orders(
363 &self,
364 instruments: &[InstrumentNameExchange],
365 ) -> Result<Vec<Order<ExchangeId, InstrumentNameExchange, Open>>, UnindexedClientError> {
366 let (response_tx, response_rx) = oneshot::channel();
367
368 self.request_tx
369 .send(MockExchangeRequest::fetch_orders_open(
370 self.time_request(),
371 instruments.to_vec(),
372 response_tx,
373 ))
374 .map_err(|_| {
375 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
376 self.mocked_exchange,
377 ))
378 })?;
379
380 response_rx.await.map_err(|_| {
381 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
382 self.mocked_exchange,
383 ))
384 })
385 }
386
387 async fn fetch_trades(
390 &self,
391 start: DateTime<Utc>,
392 end: DateTime<Utc>,
393 instruments: &[InstrumentNameExchange],
394 ) -> Result<TradesRead<AssetNameExchange, InstrumentNameExchange>, UnindexedClientError> {
395 let (response_tx, response_rx) = oneshot::channel();
396
397 self.request_tx
398 .send(MockExchangeRequest::fetch_trades(
399 self.time_request(),
400 response_tx,
401 start,
402 end,
403 ))
404 .map_err(|_| {
405 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
406 self.mocked_exchange,
407 ))
408 })?;
409
410 let mut trades = response_rx.await.map_err(|_| {
411 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
412 self.mocked_exchange,
413 ))
414 })?;
415 if !instruments.is_empty() {
416 trades.retain(|trade| instruments.contains(&trade.instrument));
417 }
418 Ok(TradesRead::complete(trades))
419 }
420}
421
422impl<FnTime> OrderStatusClient for MockExecution<FnTime>
426where
427 FnTime: Fn() -> DateTime<Utc> + Clone + Send + Sync,
428{
429 async fn fetch_ended_orders(
430 &self,
431 orders: &[UnindexedOrderKey],
432 ) -> Result<Vec<UnindexedInactiveOrder>, UnindexedClientError> {
433 let (response_tx, response_rx) = oneshot::channel();
434
435 self.request_tx
436 .send(MockExchangeRequest::fetch_orders_ended(
437 self.time_request(),
438 orders.to_vec(),
439 response_tx,
440 ))
441 .map_err(|_| {
442 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
443 self.mocked_exchange,
444 ))
445 })?;
446
447 response_rx.await.map_err(|_| {
448 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
449 self.mocked_exchange,
450 ))
451 })
452 }
453}
454
455#[cfg(test)]
456#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
458 use super::*;
459 use crate::{AccountEventKind, balance::Balance};
460 use rust_decimal::Decimal;
461 use rustrade_integration::collection::snapshot::Snapshot;
462
463 fn filler_event() -> UnindexedAccountEvent {
464 UnindexedAccountEvent::new(
467 ExchangeId::Mock,
468 AccountEventKind::BalanceSnapshot(Snapshot::new(AssetBalance {
469 asset: AssetNameExchange::new("btc"),
470 balance: Balance::new(Decimal::ONE, Decimal::ONE),
471 time_exchange: Utc::now(),
472 })),
473 )
474 }
475
476 #[tokio::test]
479 async fn fetch_trades_reads_the_span_for_the_requested_instruments() {
480 use crate::{
481 order::id::{OrderId, StrategyId},
482 trade::{AssetFees, Trade, TradeId},
483 };
484 use rustrade_instrument::Side;
485
486 let (_event_tx, event_rx) = broadcast::channel::<UnindexedAccountEvent>(1);
487 let (request_tx, mut request_rx) = mpsc::unbounded_channel();
488 let client = MockExecution::new(ExchangeId::Mock, Utc::now, request_tx, event_rx);
489 let start = Utc::now() - chrono::TimeDelta::hours(1);
490 let end = Utc::now();
491
492 let trade = move |instrument: &str| {
493 Trade::new(
494 TradeId::new(instrument),
495 OrderId::new("order"),
496 InstrumentNameExchange::new(instrument),
497 StrategyId::unknown(),
498 start,
499 Side::Buy,
500 Decimal::ONE,
501 Decimal::ONE,
502 None,
503 AssetFees::new(AssetNameExchange::new("usdt"), Decimal::ZERO, None),
504 )
505 };
506 let exchange = tokio::spawn(async move {
507 let request = request_rx.recv().await.expect("a request");
508 let crate::exchange::mock::request::MockExchangeRequestKind::FetchTrades {
509 response_tx,
510 start: asked_start,
511 end: asked_end,
512 } = request.kind
513 else {
514 panic!("expected FetchTrades, got {:?}", request.kind);
515 };
516 assert_eq!((asked_start, asked_end), (start, end));
517 response_tx
518 .send(vec![trade("btc_usdt"), trade("eth_usdt")])
519 .expect("the client awaits the answer");
520 });
521
522 let read = client
523 .fetch_trades(start, end, &[InstrumentNameExchange::new("eth_usdt")])
524 .await
525 .unwrap();
526 exchange.await.unwrap();
527 assert_eq!(read, TradesRead::complete(vec![trade("eth_usdt")]));
528 }
529
530 #[tokio::test]
533 async fn account_stream_emits_stream_terminated_on_broadcast_lag() {
534 let (event_tx, event_rx) = broadcast::channel::<UnindexedAccountEvent>(1);
536 let (request_tx, _request_rx) = mpsc::unbounded_channel();
537 let client = MockExecution::new(ExchangeId::Mock, Utc::now, request_tx, event_rx);
540
541 let mut stream = client.account_stream(&[], &[]).await.unwrap();
542
543 for _ in 0..3 {
545 event_tx.send(filler_event()).unwrap();
546 }
547
548 let event = stream.next().await.expect("expected a terminal event");
550 assert!(
551 matches!(
552 event.kind,
553 AccountEventKind::StreamTerminated(StreamTerminationReason::Error(_))
554 ),
555 "expected StreamTerminated(Error) on broadcast lag, got {:?}",
556 event.kind
557 );
558 assert_eq!(event.exchange, ExchangeId::Mock);
559
560 assert!(
562 stream.next().await.is_none(),
563 "stream must end after the terminal event"
564 );
565 }
566
567 #[tokio::test]
571 async fn account_stream_ends_right_after_a_terminal_event_from_the_venue() {
572 let (event_tx, event_rx) = broadcast::channel::<UnindexedAccountEvent>(16);
573 let (request_tx, _request_rx) = mpsc::unbounded_channel();
574 let client = MockExecution::new(ExchangeId::Mock, Utc::now, request_tx, event_rx);
575 let mut stream = client.account_stream(&[], &[]).await.unwrap();
576
577 event_tx.send(filler_event()).unwrap();
578 event_tx
579 .send(UnindexedAccountEvent::stream_terminated(
580 ExchangeId::Mock,
581 StreamTerminationReason::Error("market feed lagged".to_string()),
582 ))
583 .unwrap();
584
585 assert!(matches!(
586 stream.next().await.map(|event| event.kind),
587 Some(AccountEventKind::BalanceSnapshot(_))
588 ));
589 assert!(matches!(
590 stream.next().await.map(|event| event.kind),
591 Some(AccountEventKind::StreamTerminated(_))
592 ));
593 assert_eq!(
594 futures::FutureExt::now_or_never(stream.next()),
595 Some(None),
596 "the stream must end on the poll after its terminal event, with the sender still open"
597 );
598 assert_eq!(
599 futures::FutureExt::now_or_never(stream.next()),
600 Some(None),
601 "and stay ended however often it is polled"
602 );
603 drop(event_tx);
604 }
605}