1use std::sync::{
19 Arc, Mutex,
20 atomic::{AtomicBool, Ordering},
21};
22
23use ahash::{AHashMap, AHashSet};
24use async_trait::async_trait;
25use nautilus_common::{
26 clients::DataClient,
27 live::{get_runtime, runner::get_data_event_sender},
28 messages::{
29 DataEvent,
30 data::{
31 SubscribeBookDeltas, SubscribeInstrumentClose, SubscribeInstrumentStatus,
32 SubscribeTrades, UnsubscribeBars, UnsubscribeBookDeltas, UnsubscribeCustomData,
33 UnsubscribeInstrument, UnsubscribeInstrumentClose, UnsubscribeInstrumentStatus,
34 UnsubscribeInstruments, UnsubscribeQuotes, UnsubscribeTrades,
35 },
36 },
37 providers::InstrumentProvider,
38};
39use nautilus_core::{AtomicMap, Params};
40use nautilus_model::{
41 data::{
42 CustomData, CustomDataTrait, Data, DataType, OrderBookDeltas, OrderBookDeltas_API,
43 TradeTick,
44 },
45 identifiers::{ClientId, InstrumentId, TradeId, Venue},
46 instruments::{Instrument, InstrumentAny},
47 types::{Currency, Money},
48};
49use nautilus_network::socket::TcpMessageHandler;
50use rust_decimal::Decimal;
51use tokio::task::JoinHandle;
52
53use crate::{
54 common::{
55 consts::{BETFAIR_RACE_STREAM_HOST, BETFAIR_VENUE},
56 credential::BetfairCredential,
57 enums::{MarketDataFilterField, MarketStatus},
58 parse::{
59 extract_market_id, make_instrument_id, parse_betfair_price, parse_betfair_quantity,
60 parse_market_definition, parse_millis_timestamp,
61 },
62 },
63 config::BetfairDataConfig,
64 data_types::{BetfairSequenceCompleted, register_betfair_custom_data},
65 http::client::BetfairHttpClient,
66 provider::{BetfairInstrumentProvider, NavigationFilter},
67 stream::{
68 client::{BetfairRaceStreamClient, BetfairStreamClient},
69 config::BetfairStreamConfig,
70 messages::{MarketDataFilter, StreamMarketFilter, StreamMessage, stream_decode},
71 parse::{
72 make_trade_tick, parse_betfair_starting_prices, parse_betfair_ticker,
73 parse_bsp_book_deltas, parse_cricket_match, parse_instrument_closes,
74 parse_instrument_statuses, parse_race_progress, parse_race_runner_data,
75 parse_runner_book_deltas,
76 },
77 },
78};
79
80const KEEP_ALIVE_INTERVAL_SECS: u64 = 36_000;
82
83pub(crate) fn custom_data_with_instrument(
86 value: Arc<dyn CustomDataTrait>,
87 instrument_id: InstrumentId,
88) -> CustomData {
89 let mut metadata = Params::new();
90 metadata.insert(
91 "instrument_id".to_string(),
92 serde_json::Value::String(instrument_id.to_string()),
93 );
94 let data_type = DataType::new(
95 value.type_name(),
96 Some(metadata),
97 Some(instrument_id.to_string()),
98 );
99 CustomData::new(value, data_type)
100}
101
102#[derive(Debug)]
104pub struct BetfairDataClient {
105 client_id: ClientId,
106 http_client: Arc<BetfairHttpClient>,
107 provider: BetfairInstrumentProvider,
108 stream_client: Option<Arc<BetfairStreamClient>>,
109 race_stream_client: Option<Arc<BetfairRaceStreamClient>>,
110 cricket_stream_client: Option<Arc<BetfairRaceStreamClient>>,
111 credential: BetfairCredential,
112 stream_config: BetfairStreamConfig,
113 config: BetfairDataConfig,
114 currency: Currency,
115 is_connected: AtomicBool,
116 data_sender: tokio::sync::mpsc::UnboundedSender<DataEvent>,
117 instruments: Arc<AtomicMap<InstrumentId, InstrumentAny>>,
118 subscribed_market_ids: AHashSet<String>,
119 keep_alive_handle: Option<JoinHandle<()>>,
120 reconnect_handle: Option<JoinHandle<()>>,
121 race_fatal_handle: Option<JoinHandle<()>>,
122 cricket_fatal_handle: Option<JoinHandle<()>>,
123}
124
125impl BetfairDataClient {
126 #[must_use]
128 #[expect(clippy::too_many_arguments)]
129 pub fn new(
130 client_id: ClientId,
131 http_client: BetfairHttpClient,
132 credential: BetfairCredential,
133 stream_config: BetfairStreamConfig,
134 config: BetfairDataConfig,
135 nav_filter: NavigationFilter,
136 currency: Currency,
137 min_notional: Option<Money>,
138 ) -> Self {
139 let data_sender = get_data_event_sender();
140 let http_client = Arc::new(http_client);
141 let provider = BetfairInstrumentProvider::new(
142 Arc::clone(&http_client),
143 nav_filter,
144 currency,
145 min_notional,
146 );
147
148 Self {
149 client_id,
150 http_client,
151 provider,
152 stream_client: None,
153 race_stream_client: None,
154 cricket_stream_client: None,
155 credential,
156 stream_config,
157 config,
158 currency,
159 is_connected: AtomicBool::new(false),
160 data_sender,
161 instruments: Arc::new(AtomicMap::new()),
162 subscribed_market_ids: AHashSet::new(),
163 keep_alive_handle: None,
164 reconnect_handle: None,
165 race_fatal_handle: None,
166 cricket_fatal_handle: None,
167 }
168 }
169
170 fn create_stream_handler(
171 data_sender: tokio::sync::mpsc::UnboundedSender<DataEvent>,
172 instruments: Arc<AtomicMap<InstrumentId, InstrumentAny>>,
173 currency: Currency,
174 min_notional: Option<Money>,
175 reconnect_tx: tokio::sync::mpsc::UnboundedSender<()>,
176 ) -> TcpMessageHandler {
177 let traded_volumes: Arc<Mutex<AHashMap<(InstrumentId, Decimal), Decimal>>> =
180 Arc::new(Mutex::new(AHashMap::new()));
181 let has_initial_connection = Arc::new(AtomicBool::new(false));
182
183 Arc::new(move |data: &[u8]| {
184 let msg = match stream_decode(data) {
185 Ok(msg) => msg,
186 Err(e) => {
187 log::warn!("Failed to decode stream message: {e}");
188 return;
189 }
190 };
191
192 match msg {
193 StreamMessage::MarketChange(mcm) => {
194 if mcm.is_heartbeat() {
195 return;
196 }
197
198 let Some(market_changes) = &mcm.mc else {
199 return;
200 };
201
202 let ts_event = parse_millis_timestamp(mcm.pt);
203 let ts_init = ts_event;
204
205 for mc in market_changes {
206 let is_snapshot = mc.img;
207 let mut market_closed = false;
208
209 if let Some(def) = &mc.market_definition {
210 match parse_market_definition(
214 &mc.id,
215 def,
216 currency,
217 ts_init,
218 min_notional,
219 ) {
220 Ok(new_instruments) => {
221 instruments.rcu(|m| {
222 for inst in &new_instruments {
223 m.insert(inst.id(), inst.clone());
224 }
225 });
226
227 for inst in new_instruments {
228 if let Err(e) =
229 data_sender.send(DataEvent::Instrument(inst))
230 {
231 log::warn!("Failed to send instrument: {e}");
232 }
233 }
234 }
235 Err(e) => {
236 log::warn!(
237 "Failed to parse market definition for {}: {e}",
238 mc.id
239 );
240 }
241 }
242
243 if let Some(status) = &def.status {
244 market_closed = *status == MarketStatus::Closed;
245
246 for event in
247 parse_instrument_statuses(&mc.id, def, ts_event, ts_init)
248 {
249 if let Err(e) =
250 data_sender.send(DataEvent::InstrumentStatus(event))
251 {
252 log::warn!("Failed to send instrument status: {e}");
253 }
254 }
255 }
256
257 for sp in parse_betfair_starting_prices(&mc.id, def, ts_event, ts_init)
258 {
259 let instrument_id = sp.instrument_id;
260 let custom =
261 custom_data_with_instrument(Arc::new(sp), instrument_id);
262
263 if let Err(e) =
264 data_sender.send(DataEvent::Data(Data::Custom(custom)))
265 {
266 log::warn!("Failed to send starting price: {e}");
267 }
268 }
269
270 for close in parse_instrument_closes(&mc.id, def, ts_event, ts_init) {
271 if let Err(e) =
272 data_sender.send(DataEvent::Data(Data::InstrumentClose(close)))
273 {
274 log::warn!("Failed to send instrument close: {e}");
275 }
276 }
277 }
278
279 let mut buffered_deltas: Vec<OrderBookDeltas> = Vec::new();
283 let mut buffered_bsp_customs: Vec<CustomData> = Vec::new();
284
285 if let Some(runner_changes) = &mc.rc {
286 for rc in runner_changes {
287 let handicap = rc.hc.unwrap_or(Decimal::ZERO);
288 let instrument_id = make_instrument_id(&mc.id, rc.id, handicap);
289
290 match parse_runner_book_deltas(
291 instrument_id,
292 rc,
293 is_snapshot,
294 mcm.pt,
295 ts_event,
296 ts_init,
297 ) {
298 Ok(Some(deltas)) => {
299 if is_snapshot {
300 if let Err(e) = data_sender.send(DataEvent::Data(
301 Data::Deltas(OrderBookDeltas_API::new(deltas)),
302 )) {
303 log::warn!("Failed to send book deltas: {e}");
304 }
305 } else {
306 buffered_deltas.push(deltas);
307 }
308 }
309 Ok(None) => {}
310 Err(e) => {
311 log::warn!(
312 "Failed to parse book deltas for {instrument_id}: {e}"
313 );
314 }
315 }
316
317 if let Some(trades) = &rc.trd {
318 let mut volumes = traded_volumes.lock().unwrap();
319
320 for pv in trades {
321 if pv.volume == Decimal::ZERO {
322 continue;
323 }
324
325 let key = (instrument_id, pv.price);
326 let prev_volume =
327 volumes.get(&key).copied().unwrap_or(Decimal::ZERO);
328
329 if pv.volume <= prev_volume {
330 volumes.insert(key, pv.volume);
331 continue;
332 }
333
334 let trade_volume = pv.volume - prev_volume;
335 volumes.insert(key, pv.volume);
336
337 let price = match parse_betfair_price(pv.price) {
338 Ok(p) => p,
339 Err(e) => {
340 log::warn!("Invalid trade price: {e}");
341 continue;
342 }
343 };
344 let size = match parse_betfair_quantity(trade_volume) {
345 Ok(q) => q,
346 Err(e) => {
347 log::warn!("Invalid trade size: {e}");
348 continue;
349 }
350 };
351 let trade_id = TradeId::new(format!(
352 "{}-{}-{}",
353 mcm.pt, rc.id, pv.price
354 ));
355 let tick: TradeTick = make_trade_tick(
356 instrument_id,
357 price,
358 size,
359 trade_id,
360 ts_event,
361 ts_init,
362 );
363
364 if let Err(e) =
365 data_sender.send(DataEvent::Data(Data::Trade(tick)))
366 {
367 log::warn!("Failed to send trade tick: {e}");
368 }
369 }
370 }
371
372 if let Some(ticker) =
373 parse_betfair_ticker(instrument_id, rc, ts_event, ts_init)
374 {
375 let custom = custom_data_with_instrument(
376 Arc::new(ticker),
377 instrument_id,
378 );
379
380 if let Err(e) =
381 data_sender.send(DataEvent::Data(Data::Custom(custom)))
382 {
383 log::warn!("Failed to send ticker: {e}");
384 }
385 }
386
387 for bsp_delta in
388 parse_bsp_book_deltas(instrument_id, rc, ts_event, ts_init)
389 {
390 buffered_bsp_customs.push(custom_data_with_instrument(
391 Arc::new(bsp_delta),
392 instrument_id,
393 ));
394 }
395 }
396 }
397
398 for deltas in buffered_deltas {
399 if let Err(e) = data_sender.send(DataEvent::Data(Data::Deltas(
400 OrderBookDeltas_API::new(deltas),
401 ))) {
402 log::warn!("Failed to send book deltas: {e}");
403 }
404 }
405
406 for custom in buffered_bsp_customs {
407 if let Err(e) = data_sender.send(DataEvent::Data(Data::Custom(custom)))
408 {
409 log::warn!("Failed to send BSP book delta: {e}");
410 }
411 }
412
413 if market_closed {
414 let prefix = format!("{}-", mc.id);
415
416 if let Ok(mut volumes) = traded_volumes.lock() {
417 volumes.retain(|k, _| !k.0.symbol.as_str().starts_with(&prefix));
418 }
419 }
420 }
421
422 let completed = BetfairSequenceCompleted::new(ts_event, ts_init);
423 let custom = CustomData::from_arc(Arc::new(completed));
424 if let Err(e) = data_sender.send(DataEvent::Data(Data::Custom(custom))) {
425 log::warn!("Failed to send sequence completed: {e}");
426 }
427 }
428 StreamMessage::Connection(_) => {
429 if has_initial_connection.swap(true, Ordering::SeqCst) {
430 log::info!("Betfair data stream reconnected");
431 let _ = reconnect_tx.send(());
432 } else {
433 log::debug!("Betfair data stream connected");
434 }
435 }
436 StreamMessage::Status(status) => {
437 if status.connection_closed {
438 log::warn!(
439 "Betfair stream closed: {:?} - {:?}",
440 status.error_code,
441 status.error_message,
442 );
443 }
444 }
445 StreamMessage::RaceChange(rcm) => {
446 if let Some(race_changes) = &rcm.rc {
447 let ts_init = parse_millis_timestamp(rcm.pt);
448
449 for rc in race_changes {
450 let race_id = rc.id.as_deref().unwrap_or("");
451 let market_id = rc.mid.as_deref().unwrap_or("");
452
453 if let Some(runners) = &rc.rrc {
454 for rrc in runners {
455 let ts_event = rrc.ft.map_or(ts_init, parse_millis_timestamp);
456
457 if let Some(runner) = parse_race_runner_data(
458 race_id, market_id, rrc, ts_event, ts_init,
459 ) {
460 let selection_id = rrc.id.unwrap_or(0);
461 let mut metadata = Params::new();
462 metadata.insert(
463 "selection_id".to_string(),
464 serde_json::Value::Number(selection_id.into()),
465 );
466 let value: Arc<dyn CustomDataTrait> = Arc::new(runner);
467 let data_type =
468 DataType::new(value.type_name(), Some(metadata), None);
469 let custom = CustomData::new(value, data_type);
470
471 if let Err(e) =
472 data_sender.send(DataEvent::Data(Data::Custom(custom)))
473 {
474 log::warn!("Failed to send race runner data: {e}");
475 }
476 }
477 }
478 }
479
480 if let Some(rpc) = &rc.rpc {
481 let ts_event = rpc.ft.map_or(ts_init, parse_millis_timestamp);
482 let progress =
483 parse_race_progress(race_id, market_id, rpc, ts_event, ts_init);
484 let mut metadata = Params::new();
485 metadata.insert(
486 "race_id".to_string(),
487 serde_json::Value::String(race_id.to_string()),
488 );
489 let value: Arc<dyn CustomDataTrait> = Arc::new(progress);
490 let data_type =
491 DataType::new(value.type_name(), Some(metadata), None);
492 let custom = CustomData::new(value, data_type);
493
494 if let Err(e) =
495 data_sender.send(DataEvent::Data(Data::Custom(custom)))
496 {
497 log::warn!("Failed to send race progress: {e}");
498 }
499 }
500 }
501 }
502 }
503 StreamMessage::CricketChange(ccm) => {
504 if let Some(cricket_changes) = &ccm.cc {
505 let ts_init = parse_millis_timestamp(ccm.pt);
506
507 for cricket_change in cricket_changes {
508 if let Some(cricket) =
509 parse_cricket_match(cricket_change, ts_init, ts_init)
510 {
511 let mut metadata = Params::new();
512 metadata.insert(
513 "event_id".to_string(),
514 serde_json::Value::String(cricket.event_id.clone()),
515 );
516 let value: Arc<dyn CustomDataTrait> = Arc::new(cricket);
517 let data_type =
518 DataType::new(value.type_name(), Some(metadata), None);
519 let custom = CustomData::new(value, data_type);
520
521 if let Err(e) =
522 data_sender.send(DataEvent::Data(Data::Custom(custom)))
523 {
524 log::warn!("Failed to send cricket match: {e}");
525 }
526 }
527 }
528 }
529 }
530 StreamMessage::OrderChange(_) => {}
531 }
532 })
533 }
534}
535
536#[async_trait(?Send)]
537impl DataClient for BetfairDataClient {
538 fn client_id(&self) -> ClientId {
539 self.client_id
540 }
541
542 fn venue(&self) -> Option<Venue> {
543 Some(*BETFAIR_VENUE)
544 }
545
546 fn start(&mut self) -> anyhow::Result<()> {
547 log::info!("Starting Betfair data client: {}", self.client_id);
548 Ok(())
549 }
550
551 fn stop(&mut self) -> anyhow::Result<()> {
552 log::info!("Stopping Betfair data client: {}", self.client_id);
553
554 if let Some(handle) = self.keep_alive_handle.take() {
555 handle.abort();
556 }
557
558 if let Some(handle) = self.reconnect_handle.take() {
559 handle.abort();
560 }
561
562 if let Some(handle) = self.race_fatal_handle.take() {
563 handle.abort();
564 }
565
566 if let Some(handle) = self.cricket_fatal_handle.take() {
567 handle.abort();
568 }
569 self.is_connected.store(false, Ordering::Relaxed);
570 Ok(())
571 }
572
573 fn reset(&mut self) -> anyhow::Result<()> {
574 log::info!("Resetting Betfair data client: {}", self.client_id);
575
576 if let Some(handle) = self.keep_alive_handle.take() {
577 handle.abort();
578 }
579
580 if let Some(handle) = self.reconnect_handle.take() {
581 handle.abort();
582 }
583
584 if let Some(handle) = self.race_fatal_handle.take() {
585 handle.abort();
586 }
587
588 if let Some(handle) = self.cricket_fatal_handle.take() {
589 handle.abort();
590 }
591 self.is_connected.store(false, Ordering::Relaxed);
592 self.stream_client = None;
593 self.race_stream_client = None;
594 self.cricket_stream_client = None;
595 self.provider.store_mut().clear();
596 self.subscribed_market_ids.clear();
597
598 self.instruments.store(AHashMap::new());
599 Ok(())
600 }
601
602 fn dispose(&mut self) -> anyhow::Result<()> {
603 log::debug!("Disposing Betfair data client: {}", self.client_id);
604 self.stop()
605 }
606
607 fn is_connected(&self) -> bool {
608 self.is_connected.load(Ordering::SeqCst)
609 }
610
611 fn is_disconnected(&self) -> bool {
612 !self.is_connected()
613 }
614
615 async fn connect(&mut self) -> anyhow::Result<()> {
616 if self.is_connected() {
617 return Ok(());
618 }
619
620 register_betfair_custom_data();
621
622 self.http_client
623 .connect()
624 .await
625 .map_err(|e| anyhow::anyhow!("{e}"))?;
626
627 self.provider.load_all(None).await?;
628
629 let loaded: Vec<InstrumentAny> = self
630 .provider
631 .store()
632 .list_all()
633 .into_iter()
634 .cloned()
635 .collect();
636
637 self.instruments.rcu(|m| {
638 for inst in &loaded {
639 m.insert(inst.id(), inst.clone());
640 }
641 });
642
643 for inst in &loaded {
644 if let Err(e) = self.data_sender.send(DataEvent::Instrument(inst.clone())) {
645 log::warn!("Failed to send instrument: {e}");
646 }
647 }
648
649 log::debug!("Cached {} instruments for {}", loaded.len(), self.client_id,);
650
651 let session_token = self
652 .http_client
653 .session_token()
654 .await
655 .ok_or_else(|| anyhow::anyhow!("No session token after login"))?;
656
657 let (reconnect_tx, mut reconnect_rx) = tokio::sync::mpsc::unbounded_channel();
658
659 let handler = Self::create_stream_handler(
660 self.data_sender.clone(),
661 Arc::clone(&self.instruments),
662 self.currency,
663 self.provider.min_notional(),
664 reconnect_tx.clone(),
665 );
666
667 let stream_client = BetfairStreamClient::connect(
668 &self.credential,
669 session_token,
670 handler,
671 self.stream_config.clone(),
672 )
673 .await
674 .map_err(|e| anyhow::anyhow!("{e}"))?;
675
676 self.stream_client = Some(Arc::new(stream_client));
677
678 if self.config.subscribe_race_data {
679 let race_config = BetfairStreamConfig {
680 host: BETFAIR_RACE_STREAM_HOST.to_string(),
681 ..self.stream_config.clone()
682 };
683
684 let race_session = self
685 .http_client
686 .session_token()
687 .await
688 .ok_or_else(|| anyhow::anyhow!("No session token for race stream"))?;
689
690 let race_handler = Self::create_stream_handler(
691 self.data_sender.clone(),
692 Arc::clone(&self.instruments),
693 self.currency,
694 self.provider.min_notional(),
695 reconnect_tx.clone(),
696 );
697
698 let (race_fatal_tx, mut race_fatal_rx) = tokio::sync::mpsc::unbounded_channel();
699
700 match BetfairRaceStreamClient::connect(
701 &self.credential,
702 race_session,
703 race_handler,
704 race_config,
705 race_fatal_tx,
706 )
707 .await
708 {
709 Ok(client) => {
710 let race_client = Arc::new(client);
711 self.race_stream_client = Some(Arc::clone(&race_client));
712
713 if let Some(handle) = self.race_fatal_handle.take() {
714 handle.abort();
715 }
716
717 self.race_fatal_handle = Some(get_runtime().spawn(async move {
718 if race_fatal_rx.recv().await.is_some() {
719 log::error!(
720 "Betfair race stream permanently disabled due to fatal error"
721 );
722 race_client.close().await;
723 }
724 }));
725
726 log::debug!("Betfair race stream connected");
727 }
728 Err(e) => {
729 log::warn!("Betfair race stream connect failed: {e}");
730 self.race_stream_client = None;
731 }
732 }
733 }
734
735 if self.config.subscribe_cricket_data {
736 let cricket_config = BetfairStreamConfig {
737 host: BETFAIR_RACE_STREAM_HOST.to_string(),
738 ..self.stream_config.clone()
739 };
740
741 let cricket_session = self
742 .http_client
743 .session_token()
744 .await
745 .ok_or_else(|| anyhow::anyhow!("No session token for cricket stream"))?;
746
747 let cricket_handler = Self::create_stream_handler(
748 self.data_sender.clone(),
749 Arc::clone(&self.instruments),
750 self.currency,
751 self.provider.min_notional(),
752 reconnect_tx.clone(),
753 );
754
755 let (cricket_fatal_tx, mut cricket_fatal_rx) = tokio::sync::mpsc::unbounded_channel();
756
757 match BetfairRaceStreamClient::connect_cricket(
758 &self.credential,
759 cricket_session,
760 cricket_handler,
761 cricket_config,
762 cricket_fatal_tx,
763 )
764 .await
765 {
766 Ok(client) => {
767 let cricket_client = Arc::new(client);
768 self.cricket_stream_client = Some(Arc::clone(&cricket_client));
769
770 if let Some(handle) = self.cricket_fatal_handle.take() {
771 handle.abort();
772 }
773
774 self.cricket_fatal_handle = Some(get_runtime().spawn(async move {
775 if cricket_fatal_rx.recv().await.is_some() {
776 log::error!(
777 "Betfair cricket stream permanently disabled due to fatal error"
778 );
779 cricket_client.close().await;
780 }
781 }));
782
783 log::debug!("Betfair cricket stream connected");
784 }
785 Err(e) => {
786 log::warn!("Betfair cricket stream connect failed: {e}");
787 self.cricket_stream_client = None;
788 }
789 }
790 }
791
792 if let Some(handle) = self.keep_alive_handle.take() {
794 handle.abort();
795 }
796
797 let keep_alive_client = Arc::clone(&self.http_client);
799 let keep_alive_stream = Arc::clone(self.stream_client.as_ref().unwrap());
800 let keep_alive_race_stream = self.race_stream_client.as_ref().map(Arc::clone);
801 let keep_alive_cricket_stream = self.cricket_stream_client.as_ref().map(Arc::clone);
802 let keep_alive_app_key = self.credential.app_key().to_string();
803
804 self.keep_alive_handle = Some(get_runtime().spawn(async move {
805 let interval = tokio::time::Duration::from_secs(KEEP_ALIVE_INTERVAL_SECS);
806 loop {
807 tokio::time::sleep(interval).await;
808
809 match keep_alive_client.keep_alive().await {
810 Ok(()) => {}
811 Err(ref e) if e.is_login_failed() => {
812 log::warn!("Betfair session expired, attempting re-login: {e}");
813 if let Err(e) = keep_alive_client.reconnect().await {
814 log::warn!("Betfair re-login failed: {e}");
815 continue;
816 }
817 }
818 Err(e) => {
819 log::warn!("Betfair keep-alive failed (transient): {e}");
820 continue;
821 }
822 }
823
824 if let Some(token) = keep_alive_client.session_token().await {
825 keep_alive_stream.update_auth(&keep_alive_app_key, token.clone());
826
827 if let Some(ref race_stream) = keep_alive_race_stream {
828 race_stream.update_auth(&keep_alive_app_key, token.clone());
829 }
830
831 if let Some(ref cricket_stream) = keep_alive_cricket_stream {
832 cricket_stream.update_auth(&keep_alive_app_key, token);
833 }
834 }
835 log::debug!("Betfair session keep-alive sent");
836 }
837 }));
838
839 let reconnect_http = Arc::clone(&self.http_client);
841 let reconnect_stream = Arc::clone(self.stream_client.as_ref().unwrap());
842 let reconnect_race_stream = self.race_stream_client.as_ref().map(Arc::clone);
843 let reconnect_cricket_stream = self.cricket_stream_client.as_ref().map(Arc::clone);
844 let reconnect_app_key = self.credential.app_key().to_string();
845
846 self.reconnect_handle = Some(get_runtime().spawn(async move {
847 while reconnect_rx.recv().await.is_some() {
848 log::info!("Handling data stream reconnection");
849
850 match reconnect_http.keep_alive().await {
851 Ok(()) => {}
852 Err(ref e) if e.is_login_failed() => {
853 log::warn!("Session expired on reconnect, attempting re-login: {e}");
854 if let Err(e) = reconnect_http.reconnect().await {
855 log::warn!("Re-login failed on reconnect: {e}");
856 continue;
857 }
858 }
859 Err(e) => {
860 log::warn!("Keep-alive failed on reconnect (transient): {e}");
861 continue;
862 }
863 }
864
865 if let Some(token) = reconnect_http.session_token().await {
866 reconnect_stream.update_auth(&reconnect_app_key, token.clone());
867
868 if let Some(ref race_stream) = reconnect_race_stream {
869 race_stream.update_auth(&reconnect_app_key, token.clone());
870 }
871
872 if let Some(ref cricket_stream) = reconnect_cricket_stream {
873 cricket_stream.update_auth(&reconnect_app_key, token);
874 }
875 }
876 }
877 }));
878
879 self.is_connected.store(true, Ordering::Release);
880
881 log::info!("Betfair data client connected: {}", self.client_id);
882 Ok(())
883 }
884
885 async fn disconnect(&mut self) -> anyhow::Result<()> {
886 if self.is_disconnected() {
887 return Ok(());
888 }
889
890 if let Some(handle) = self.keep_alive_handle.take() {
891 handle.abort();
892 }
893
894 if let Some(handle) = self.reconnect_handle.take() {
895 handle.abort();
896 }
897
898 if let Some(handle) = self.race_fatal_handle.take() {
899 handle.abort();
900 }
901
902 if let Some(handle) = self.cricket_fatal_handle.take() {
903 handle.abort();
904 }
905
906 if let Some(client) = &self.cricket_stream_client {
907 client.close().await;
908 }
909 self.cricket_stream_client = None;
910
911 if let Some(client) = &self.race_stream_client {
912 client.close().await;
913 }
914 self.race_stream_client = None;
915
916 if let Some(client) = &self.stream_client {
917 client.close().await;
918 }
919
920 self.http_client.disconnect().await;
921 self.is_connected.store(false, Ordering::Relaxed);
922 self.subscribed_market_ids.clear();
923
924 log::info!("Betfair data client disconnected: {}", self.client_id);
925 Ok(())
926 }
927
928 fn subscribe_book_deltas(&mut self, cmd: SubscribeBookDeltas) -> anyhow::Result<()> {
929 let instrument_id = cmd.instrument_id;
930 let market_id = extract_market_id(&instrument_id)?;
931
932 if !self.subscribed_market_ids.insert(market_id.clone()) {
933 log::debug!("Book deltas already subscribed for market {market_id}");
934 return Ok(());
935 }
936
937 let stream_client = Arc::clone(
938 self.stream_client
939 .as_ref()
940 .ok_or_else(|| anyhow::anyhow!("Stream client not connected"))?,
941 );
942
943 let all_ids: Vec<String> = self.subscribed_market_ids.iter().cloned().collect();
944
945 let market_filter = StreamMarketFilter {
946 market_ids: Some(all_ids),
947 ..Default::default()
948 };
949
950 let data_filter = MarketDataFilter {
951 fields: Some(vec![
952 MarketDataFilterField::ExAllOffers,
953 MarketDataFilterField::ExTraded,
954 MarketDataFilterField::ExTradedVol,
955 MarketDataFilterField::ExLtp,
956 MarketDataFilterField::ExMarketDef,
957 MarketDataFilterField::SpTraded,
958 MarketDataFilterField::SpProjected,
959 ]),
960 ladder_levels: None,
961 };
962
963 let conflate_ms = self.config.stream_conflate_ms;
964
965 nautilus_common::live::get_runtime().spawn(async move {
966 if let Err(e) = stream_client
967 .subscribe_markets(market_filter, data_filter, None, conflate_ms)
968 .await
969 {
970 log::warn!("Failed to subscribe to market data: {e}");
971 }
972 });
973
974 Ok(())
975 }
976
977 fn unsubscribe_book_deltas(&mut self, cmd: &UnsubscribeBookDeltas) -> anyhow::Result<()> {
978 log::debug!(
979 "Skipping unsubscribe book deltas for Betfair: {}",
980 cmd.instrument_id
981 );
982 Ok(())
983 }
984
985 fn subscribe_trades(&mut self, cmd: SubscribeTrades) -> anyhow::Result<()> {
986 log::debug!(
988 "Trade data included in book subscription for {}",
989 cmd.instrument_id
990 );
991 Ok(())
992 }
993
994 fn unsubscribe_trades(&mut self, cmd: &UnsubscribeTrades) -> anyhow::Result<()> {
995 log::debug!(
996 "Skipping unsubscribe trades for Betfair: {}",
997 cmd.instrument_id
998 );
999 Ok(())
1000 }
1001
1002 fn subscribe_instrument_status(
1003 &mut self,
1004 cmd: SubscribeInstrumentStatus,
1005 ) -> anyhow::Result<()> {
1006 log::debug!(
1008 "Instrument status included in book subscription for {}",
1009 cmd.instrument_id
1010 );
1011 Ok(())
1012 }
1013
1014 fn unsubscribe_instrument_status(
1015 &mut self,
1016 cmd: &UnsubscribeInstrumentStatus,
1017 ) -> anyhow::Result<()> {
1018 log::debug!(
1019 "Skipping unsubscribe instrument status for Betfair: {}",
1020 cmd.instrument_id
1021 );
1022 Ok(())
1023 }
1024
1025 fn subscribe_instrument_close(&mut self, cmd: SubscribeInstrumentClose) -> anyhow::Result<()> {
1026 log::debug!(
1029 "Instrument close included in book subscription for {}",
1030 cmd.instrument_id
1031 );
1032 Ok(())
1033 }
1034
1035 fn unsubscribe_instrument_close(
1036 &mut self,
1037 cmd: &UnsubscribeInstrumentClose,
1038 ) -> anyhow::Result<()> {
1039 log::debug!(
1040 "Skipping unsubscribe instrument close for Betfair: {}",
1041 cmd.instrument_id
1042 );
1043 Ok(())
1044 }
1045
1046 fn unsubscribe(&mut self, _cmd: &UnsubscribeCustomData) -> anyhow::Result<()> {
1047 log::debug!("Skipping unsubscribe custom data for Betfair");
1048 Ok(())
1049 }
1050
1051 fn unsubscribe_instrument(&mut self, cmd: &UnsubscribeInstrument) -> anyhow::Result<()> {
1052 log::debug!(
1053 "Skipping unsubscribe instrument for Betfair: {}",
1054 cmd.instrument_id
1055 );
1056 Ok(())
1057 }
1058
1059 fn unsubscribe_instruments(&mut self, _cmd: &UnsubscribeInstruments) -> anyhow::Result<()> {
1060 log::debug!("Skipping unsubscribe instruments for Betfair");
1061 Ok(())
1062 }
1063
1064 fn unsubscribe_quotes(&mut self, cmd: &UnsubscribeQuotes) -> anyhow::Result<()> {
1065 log::debug!(
1066 "Skipping unsubscribe quotes for Betfair: {}",
1067 cmd.instrument_id
1068 );
1069 Ok(())
1070 }
1071
1072 fn unsubscribe_bars(&mut self, cmd: &UnsubscribeBars) -> anyhow::Result<()> {
1073 log::debug!("Skipping unsubscribe bars for Betfair: {}", cmd.bar_type);
1074 Ok(())
1075 }
1076}
1077
1078#[cfg(test)]
1079mod tests {
1080 use rstest::rstest;
1081
1082 use super::*;
1083 use crate::{common::testing::load_test_json, data_types::BetfairCricketMatch};
1084
1085 #[rstest]
1086 fn test_stream_handler_emits_cricket_match_custom_data() {
1087 let (data_tx, mut data_rx) = tokio::sync::mpsc::unbounded_channel();
1088 let (reconnect_tx, _reconnect_rx) = tokio::sync::mpsc::unbounded_channel();
1089 let handler = BetfairDataClient::create_stream_handler(
1090 data_tx,
1091 Arc::new(AtomicMap::new()),
1092 Currency::GBP(),
1093 None,
1094 reconnect_tx,
1095 );
1096 let data = load_test_json("stream/ccm_single.json");
1097
1098 handler(data.as_bytes());
1099
1100 let event = data_rx.try_recv().expect("expected cricket custom data");
1101 let DataEvent::Data(Data::Custom(custom)) = event else {
1102 panic!("expected cricket custom data event, was {event:?}");
1103 };
1104 let cricket = custom
1105 .data
1106 .as_any()
1107 .downcast_ref::<BetfairCricketMatch>()
1108 .expect("custom data must be BetfairCricketMatch");
1109 let metadata = custom.data_type.metadata().expect("event metadata");
1110
1111 assert_eq!(cricket.event_id, "35741575");
1112 assert_eq!(cricket.market_id, "1.259334639");
1113 assert_eq!(
1114 metadata.get("event_id"),
1115 Some(&serde_json::Value::String("35741575".to_string())),
1116 );
1117 assert!(
1118 data_rx.try_recv().is_err(),
1119 "CCM fixture must emit exactly one event"
1120 );
1121 }
1122}