1use std::{any::Any, fmt::Debug};
19
20use nautilus_common::{config::check_unique_values, factories::ClientConfig};
21#[cfg(test)]
22use nautilus_core::string::secret::REDACTED;
23use nautilus_core::string::secret::SecretString;
24use nautilus_live::book::DEFAULT_BOOK_SNAPSHOT_TIMEOUT_SECS;
25use nautilus_model::{
26 identifiers::AccountId,
27 types::{Currency, Money},
28};
29use rust_decimal::Decimal;
30use serde::{Deserialize, Serialize};
31
32use crate::{
33 common::{
34 credential::{BetfairCredential, CredentialError},
35 parse::parse_betfair_timestamp,
36 },
37 provider::NavigationFilter,
38 stream::config::BetfairStreamConfig,
39};
40
41fn parse_currency(code: &str) -> anyhow::Result<Currency> {
42 code.parse::<Currency>()
43 .map_err(|_| anyhow::anyhow!("Invalid account currency code: {code}"))
44}
45
46fn make_min_notional(value: Option<Decimal>, currency: Currency) -> anyhow::Result<Option<Money>> {
47 value
48 .map(|amount| Money::from_decimal(amount, currency))
49 .transpose()
50 .map_err(Into::into)
51}
52
53fn validate_market_start_time(label: &str, value: &Option<String>) -> anyhow::Result<()> {
54 if let Some(value) = value {
55 parse_betfair_timestamp(value)
56 .map(|_| ())
57 .map_err(|e| anyhow::anyhow!("Invalid {label} '{value}': {e}"))?;
58 }
59
60 Ok(())
61}
62
63fn resolve_credential(
64 username: Option<SecretString>,
65 password: Option<SecretString>,
66 app_key: Option<SecretString>,
67) -> anyhow::Result<BetfairCredential> {
68 match BetfairCredential::resolve(
69 username.map(SecretString::into_inner),
70 password.map(SecretString::into_inner),
71 app_key.map(SecretString::into_inner),
72 ) {
73 Ok(Some(credential)) => Ok(credential),
74 Ok(None) => anyhow::bail!("Missing Betfair credentials in config and environment"),
75 Err(e) => Err(match e {
76 CredentialError::MissingPassword => anyhow::anyhow!(
77 "Invalid Betfair credentials: username provided but password is missing",
78 ),
79 CredentialError::MissingUsername => anyhow::anyhow!(
80 "Invalid Betfair credentials: password or app key provided but username is missing",
81 ),
82 CredentialError::MissingAppKey => {
83 anyhow::anyhow!("Invalid Betfair credentials: app key is missing")
84 }
85 }),
86 }
87}
88
89fn build_stream_config(
90 stream_host: &Option<String>,
91 stream_port: &Option<u16>,
92 stream_heartbeat_secs: Option<u64>,
93 stream_heartbeat_timeout_secs: Option<u64>,
94 stream_reconnect_delay_initial_ms: u64,
95 stream_reconnect_delay_max_ms: u64,
96 stream_use_tls: bool,
97) -> BetfairStreamConfig {
98 let defaults = BetfairStreamConfig::default();
99
100 BetfairStreamConfig {
101 host: stream_host.clone().unwrap_or(defaults.host),
102 port: stream_port.unwrap_or(defaults.port),
103 heartbeat_secs: stream_heartbeat_secs,
104 heartbeat_timeout_secs: stream_heartbeat_timeout_secs,
105 reconnect_delay_initial_ms: stream_reconnect_delay_initial_ms,
106 reconnect_delay_max_ms: stream_reconnect_delay_max_ms,
107 use_tls: stream_use_tls,
108 }
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
113#[serde(default, deny_unknown_fields)]
114#[cfg_attr(
115 feature = "python",
116 pyo3::pyclass(module = "nautilus_trader.adapters.betfair", from_py_object)
117)]
118#[cfg_attr(
119 feature = "python",
120 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.betfair")
121)]
122pub struct BetfairDataClientConfig {
123 #[builder(default = "GBP".to_string())]
125 pub account_currency: String,
126 pub username: Option<SecretString>,
128 pub password: Option<SecretString>,
130 pub app_key: Option<SecretString>,
132 pub proxy_url: Option<SecretString>,
134 #[builder(default = 5)]
136 pub request_rate_per_second: u32,
137 pub default_min_notional: Option<Decimal>,
139 pub event_type_ids: Option<Vec<String>>,
141 pub event_type_names: Option<Vec<String>>,
143 pub event_ids: Option<Vec<String>>,
145 pub country_codes: Option<Vec<String>>,
147 pub market_types: Option<Vec<String>>,
149 pub market_ids: Option<Vec<String>>,
151 pub min_market_start_time: Option<String>,
153 pub max_market_start_time: Option<String>,
155 pub stream_host: Option<String>,
157 pub stream_port: Option<u16>,
159 pub stream_heartbeat_secs: Option<u64>,
161 pub stream_heartbeat_timeout_secs: Option<u64>,
163 #[builder(default = 2_000)]
165 pub stream_reconnect_delay_initial_ms: u64,
166 #[builder(default = 30_000)]
168 pub stream_reconnect_delay_max_ms: u64,
169 #[builder(default = true)]
171 pub stream_use_tls: bool,
172 pub stream_conflate_ms: Option<u64>,
175 #[builder(default = 3)]
177 pub subscription_delay_secs: u64,
178 #[builder(default)]
180 pub subscribe_race_data: bool,
181 #[builder(default)]
183 pub subscribe_cricket_data: bool,
184 #[builder(default = DEFAULT_BOOK_SNAPSHOT_TIMEOUT_SECS)]
187 pub book_snapshot_timeout_secs: u64,
188}
189
190#[cfg(feature = "python")]
191nautilus_core::impl_pyo3_config_getters!(BetfairDataClientConfig {
192 account_currency: String,
193 request_rate_per_second: u32,
194 default_min_notional: Option<Decimal>,
195 event_type_ids: Option<Vec<String>>,
196 event_type_names: Option<Vec<String>>,
197 event_ids: Option<Vec<String>>,
198 country_codes: Option<Vec<String>>,
199 market_types: Option<Vec<String>>,
200 market_ids: Option<Vec<String>>,
201 min_market_start_time: Option<String>,
202 max_market_start_time: Option<String>,
203 stream_host: Option<String>,
204 stream_port: Option<u16>,
205 stream_heartbeat_secs: Option<u64>,
206 stream_heartbeat_timeout_secs: Option<u64>,
207 stream_reconnect_delay_initial_ms: u64,
208 stream_reconnect_delay_max_ms: u64,
209 stream_use_tls: bool,
210 stream_conflate_ms: Option<u64>,
211 subscription_delay_secs: u64,
212 subscribe_race_data: bool,
213 subscribe_cricket_data: bool,
214 book_snapshot_timeout_secs: u64,
215});
216
217impl Default for BetfairDataClientConfig {
218 fn default() -> Self {
219 Self::builder().build()
220 }
221}
222
223impl ClientConfig for BetfairDataClientConfig {
224 fn as_any(&self) -> &dyn Any {
225 self
226 }
227}
228
229impl BetfairDataClientConfig {
230 pub fn credential(&self) -> anyhow::Result<BetfairCredential> {
236 resolve_credential(
237 self.username.clone(),
238 self.password.clone(),
239 self.app_key.clone(),
240 )
241 }
242
243 pub fn currency(&self) -> anyhow::Result<Currency> {
249 parse_currency(&self.account_currency)
250 }
251
252 pub fn min_notional(&self) -> anyhow::Result<Option<Money>> {
258 let currency = self.currency()?;
259 make_min_notional(self.default_min_notional, currency)
260 }
261
262 #[must_use]
264 pub fn navigation_filter(&self) -> NavigationFilter {
265 NavigationFilter {
266 event_type_ids: self.event_type_ids.clone(),
267 event_type_names: self.event_type_names.clone(),
268 event_ids: self.event_ids.clone(),
269 country_codes: self.country_codes.clone(),
270 market_types: self.market_types.clone(),
271 market_ids: self.market_ids.clone(),
272 min_market_start_time: self.min_market_start_time.clone(),
273 max_market_start_time: self.max_market_start_time.clone(),
274 }
275 }
276
277 #[must_use]
279 pub fn stream_config(&self) -> BetfairStreamConfig {
280 build_stream_config(
281 &self.stream_host,
282 &self.stream_port,
283 self.stream_heartbeat_secs,
284 self.stream_heartbeat_timeout_secs,
285 self.stream_reconnect_delay_initial_ms,
286 self.stream_reconnect_delay_max_ms,
287 self.stream_use_tls,
288 )
289 }
290
291 pub fn validate(&self) -> anyhow::Result<()> {
297 if let Some(values) = &self.event_type_ids {
298 check_unique_values("event_type_ids", values)?;
299 }
300
301 if let Some(values) = &self.event_type_names {
302 check_unique_values("event_type_names", values)?;
303 }
304
305 if let Some(values) = &self.event_ids {
306 check_unique_values("event_ids", values)?;
307 }
308
309 if let Some(values) = &self.country_codes {
310 check_unique_values("country_codes", values)?;
311 }
312
313 if let Some(values) = &self.market_types {
314 check_unique_values("market_types", values)?;
315 }
316
317 if let Some(values) = &self.market_ids {
318 check_unique_values("market_ids", values)?;
319 }
320
321 let _ = self.currency()?;
322 validate_market_start_time("min_market_start_time", &self.min_market_start_time)?;
323 validate_market_start_time("max_market_start_time", &self.max_market_start_time)?;
324
325 if self.request_rate_per_second == 0 {
326 anyhow::bail!("request_rate_per_second must be greater than zero");
327 }
328
329 self.stream_config().validate()?;
330
331 Ok(())
332 }
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
337#[serde(default, deny_unknown_fields)]
338#[cfg_attr(
339 feature = "python",
340 pyo3::pyclass(module = "nautilus_trader.adapters.betfair", from_py_object)
341)]
342#[cfg_attr(
343 feature = "python",
344 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.betfair")
345)]
346pub struct BetfairExecutionClientConfig {
347 #[builder(default = AccountId::from("BETFAIR-001"))]
349 pub account_id: AccountId,
350 #[builder(default = "GBP".to_string())]
352 pub account_currency: String,
353 pub username: Option<SecretString>,
355 pub password: Option<SecretString>,
357 pub app_key: Option<SecretString>,
359 pub proxy_url: Option<SecretString>,
361 #[builder(default = 5)]
363 pub request_rate_per_second: u32,
364 #[builder(default = 20)]
366 pub order_request_rate_per_second: u32,
367 pub stream_host: Option<String>,
369 pub stream_port: Option<u16>,
371 pub stream_heartbeat_secs: Option<u64>,
373 pub stream_heartbeat_timeout_secs: Option<u64>,
375 #[builder(default = 2_000)]
377 pub stream_reconnect_delay_initial_ms: u64,
378 #[builder(default = 30_000)]
380 pub stream_reconnect_delay_max_ms: u64,
381 #[builder(default = true)]
383 pub stream_use_tls: bool,
384 pub stream_market_ids_filter: Option<Vec<String>>,
387 #[builder(default)]
389 pub ignore_external_orders: bool,
390 #[builder(default = true)]
392 pub calculate_account_state: bool,
393 #[builder(default = 300)]
395 pub request_account_state_secs: u64,
396 #[builder(default)]
398 pub reconcile_market_ids_only: bool,
399 pub reconcile_market_ids: Option<Vec<String>>,
401 #[builder(default)]
403 pub use_market_version: bool,
404 #[builder(default = 10)]
408 pub stream_gap_recovery_lookback_mins: u64,
409}
410
411#[cfg(feature = "python")]
412nautilus_core::impl_pyo3_config_getters!(BetfairExecutionClientConfig {
413 account_id: AccountId,
414 account_currency: String,
415 request_rate_per_second: u32,
416 order_request_rate_per_second: u32,
417 stream_host: Option<String>,
418 stream_port: Option<u16>,
419 stream_heartbeat_secs: Option<u64>,
420 stream_heartbeat_timeout_secs: Option<u64>,
421 stream_reconnect_delay_initial_ms: u64,
422 stream_reconnect_delay_max_ms: u64,
423 stream_use_tls: bool,
424 stream_market_ids_filter: Option<Vec<String>>,
425 ignore_external_orders: bool,
426 calculate_account_state: bool,
427 request_account_state_secs: u64,
428 reconcile_market_ids_only: bool,
429 reconcile_market_ids: Option<Vec<String>>,
430 use_market_version: bool,
431 stream_gap_recovery_lookback_mins: u64,
432});
433
434impl Default for BetfairExecutionClientConfig {
435 fn default() -> Self {
436 Self::builder().build()
437 }
438}
439
440impl ClientConfig for BetfairExecutionClientConfig {
441 fn as_any(&self) -> &dyn Any {
442 self
443 }
444}
445
446impl BetfairExecutionClientConfig {
447 pub fn credential(&self) -> anyhow::Result<BetfairCredential> {
453 resolve_credential(
454 self.username.clone(),
455 self.password.clone(),
456 self.app_key.clone(),
457 )
458 }
459
460 pub fn currency(&self) -> anyhow::Result<Currency> {
466 parse_currency(&self.account_currency)
467 }
468
469 #[must_use]
471 pub fn stream_config(&self) -> BetfairStreamConfig {
472 build_stream_config(
473 &self.stream_host,
474 &self.stream_port,
475 self.stream_heartbeat_secs,
476 self.stream_heartbeat_timeout_secs,
477 self.stream_reconnect_delay_initial_ms,
478 self.stream_reconnect_delay_max_ms,
479 self.stream_use_tls,
480 )
481 }
482
483 pub fn validate(&self) -> anyhow::Result<()> {
489 if let Some(values) = &self.stream_market_ids_filter {
490 check_unique_values("stream_market_ids_filter", values)?;
491 }
492
493 if let Some(values) = &self.reconcile_market_ids {
494 check_unique_values("reconcile_market_ids", values)?;
495 }
496
497 let _ = self.currency()?;
498
499 if self.request_rate_per_second == 0 {
500 anyhow::bail!("request_rate_per_second must be greater than zero");
501 }
502
503 if self.order_request_rate_per_second == 0 {
504 anyhow::bail!("order_request_rate_per_second must be greater than zero");
505 }
506
507 self.stream_config().validate()?;
508
509 Ok(())
510 }
511}
512
513#[cfg(test)]
514mod tests {
515 use nautilus_common::config::ConfigError;
516 use rstest::rstest;
517
518 use super::*;
519
520 #[rstest]
521 #[case("stream_market_ids_filter")]
522 #[case("reconcile_market_ids")]
523 fn test_execution_config_rejects_duplicate_selections(#[case] field: &str) {
524 let mut input = serde_json::to_value(BetfairExecutionClientConfig::default()).unwrap();
525 input[field] = serde_json::json!(["first", "second", "first"]);
526 let config: BetfairExecutionClientConfig = serde_json::from_value(input).unwrap();
527 let original = serde_json::to_value(&config).unwrap();
528 let result = config.validate();
529
530 assert_eq!(
531 result.unwrap_err().downcast_ref::<ConfigError>(),
532 Some(&ConfigError::duplicate(field, None))
533 );
534 assert_eq!(serde_json::to_value(&config).unwrap(), original);
535 }
536
537 #[rstest]
538 #[case("event_type_ids")]
539 #[case("event_type_names")]
540 #[case("event_ids")]
541 #[case("country_codes")]
542 #[case("market_types")]
543 #[case("market_ids")]
544 fn test_data_config_rejects_duplicate_selections(#[case] field: &str) {
545 let mut input = serde_json::to_value(BetfairDataClientConfig::default()).unwrap();
546 input[field] = serde_json::json!(["first", "second", "first"]);
547 let config: BetfairDataClientConfig = serde_json::from_value(input).unwrap();
548 let original = serde_json::to_value(&config).unwrap();
549 let result = config.validate();
550
551 assert_eq!(
552 result.unwrap_err().downcast_ref::<ConfigError>(),
553 Some(&ConfigError::duplicate(field, None))
554 );
555 assert_eq!(serde_json::to_value(&config).unwrap(), original);
556 }
557
558 #[rstest]
559 fn test_config_debug_redacts_credentials() {
560 let data = BetfairDataClientConfig {
561 username: Some("data-username".into()),
562 password: Some("data-password".into()),
563 app_key: Some("data-app-key".into()),
564 proxy_url: Some("http://user:data-proxy@localhost".into()),
565 ..Default::default()
566 };
567 let execution = BetfairExecutionClientConfig {
568 username: Some("exec-username".into()),
569 password: Some("exec-password".into()),
570 app_key: Some("exec-app-key".into()),
571 proxy_url: Some("http://user:exec-proxy@localhost".into()),
572 ..Default::default()
573 };
574
575 let formatted = format!("{data:?} {execution:?}");
576
577 assert_eq!(formatted.matches(REDACTED).count(), 8);
578
579 for secret in [
580 "data-username",
581 "data-password",
582 "data-app-key",
583 "data-proxy",
584 "exec-username",
585 "exec-password",
586 "exec-app-key",
587 "exec-proxy",
588 ] {
589 assert!(!formatted.contains(secret));
590 }
591 }
592
593 #[rstest]
594 fn test_data_config_default() {
595 let config = BetfairDataClientConfig::default();
596
597 assert_eq!(config.account_currency, "GBP");
598 assert_eq!(config.request_rate_per_second, 5);
599 assert!(config.market_ids.is_none());
600 assert_eq!(config.stream_heartbeat_secs, None);
601 assert_eq!(config.stream_heartbeat_timeout_secs, None);
602 assert!(config.stream_conflate_ms.is_none());
603 assert_eq!(config.subscription_delay_secs, 3);
604 assert!(!config.subscribe_race_data);
605 assert!(!config.subscribe_cricket_data);
606 assert_eq!(config.book_snapshot_timeout_secs, 10);
607 }
608
609 #[rstest]
610 fn test_data_config_navigation_filter() {
611 let config = BetfairDataClientConfig {
612 event_type_names: Some(vec!["Horse Racing".to_string()]),
613 market_ids: Some(vec!["1.234567".to_string()]),
614 ..Default::default()
615 };
616
617 let filter = config.navigation_filter();
618
619 assert_eq!(
620 filter.event_type_names,
621 Some(vec!["Horse Racing".to_string()])
622 );
623 assert_eq!(filter.market_ids, Some(vec!["1.234567".to_string()]));
624 }
625
626 #[rstest]
627 fn test_data_config_stream_config() {
628 let config = BetfairDataClientConfig {
629 stream_host: Some("localhost".to_string()),
630 stream_port: Some(9443),
631 stream_heartbeat_secs: Some(3),
632 stream_heartbeat_timeout_secs: Some(30),
633 stream_reconnect_delay_initial_ms: 500,
634 stream_reconnect_delay_max_ms: 5_000,
635 stream_use_tls: false,
636 ..Default::default()
637 };
638
639 let stream_config = config.stream_config();
640
641 assert_eq!(stream_config.host, "localhost");
642 assert_eq!(stream_config.port, 9443);
643 assert_eq!(stream_config.heartbeat_secs, Some(3));
644 assert_eq!(stream_config.heartbeat_timeout_secs, Some(30));
645 assert_eq!(stream_config.reconnect_delay_initial_ms, 500);
646 assert_eq!(stream_config.reconnect_delay_max_ms, 5_000);
647 assert!(!stream_config.use_tls);
648 }
649
650 #[rstest]
651 fn test_data_config_stream_config_uses_defaults() {
652 let config = BetfairDataClientConfig::default();
653
654 let stream_config = config.stream_config();
655
656 assert_eq!(stream_config.host, BetfairStreamConfig::default().host);
657 assert_eq!(stream_config.port, BetfairStreamConfig::default().port);
658 }
659
660 #[rstest]
661 fn test_data_config_credential_rejects_partial_credentials() {
662 let config = BetfairDataClientConfig {
663 username: Some("testuser".into()),
664 ..Default::default()
665 };
666
667 let result = config.credential();
668
669 assert!(result.is_err());
670 assert!(
671 result
672 .err()
673 .unwrap()
674 .to_string()
675 .contains("password is missing")
676 );
677 }
678
679 #[rstest]
680 fn test_exec_config_default() {
681 let config = BetfairExecutionClientConfig::default();
682 assert_eq!(config.account_id, AccountId::from("BETFAIR-001"));
683 assert_eq!(config.account_currency, "GBP");
684 assert_eq!(config.request_rate_per_second, 5);
685 assert_eq!(config.order_request_rate_per_second, 20);
686 assert_eq!(config.stream_heartbeat_secs, None);
687 assert_eq!(config.stream_heartbeat_timeout_secs, None);
688 assert!(config.stream_market_ids_filter.is_none());
689 assert!(!config.ignore_external_orders);
690 assert!(config.calculate_account_state);
691 assert_eq!(config.request_account_state_secs, 300);
692 assert!(!config.reconcile_market_ids_only);
693 assert!(config.reconcile_market_ids.is_none());
694 assert!(!config.use_market_version);
695 }
696
697 #[rstest]
698 fn test_exec_config_with_market_filter() {
699 let config = BetfairExecutionClientConfig {
700 stream_market_ids_filter: Some(vec!["1.234567".to_string(), "1.890123".to_string()]),
701 ..Default::default()
702 };
703
704 let filter = config.stream_market_ids_filter.as_ref().unwrap();
705 assert_eq!(filter.len(), 2);
706 assert!(filter.contains(&"1.234567".to_string()));
707 }
708
709 #[rstest]
710 fn test_exec_config_external_orders_ignored() {
711 let config = BetfairExecutionClientConfig {
712 ignore_external_orders: true,
713 ..Default::default()
714 };
715
716 assert!(config.ignore_external_orders);
717 }
718
719 #[rstest]
720 fn test_exec_config_account_state_disabled() {
721 let config = BetfairExecutionClientConfig {
722 calculate_account_state: false,
723 ..Default::default()
724 };
725
726 assert!(!config.calculate_account_state);
727 }
728
729 #[rstest]
730 fn test_exec_config_reconcile_market_ids() {
731 let config = BetfairExecutionClientConfig {
732 reconcile_market_ids_only: true,
733 reconcile_market_ids: Some(vec!["1.234567".to_string()]),
734 ..Default::default()
735 };
736
737 assert!(config.reconcile_market_ids_only);
738 assert_eq!(config.reconcile_market_ids.as_ref().unwrap().len(), 1);
739 }
740
741 #[rstest]
742 fn test_exec_config_use_market_version() {
743 let config = BetfairExecutionClientConfig {
744 use_market_version: true,
745 ..Default::default()
746 };
747
748 assert!(config.use_market_version);
749 }
750
751 #[rstest]
752 fn test_exec_config_validate_rejects_zero_order_rate_limit() {
753 let config = BetfairExecutionClientConfig {
754 order_request_rate_per_second: 0,
755 ..Default::default()
756 };
757
758 let result = config.validate();
759 assert!(result.is_err());
760 assert!(
761 result
762 .err()
763 .unwrap()
764 .to_string()
765 .contains("order_request_rate_per_second")
766 );
767 }
768
769 #[rstest]
770 fn test_exec_config_validate_rejects_invalid_currency() {
771 let config = BetfairExecutionClientConfig {
772 account_currency: "INVALID".to_string(),
773 ..Default::default()
774 };
775
776 let result = config.validate();
777
778 assert!(result.is_err());
779 assert!(
780 result
781 .err()
782 .unwrap()
783 .to_string()
784 .contains("Invalid account currency")
785 );
786 }
787
788 #[rstest]
789 fn test_data_config_validate_rejects_bad_market_start_time() {
790 let config = BetfairDataClientConfig {
791 min_market_start_time: Some("not-a-timestamp".to_string()),
792 ..Default::default()
793 };
794
795 let result = config.validate();
796 assert!(result.is_err());
797 assert!(
798 result
799 .err()
800 .unwrap()
801 .to_string()
802 .contains("min_market_start_time")
803 );
804 }
805
806 #[rstest]
807 fn test_data_config_min_notional() {
808 let config = BetfairDataClientConfig {
809 default_min_notional: Some(Decimal::new(2, 0)),
810 ..Default::default()
811 };
812
813 let min_notional = config.min_notional().unwrap();
814 assert_eq!(
815 min_notional,
816 Some(Money::from_decimal(Decimal::new(2, 0), Currency::GBP()).unwrap())
817 );
818 }
819
820 #[rstest]
821 fn test_data_config_toml_minimal() {
822 let config: BetfairDataClientConfig = toml::from_str(
823 r#"
824account_currency = "USD"
825request_rate_per_second = 10
826stream_heartbeat_secs = 3
827stream_heartbeat_timeout_secs = 30
828stream_reconnect_delay_initial_ms = 500
829stream_reconnect_delay_max_ms = 5000
830stream_use_tls = false
831subscription_delay_secs = 1
832subscribe_race_data = true
833subscribe_cricket_data = true
834"#,
835 )
836 .unwrap();
837
838 assert_eq!(config.account_currency, "USD");
839 assert_eq!(config.request_rate_per_second, 10);
840 assert_eq!(config.stream_heartbeat_secs, Some(3));
841 assert!(!config.stream_use_tls);
842 assert!(config.subscribe_race_data);
843 assert!(config.subscribe_cricket_data);
844 }
845
846 #[rstest]
847 fn test_exec_config_toml_empty_uses_defaults() {
848 let config: BetfairExecutionClientConfig = toml::from_str("").unwrap();
849 let expected = BetfairExecutionClientConfig::default();
850 assert_eq!(config.account_id, expected.account_id);
851 assert_eq!(config.account_currency, expected.account_currency);
852 assert_eq!(
853 config.request_rate_per_second,
854 expected.request_rate_per_second,
855 );
856 assert_eq!(
857 config.order_request_rate_per_second,
858 expected.order_request_rate_per_second,
859 );
860 assert_eq!(config.stream_heartbeat_secs, expected.stream_heartbeat_secs);
861 assert_eq!(config.stream_use_tls, expected.stream_use_tls);
862 assert_eq!(
863 config.calculate_account_state,
864 expected.calculate_account_state,
865 );
866 }
867}