1use crate::{
2 UnindexedAccountEvent, UnindexedAccountSnapshot,
3 balance::AssetBalance,
4 client::ExecutionClient,
5 error::{
6 ConnectivityError, OrderError, StreamTerminationReason, UnindexedClientError,
7 UnindexedOrderError,
8 },
9 exchange::mock::request::{MarketPrices, MockExchangeRequest},
10 fee::FeeModelConfig,
11 fill::SimFillConfig,
12 order::{
13 Order, OrderEvent, OrderKey,
14 request::{OrderRequestCancel, OrderRequestOpen, UnindexedOrderResponseCancel},
15 state::{Open, OrderState, UnindexedOrderState},
16 },
17 trade::Trade,
18};
19use chrono::{DateTime, Utc};
20use derive_more::Constructor;
21use futures::{StreamExt, stream::BoxStream};
22use rustrade_instrument::{
23 asset::name::AssetNameExchange, exchange::ExchangeId, instrument::name::InstrumentNameExchange,
24};
25use serde::{Deserialize, Serialize};
26use tokio::sync::{broadcast, mpsc, oneshot};
27use tokio_stream::wrappers::BroadcastStream;
28use tracing::error;
29
30#[derive(
31 Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Constructor,
32)]
33pub struct MockExecutionConfig {
34 pub mocked_exchange: ExchangeId,
35 pub initial_state: UnindexedAccountSnapshot,
36 pub latency_ms: u64,
37 #[serde(default)]
42 pub fee_model: FeeModelConfig,
43 #[serde(default)]
51 pub fill_model: SimFillConfig,
52}
53
54#[derive(Debug, Constructor)]
55pub struct MockExecutionClientConfig<FnTime> {
56 pub mocked_exchange: ExchangeId,
57 pub clock: FnTime,
58 pub request_tx: mpsc::UnboundedSender<MockExchangeRequest>,
59 pub event_rx: broadcast::Receiver<UnindexedAccountEvent>,
60}
61
62impl<FnTime> Clone for MockExecutionClientConfig<FnTime>
63where
64 FnTime: Clone,
65{
66 fn clone(&self) -> Self {
67 Self {
68 mocked_exchange: self.mocked_exchange,
69 clock: self.clock.clone(),
70 request_tx: self.request_tx.clone(),
71 event_rx: self.event_rx.resubscribe(),
72 }
73 }
74}
75
76#[derive(Debug, Constructor)]
77pub struct MockExecution<FnTime> {
78 pub mocked_exchange: ExchangeId,
79 pub clock: FnTime,
80 pub request_tx: mpsc::UnboundedSender<MockExchangeRequest>,
81 pub event_rx: broadcast::Receiver<UnindexedAccountEvent>,
82}
83
84impl<FnTime> Clone for MockExecution<FnTime>
85where
86 FnTime: Clone,
87{
88 fn clone(&self) -> Self {
89 Self {
90 mocked_exchange: self.mocked_exchange,
91 clock: self.clock.clone(),
92 request_tx: self.request_tx.clone(),
93 event_rx: self.event_rx.resubscribe(),
94 }
95 }
96}
97
98impl<FnTime> MockExecution<FnTime>
99where
100 FnTime: Fn() -> DateTime<Utc>,
101{
102 pub fn time_request(&self) -> DateTime<Utc> {
103 (self.clock)()
104 }
105}
106
107impl<FnTime> ExecutionClient for MockExecution<FnTime>
108where
109 FnTime: Fn() -> DateTime<Utc> + Clone + Send + Sync,
110{
111 const EXCHANGE: ExchangeId = ExchangeId::Mock;
112 type Config = MockExecutionClientConfig<FnTime>;
113 type AccountStream = BoxStream<'static, UnindexedAccountEvent>;
114
115 fn new(config: Self::Config) -> Self {
116 Self {
117 mocked_exchange: config.mocked_exchange,
118 clock: config.clock,
119 request_tx: config.request_tx,
120 event_rx: config.event_rx,
121 }
122 }
123
124 async fn account_snapshot(
125 &self,
126 _: &[AssetNameExchange],
127 _: &[InstrumentNameExchange],
128 ) -> Result<UnindexedAccountSnapshot, UnindexedClientError> {
129 let (response_tx, response_rx) = oneshot::channel();
130
131 self.request_tx
132 .send(MockExchangeRequest::fetch_account_snapshot(
133 self.time_request(),
134 response_tx,
135 ))
136 .map_err(|_| {
137 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
138 self.mocked_exchange,
139 ))
140 })?;
141
142 response_rx.await.map_err(|_| {
143 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
144 self.mocked_exchange,
145 ))
146 })
147 }
148
149 async fn account_stream(
150 &self,
151 _: &[AssetNameExchange],
152 _: &[InstrumentNameExchange],
153 ) -> Result<Self::AccountStream, UnindexedClientError> {
154 let exchange = self.mocked_exchange;
158 Ok(BroadcastStream::new(self.event_rx.resubscribe())
159 .scan(false, move |done, result| {
160 let item = if *done {
162 None
164 } else {
165 match result {
166 Ok(event) => Some(event),
167 Err(error) => {
168 error!(
169 ?error,
170 "MockExchange Broadcast AccountStream lagged - terminating"
171 );
172 *done = true;
173 Some(UnindexedAccountEvent::stream_terminated(
174 exchange,
175 StreamTerminationReason::Error(
176 "mock broadcast stream lagged".to_string(),
177 ),
178 ))
179 }
180 }
181 };
182 futures::future::ready(item)
183 })
184 .boxed())
185 }
186
187 async fn cancel_order(
188 &self,
189 request: OrderRequestCancel<ExchangeId, &InstrumentNameExchange>,
190 ) -> Option<UnindexedOrderResponseCancel> {
191 let (response_tx, response_rx) = oneshot::channel();
192
193 let key = OrderKey {
194 exchange: request.key.exchange,
195 instrument: request.key.instrument.clone(),
196 strategy: request.key.strategy.clone(),
197 cid: request.key.cid.clone(),
198 };
199
200 if self
201 .request_tx
202 .send(MockExchangeRequest::cancel_order(
203 self.time_request(),
204 response_tx,
205 into_owned_request(request),
206 ))
207 .is_err()
208 {
209 return Some(UnindexedOrderResponseCancel {
210 key,
211 state: Err(UnindexedOrderError::Connectivity(
212 ConnectivityError::ExchangeOffline(self.mocked_exchange),
213 )),
214 });
215 }
216
217 Some(match response_rx.await {
218 Ok(response) => response,
219 Err(_) => UnindexedOrderResponseCancel {
220 key,
221 state: Err(UnindexedOrderError::Connectivity(
222 ConnectivityError::ExchangeOffline(self.mocked_exchange),
223 )),
224 },
225 })
226 }
227
228 async fn open_order(
229 &self,
230 request: OrderRequestOpen<ExchangeId, &InstrumentNameExchange>,
231 ) -> Option<Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>> {
232 let (response_tx, response_rx) = oneshot::channel();
233
234 let request = into_owned_request(request);
235
236 if self
237 .request_tx
238 .send(MockExchangeRequest::open_order(
239 self.time_request(),
240 response_tx,
241 request.clone(),
242 MarketPrices::default(), ))
244 .is_err()
245 {
246 return Some(Order {
247 key: request.key,
248 side: request.state.side,
249 price: request.state.price,
250 quantity: request.state.quantity,
251 kind: request.state.kind,
252 time_in_force: request.state.time_in_force,
253 state: OrderState::inactive(OrderError::Connectivity(
254 ConnectivityError::ExchangeOffline(self.mocked_exchange),
255 )),
256 });
257 }
258
259 Some(match response_rx.await {
260 Ok(response) => response,
261 Err(_) => Order {
262 key: request.key,
263 side: request.state.side,
264 price: request.state.price,
265 quantity: request.state.quantity,
266 kind: request.state.kind,
267 time_in_force: request.state.time_in_force,
268 state: OrderState::inactive(OrderError::Connectivity(
269 ConnectivityError::ExchangeOffline(self.mocked_exchange),
270 )),
271 },
272 })
273 }
274
275 async fn fetch_balances(
276 &self,
277 assets: &[AssetNameExchange],
278 ) -> Result<Vec<AssetBalance<AssetNameExchange>>, UnindexedClientError> {
279 let (response_tx, response_rx) = oneshot::channel();
280
281 self.request_tx
282 .send(MockExchangeRequest::fetch_balances(
283 self.time_request(),
284 assets.to_vec(),
285 response_tx,
286 ))
287 .map_err(|_| {
288 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
289 self.mocked_exchange,
290 ))
291 })?;
292
293 response_rx.await.map_err(|_| {
294 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
295 self.mocked_exchange,
296 ))
297 })
298 }
299
300 async fn fetch_open_orders(
301 &self,
302 instruments: &[InstrumentNameExchange],
303 ) -> Result<Vec<Order<ExchangeId, InstrumentNameExchange, Open>>, UnindexedClientError> {
304 let (response_tx, response_rx) = oneshot::channel();
305
306 self.request_tx
307 .send(MockExchangeRequest::fetch_orders_open(
308 self.time_request(),
309 instruments.to_vec(),
310 response_tx,
311 ))
312 .map_err(|_| {
313 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
314 self.mocked_exchange,
315 ))
316 })?;
317
318 response_rx.await.map_err(|_| {
319 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
320 self.mocked_exchange,
321 ))
322 })
323 }
324
325 async fn fetch_trades(
326 &self,
327 time_since: DateTime<Utc>,
328 _instruments: &[InstrumentNameExchange],
330 ) -> Result<Vec<Trade<AssetNameExchange, InstrumentNameExchange>>, UnindexedClientError> {
331 let (response_tx, response_rx) = oneshot::channel();
332
333 self.request_tx
334 .send(MockExchangeRequest::fetch_trades(
335 self.time_request(),
336 response_tx,
337 time_since,
338 ))
339 .map_err(|_| {
340 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
341 self.mocked_exchange,
342 ))
343 })?;
344
345 response_rx.await.map_err(|_| {
346 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
347 self.mocked_exchange,
348 ))
349 })
350 }
351}
352
353fn into_owned_request<Kind>(
354 request: OrderEvent<Kind, ExchangeId, &InstrumentNameExchange>,
355) -> OrderEvent<Kind, ExchangeId, InstrumentNameExchange> {
356 let OrderEvent {
357 key:
358 OrderKey {
359 exchange,
360 instrument,
361 strategy,
362 cid,
363 },
364 state,
365 } = request;
366
367 OrderEvent {
368 key: OrderKey {
369 exchange,
370 instrument: instrument.clone(),
371 strategy,
372 cid,
373 },
374 state,
375 }
376}
377
378#[cfg(test)]
379#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
381 use super::*;
382 use crate::{AccountEventKind, balance::Balance};
383 use rust_decimal::Decimal;
384 use rustrade_integration::collection::snapshot::Snapshot;
385
386 fn filler_event() -> UnindexedAccountEvent {
387 UnindexedAccountEvent::new(
390 ExchangeId::Mock,
391 AccountEventKind::BalanceSnapshot(Snapshot::new(AssetBalance {
392 asset: AssetNameExchange::new("btc"),
393 balance: Balance::new(Decimal::ONE, Decimal::ONE),
394 time_exchange: Utc::now(),
395 })),
396 )
397 }
398
399 #[tokio::test]
402 async fn account_stream_emits_stream_terminated_on_broadcast_lag() {
403 let (event_tx, event_rx) = broadcast::channel::<UnindexedAccountEvent>(1);
405 let (request_tx, _request_rx) = mpsc::unbounded_channel();
406 let client = MockExecution::new(ExchangeId::Mock, Utc::now, request_tx, event_rx);
409
410 let mut stream = client.account_stream(&[], &[]).await.unwrap();
411
412 for _ in 0..3 {
414 event_tx.send(filler_event()).unwrap();
415 }
416
417 let event = stream.next().await.expect("expected a terminal event");
419 assert!(
420 matches!(
421 event.kind,
422 AccountEventKind::StreamTerminated(StreamTerminationReason::Error(_))
423 ),
424 "expected StreamTerminated(Error) on broadcast lag, got {:?}",
425 event.kind
426 );
427 assert_eq!(event.exchange, ExchangeId::Mock);
428
429 assert!(
431 stream.next().await.is_none(),
432 "stream must end after the terminal event"
433 );
434 }
435}