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