1use std::{
19 future::Future,
20 time::{Duration, Instant},
21};
22
23use nautilus_common::enums::LogLevel;
24use nautilus_live::{ExecutionClientCore, task::TaskGroup};
25use nautilus_model::{
26 events::OrderEventAny,
27 identifiers::AccountId,
28 orders::{Order, OrderAny},
29};
30
31pub(crate) fn active_orders(core: &ExecutionClientCore) -> Vec<OrderAny> {
32 let cache = core.cache();
33 cache
34 .orders_refs(Some(&core.venue), None, None, Some(&core.account_id), None)
35 .into_iter()
36 .filter(|order| {
37 (order.is_open() || order.is_inflight())
38 && !order.strategy_id().is_external()
39 && cache.client_id(&order.client_order_id()) == Some(&core.client_id)
40 && (order.ts_submitted().is_some()
41 || order.events().iter().any(|event| {
42 matches!(event, OrderEventAny::Accepted(accepted) if !accepted.reconciliation)
43 }))
44 })
45 .map(|order| order.cloned())
46 .collect()
47}
48
49pub(crate) fn log_report_receipt(count: usize, report_type: &str, level: LogLevel) {
50 let level = match level {
51 LogLevel::Off => return,
52 LogLevel::Trace => log::Level::Trace,
53 LogLevel::Debug => log::Level::Debug,
54 LogLevel::Info => log::Level::Info,
55 LogLevel::Warning => log::Level::Warn,
56 LogLevel::Error => log::Level::Error,
57 };
58 let plural = if count == 1 { "" } else { "s" };
59 log::log!(level, "Received {count} {report_type}{plural}");
60}
61
62pub fn spawn_task<F>(pending_tasks: &TaskGroup, description: &'static str, fut: F)
64where
65 F: Future<Output = anyhow::Result<()>> + Send + 'static,
66{
67 let future = async move {
68 if let Err(e) = fut.await {
69 log::warn!("{description} failed: {e}");
70 }
71 };
72
73 if let Err(e) = pending_tasks.spawn(future) {
74 log::warn!("Skipping Binance {description} after shutdown began: {e}");
75 }
76}
77
78pub fn abort_pending_tasks(pending_tasks: &TaskGroup) {
80 pending_tasks.begin_shutdown();
81}
82
83pub async fn await_pending_tasks(pending_tasks: &TaskGroup) -> anyhow::Result<()> {
89 pending_tasks.begin_shutdown();
90 pending_tasks
91 .finish_shutdown(Duration::from_secs(1), Duration::from_secs(2))
92 .await
93 .map_err(|e| anyhow::anyhow!("Failed to terminate Binance execution tasks: {e}"))?;
94 Ok(())
95}
96
97pub async fn await_account_registered(
107 core: &ExecutionClientCore,
108 account_id: AccountId,
109 timeout_secs: f64,
110) -> anyhow::Result<()> {
111 if core.cache().account(&account_id).is_some() {
112 log::info!("Account {account_id} registered");
113 return Ok(());
114 }
115
116 let start = Instant::now();
117 let timeout = Duration::from_secs_f64(timeout_secs);
118 let interval = Duration::from_millis(10);
119
120 loop {
121 tokio::time::sleep(interval).await;
122
123 if core.cache().account(&account_id).is_some() {
124 log::info!("Account {account_id} registered");
125 return Ok(());
126 }
127
128 if start.elapsed() >= timeout {
129 anyhow::bail!(
130 "Timeout waiting for account {account_id} to be registered after {timeout_secs}s"
131 );
132 }
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use std::{cell::RefCell, rc::Rc};
139
140 use nautilus_common::cache::Cache;
141 use nautilus_live::ExecutionClientCore;
142 use nautilus_model::{
143 accounts::{AccountAny, CashAccount},
144 enums::{AccountType, OmsType, OrderType},
145 events::AccountState,
146 identifiers::{AccountId, TraderId},
147 orders::{OrderTestBuilder, stubs::TestOrderEventStubs},
148 types::{AccountBalance, Money, Price, Quantity},
149 };
150 use rstest::rstest;
151
152 use super::*;
153 use crate::common::consts::{BINANCE_CLIENT_ID, BINANCE_VENUE};
154
155 #[rstest]
156 #[case::accepted(
157 "BINANCE-001",
158 "BINANCE",
159 "OWNED-002",
160 "BTCUSDT.BINANCE",
161 (true, true, false),
162 false,
163 1
164 )]
165 #[case::submitted(
166 "BINANCE-001",
167 "BINANCE",
168 "OWNED-002",
169 "BTCUSDT.BINANCE",
170 (true, false, false),
171 false,
172 1
173 )]
174 #[case::other_account(
175 "BINANCE-002",
176 "BINANCE",
177 "OWNED-002",
178 "BTCUSDT.BINANCE",
179 (true, true, false),
180 false,
181 0
182 )]
183 #[case::other_client(
184 "BINANCE-001",
185 "OTHER",
186 "OWNED-002",
187 "BTCUSDT.BINANCE",
188 (true, true, false),
189 false,
190 0
191 )]
192 #[case::unbound(
193 "BINANCE-001",
194 "",
195 "OWNED-002",
196 "BTCUSDT.BINANCE",
197 (true, true, false),
198 false,
199 0
200 )]
201 #[case::external(
202 "BINANCE-001",
203 "BINANCE",
204 "EXTERNAL",
205 "BTCUSDT.BINANCE",
206 (true, true, false),
207 false,
208 0
209 )]
210 #[case::other_venue(
211 "BINANCE-001",
212 "BINANCE",
213 "OWNED-002",
214 "BTCUSDT.OTHER",
215 (true, true, false),
216 false,
217 0
218 )]
219 #[case::initialized(
220 "BINANCE-001",
221 "BINANCE",
222 "OWNED-002",
223 "BTCUSDT.BINANCE",
224 (false, false, false),
225 false,
226 0
227 )]
228 #[case::closed(
229 "BINANCE-001",
230 "BINANCE",
231 "OWNED-002",
232 "BTCUSDT.BINANCE",
233 (true, true, true),
234 false,
235 0
236 )]
237 #[case::claimed_external(
238 "BINANCE-001",
239 "BINANCE",
240 "OWNED-002",
241 "BTCUSDT.BINANCE",
242 (false, true, false),
243 true,
244 0
245 )]
246 #[case::reconciled_local(
247 "BINANCE-001",
248 "BINANCE",
249 "OWNED-002",
250 "BTCUSDT.BINANCE",
251 (true, true, false),
252 true,
253 1
254 )]
255 fn test_active_orders_selects_only_retained_local_owners(
256 #[case] account: &str,
257 #[case] client: &str,
258 #[case] strategy: &str,
259 #[case] instrument: &str,
260 #[case] lifecycle: (bool, bool, bool),
261 #[case] reconciled: bool,
262 #[case] expected_count: usize,
263 ) {
264 let (submitted, accepted, closed) = lifecycle;
265 let cache = Rc::new(RefCell::new(Cache::default()));
266 let core = create_test_core(cache.clone(), AccountId::from("BINANCE-001"));
267 let order = OrderTestBuilder::new(OrderType::Limit)
268 .strategy_id(strategy.into())
269 .instrument_id(instrument.into())
270 .quantity(Quantity::from("0.002"))
271 .price(Price::from("45678.90"))
272 .build();
273 let cid = order.client_order_id();
274 cache
275 .borrow_mut()
276 .add_order(
277 order.clone(),
278 None,
279 (!client.is_empty()).then(|| client.into()),
280 false,
281 )
282 .unwrap();
283
284 if submitted {
285 cache
286 .borrow_mut()
287 .update_order(&TestOrderEventStubs::submitted(&order, account.into()))
288 .unwrap();
289 }
290
291 if accepted {
292 let OrderEventAny::Accepted(mut event) =
293 TestOrderEventStubs::accepted(&order, account.into(), "123".into())
294 else {
295 panic!("Expected acceptance stub")
296 };
297
298 event.reconciliation = reconciled;
299 cache
300 .borrow_mut()
301 .update_order(&OrderEventAny::Accepted(event))
302 .unwrap();
303 }
304
305 if closed {
306 cache
307 .borrow_mut()
308 .update_order(&TestOrderEventStubs::canceled(
309 &order,
310 account.into(),
311 Some("123".into()),
312 ))
313 .unwrap();
314 }
315
316 let selected = active_orders(&core);
317 assert_eq!(selected.len(), expected_count);
318
319 if expected_count == 1 {
320 assert_eq!(selected[0], cache.borrow().order_owned(&cid).unwrap());
321 }
322 }
323
324 #[rstest]
325 #[tokio::test]
326 async fn test_spawn_task_unregisters_finished_task_before_shutdown() {
327 let pending_tasks = TaskGroup::new();
328
329 spawn_task(&pending_tasks, "test task", async { Ok(()) });
330 tokio::time::timeout(Duration::from_secs(1), async {
331 while !pending_tasks.all_finished() {
332 tokio::task::yield_now().await;
333 }
334 })
335 .await
336 .expect("task should finish");
337
338 assert!(pending_tasks.is_empty());
339 abort_pending_tasks(&pending_tasks);
340 await_pending_tasks(&pending_tasks)
341 .await
342 .expect("task shutdown");
343 assert!(pending_tasks.is_empty());
344 }
345
346 #[rstest]
347 #[tokio::test]
348 async fn test_abort_pending_tasks_aborts_running_tasks() {
349 let (drop_tx, drop_rx) = tokio::sync::oneshot::channel();
350 let guard = AbortDropSignal { tx: Some(drop_tx) };
351
352 let pending_tasks = TaskGroup::new();
353 pending_tasks
354 .spawn(async move {
355 let _guard = guard;
356 tokio::time::sleep(Duration::from_secs(60)).await;
357 })
358 .expect("task spawn");
359
360 abort_pending_tasks(&pending_tasks);
361 await_pending_tasks(&pending_tasks)
362 .await
363 .expect("task shutdown");
364
365 assert!(pending_tasks.is_empty());
366 tokio::time::timeout(Duration::from_secs(1), drop_rx)
367 .await
368 .expect("Aborted task should drop its future")
369 .expect("Drop signal should be sent");
370 }
371
372 #[rstest]
373 #[tokio::test]
374 async fn test_await_account_registered_returns_when_account_is_added() {
375 let account_id = AccountId::from("BINANCE-001");
376 let cache = Rc::new(RefCell::new(Cache::default()));
377 let core = create_test_core(cache.clone(), account_id);
378
379 let wait_fut = await_account_registered(&core, account_id, 0.5);
380 let register_fut = async move {
381 tokio::time::sleep(Duration::from_millis(20)).await;
382 add_test_account_to_cache(&cache, account_id);
383 };
384
385 let (result, ()) = tokio::join!(wait_fut, register_fut);
386 result.unwrap();
387 }
388
389 #[rstest]
390 #[tokio::test]
391 async fn test_await_account_registered_times_out() {
392 let account_id = AccountId::from("BINANCE-001");
393 let cache = Rc::new(RefCell::new(Cache::default()));
394 let core = create_test_core(cache, account_id);
395
396 let error = await_account_registered(&core, account_id, 0.02)
397 .await
398 .expect_err("Missing account should time out");
399
400 assert!(error.to_string().contains("BINANCE-001"));
401 }
402
403 struct AbortDropSignal {
404 tx: Option<tokio::sync::oneshot::Sender<()>>,
405 }
406
407 impl Drop for AbortDropSignal {
408 fn drop(&mut self) {
409 if let Some(tx) = self.tx.take() {
410 let _ = tx.send(());
411 }
412 }
413 }
414
415 fn create_test_core(cache: Rc<RefCell<Cache>>, account_id: AccountId) -> ExecutionClientCore {
416 ExecutionClientCore::new(
417 TraderId::from("TESTER-001"),
418 *BINANCE_CLIENT_ID,
419 *BINANCE_VENUE,
420 OmsType::Hedging,
421 account_id,
422 AccountType::Cash,
423 None,
424 cache,
425 )
426 }
427
428 fn add_test_account_to_cache(cache: &Rc<RefCell<Cache>>, account_id: AccountId) {
429 let state = AccountState::new(
430 account_id,
431 AccountType::Cash,
432 vec![AccountBalance::new(
433 Money::from("1.0 BTC"),
434 Money::from("0 BTC"),
435 Money::from("1.0 BTC"),
436 )],
437 vec![],
438 true,
439 nautilus_core::UUID4::new(),
440 nautilus_core::UnixNanos::default(),
441 nautilus_core::UnixNanos::default(),
442 None,
443 );
444
445 let account = AccountAny::Cash(CashAccount::new(state, true, false));
446 cache.borrow_mut().add_account(account).unwrap();
447 }
448}