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::MockExchangeRequest,
10 fee::FeeModelConfig,
11 fill::SimFillConfig,
12 order::{
13 Order, 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,
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 ) -> Option<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 Some(UnindexedOrderResponseCancel {
220 key,
221 state: Err(UnindexedOrderError::Connectivity(
222 ConnectivityError::ExchangeOffline(self.mocked_exchange),
223 )),
224 });
225 }
226
227 Some(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 ) -> Option<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 Some(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 Some(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(
339 &self,
340 time_since: DateTime<Utc>,
341 _instruments: &[InstrumentNameExchange],
343 ) -> Result<Vec<Trade<AssetNameExchange, InstrumentNameExchange>>, UnindexedClientError> {
344 let (response_tx, response_rx) = oneshot::channel();
345
346 self.request_tx
347 .send(MockExchangeRequest::fetch_trades(
348 self.time_request(),
349 response_tx,
350 time_since,
351 ))
352 .map_err(|_| {
353 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
354 self.mocked_exchange,
355 ))
356 })?;
357
358 response_rx.await.map_err(|_| {
359 UnindexedClientError::Connectivity(ConnectivityError::ExchangeOffline(
360 self.mocked_exchange,
361 ))
362 })
363 }
364}
365
366#[cfg(test)]
367#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
369 use super::*;
370 use crate::{AccountEventKind, balance::Balance};
371 use rust_decimal::Decimal;
372 use rustrade_integration::collection::snapshot::Snapshot;
373
374 fn filler_event() -> UnindexedAccountEvent {
375 UnindexedAccountEvent::new(
378 ExchangeId::Mock,
379 AccountEventKind::BalanceSnapshot(Snapshot::new(AssetBalance {
380 asset: AssetNameExchange::new("btc"),
381 balance: Balance::new(Decimal::ONE, Decimal::ONE),
382 time_exchange: Utc::now(),
383 })),
384 )
385 }
386
387 #[tokio::test]
390 async fn account_stream_emits_stream_terminated_on_broadcast_lag() {
391 let (event_tx, event_rx) = broadcast::channel::<UnindexedAccountEvent>(1);
393 let (request_tx, _request_rx) = mpsc::unbounded_channel();
394 let client = MockExecution::new(ExchangeId::Mock, Utc::now, request_tx, event_rx);
397
398 let mut stream = client.account_stream(&[], &[]).await.unwrap();
399
400 for _ in 0..3 {
402 event_tx.send(filler_event()).unwrap();
403 }
404
405 let event = stream.next().await.expect("expected a terminal event");
407 assert!(
408 matches!(
409 event.kind,
410 AccountEventKind::StreamTerminated(StreamTerminationReason::Error(_))
411 ),
412 "expected StreamTerminated(Error) on broadcast lag, got {:?}",
413 event.kind
414 );
415 assert_eq!(event.exchange, ExchangeId::Mock);
416
417 assert!(
419 stream.next().await.is_none(),
420 "stream must end after the terminal event"
421 );
422 }
423}