1use std::{cell::RefCell, rc::Rc};
19
20use nautilus_common::{
21 cache::CacheView,
22 clients::{DataClient, ExecutionClient},
23 clock::Clock,
24 factories::{ClientConfig, DataClientFactory, ExecutionClientFactory},
25};
26use nautilus_live::ExecutionClientCore;
27use nautilus_model::{
28 enums::{AccountType, OmsType},
29 identifiers::{ClientId, TraderId},
30};
31
32use crate::{
33 common::consts::{BETFAIR, BETFAIR_VENUE},
34 config::{BetfairDataClientConfig, BetfairExecutionClientConfig},
35 data::BetfairDataClient,
36 execution::BetfairExecutionClient,
37 http::client::BetfairHttpClient,
38};
39
40#[derive(Debug, Clone)]
42#[cfg_attr(
43 feature = "python",
44 pyo3::pyclass(module = "nautilus_trader.adapters.betfair", from_py_object)
45)]
46#[cfg_attr(
47 feature = "python",
48 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.betfair")
49)]
50pub struct BetfairDataClientFactory;
51
52impl BetfairDataClientFactory {
53 #[must_use]
55 pub const fn new() -> Self {
56 Self
57 }
58}
59
60impl Default for BetfairDataClientFactory {
61 fn default() -> Self {
62 Self::new()
63 }
64}
65
66impl DataClientFactory for BetfairDataClientFactory {
67 fn create(
68 &self,
69 name: &str,
70 config: &dyn ClientConfig,
71 _cache: CacheView,
72 _clock: Rc<RefCell<dyn Clock>>,
73 ) -> anyhow::Result<Box<dyn DataClient>> {
74 let betfair_config = config
75 .as_any()
76 .downcast_ref::<BetfairDataClientConfig>()
77 .ok_or_else(|| {
78 anyhow::anyhow!(
79 "Invalid config type for BetfairDataClientFactory. Expected BetfairDataClientConfig, was {config:?}",
80 )
81 })?
82 .clone();
83
84 betfair_config.validate()?;
85
86 let credential = betfair_config.credential()?;
87 let stream_config = betfair_config.stream_config();
88 let nav_filter = betfair_config.navigation_filter();
89 let currency = betfair_config.currency()?;
90 let min_notional = betfair_config.min_notional()?;
91
92 let http_client = BetfairHttpClient::new(
93 credential.clone(),
94 None,
95 None,
96 None,
97 betfair_config.proxy_url.clone(),
98 Some(betfair_config.request_rate_per_second),
99 None,
100 )?;
101
102 let client = BetfairDataClient::new(
103 ClientId::from(name),
104 http_client,
105 credential,
106 stream_config,
107 betfair_config,
108 nav_filter,
109 currency,
110 min_notional,
111 );
112
113 Ok(Box::new(client))
114 }
115
116 fn name(&self) -> &'static str {
117 BETFAIR
118 }
119
120 fn config_type(&self) -> &'static str {
121 stringify!(BetfairDataClientConfig)
122 }
123}
124
125#[derive(Debug, Clone)]
127#[cfg_attr(
128 feature = "python",
129 pyo3::pyclass(module = "nautilus_trader.adapters.betfair", from_py_object)
130)]
131#[cfg_attr(
132 feature = "python",
133 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.betfair")
134)]
135pub struct BetfairExecutionClientFactory;
136
137impl BetfairExecutionClientFactory {
138 #[must_use]
140 pub const fn new() -> Self {
141 Self
142 }
143}
144
145impl Default for BetfairExecutionClientFactory {
146 fn default() -> Self {
147 Self::new()
148 }
149}
150
151impl ExecutionClientFactory for BetfairExecutionClientFactory {
152 fn create(
153 &self,
154 trader_id: TraderId,
155 name: &str,
156 config: &dyn ClientConfig,
157 cache: CacheView,
158 ) -> anyhow::Result<Box<dyn ExecutionClient>> {
159 let betfair_config = config
160 .as_any()
161 .downcast_ref::<BetfairExecutionClientConfig>()
162 .ok_or_else(|| {
163 anyhow::anyhow!(
164 "Invalid config type for BetfairExecutionClientFactory. Expected BetfairExecutionClientConfig, was {config:?}",
165 )
166 })?
167 .clone();
168
169 betfair_config.validate()?;
170
171 let credential = betfair_config.credential()?;
172 let stream_config = betfair_config.stream_config();
173 let currency = betfair_config.currency()?;
174
175 let http_client = BetfairHttpClient::new(
176 credential.clone(),
177 None,
178 None,
179 None,
180 betfair_config.proxy_url.clone(),
181 Some(betfair_config.request_rate_per_second),
182 Some(betfair_config.order_request_rate_per_second),
183 )?;
184
185 let core = ExecutionClientCore::new(
186 trader_id,
187 ClientId::from(name),
188 *BETFAIR_VENUE,
189 OmsType::Netting,
190 betfair_config.account_id,
191 AccountType::Betting,
192 None,
193 cache,
194 );
195
196 let client = BetfairExecutionClient::new(
197 core,
198 http_client,
199 credential,
200 stream_config,
201 betfair_config,
202 currency,
203 );
204
205 Ok(Box::new(client))
206 }
207
208 fn name(&self) -> &'static str {
209 BETFAIR
210 }
211
212 fn config_type(&self) -> &'static str {
213 stringify!(BetfairExecutionClientConfig)
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use std::{cell::RefCell, rc::Rc};
220
221 use nautilus_common::{
222 cache::Cache,
223 clock::TestClock,
224 factories::{ClientConfig, DataClientFactory, ExecutionClientFactory},
225 live::runner::set_data_event_sender,
226 };
227 use rstest::rstest;
228
229 use super::*;
230 use crate::config::{BetfairDataClientConfig, BetfairExecutionClientConfig};
231
232 fn data_config() -> BetfairDataClientConfig {
233 BetfairDataClientConfig {
234 username: Some("testuser".to_string()),
235 password: Some("testpass".to_string()),
236 app_key: Some("testappkey".to_string()),
237 ..Default::default()
238 }
239 }
240
241 fn exec_config() -> BetfairExecutionClientConfig {
242 BetfairExecutionClientConfig {
243 username: Some("testuser".to_string()),
244 password: Some("testpass".to_string()),
245 app_key: Some("testappkey".to_string()),
246 ..Default::default()
247 }
248 }
249
250 #[rstest]
251 fn test_betfair_data_client_factory_creation() {
252 let factory = BetfairDataClientFactory::new();
253 assert_eq!(factory.name(), BETFAIR);
254 assert_eq!(factory.config_type(), "BetfairDataClientConfig");
255 }
256
257 #[rstest]
258 fn test_betfair_execution_client_factory_creation() {
259 let factory = BetfairExecutionClientFactory::new();
260 assert_eq!(factory.name(), BETFAIR);
261 assert_eq!(factory.config_type(), "BetfairExecutionClientConfig");
262 }
263
264 #[rstest]
265 fn test_betfair_data_config_implements_client_config() {
266 let config = BetfairDataClientConfig::default();
267 let boxed_config: Box<dyn ClientConfig> = Box::new(config);
268 let downcasted = boxed_config
269 .as_any()
270 .downcast_ref::<BetfairDataClientConfig>();
271 assert!(downcasted.is_some());
272 }
273
274 #[rstest]
275 fn test_betfair_exec_config_implements_client_config() {
276 let config = BetfairExecutionClientConfig::default();
277 let boxed_config: Box<dyn ClientConfig> = Box::new(config);
278 let downcasted = boxed_config
279 .as_any()
280 .downcast_ref::<BetfairExecutionClientConfig>();
281 assert!(downcasted.is_some());
282 }
283
284 #[rstest]
285 fn test_betfair_data_client_factory_creates_client() {
286 let factory = BetfairDataClientFactory::new();
287 let config = data_config();
288 let cache = Rc::new(RefCell::new(Cache::default()));
289 let clock = Rc::new(RefCell::new(TestClock::new()));
290 let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
291 set_data_event_sender(tx);
292
293 let result = factory.create(BETFAIR, &config, cache.into(), clock);
294 assert!(result.is_ok());
295
296 let client = result.unwrap();
297 assert_eq!(client.client_id(), ClientId::from(BETFAIR));
298 }
299
300 #[rstest]
301 fn test_betfair_execution_client_factory_creates_client() {
302 let factory = BetfairExecutionClientFactory::new();
303 let config = exec_config();
304 let cache = Rc::new(RefCell::new(Cache::default()));
305
306 let result = factory.create(TraderId::from("TRADER-001"), BETFAIR, &config, cache.into());
307 assert!(result.is_ok());
308
309 let client = result.unwrap();
310 assert_eq!(client.client_id(), ClientId::from(BETFAIR));
311 }
312
313 #[rstest]
314 fn test_betfair_execution_client_factory_rejects_wrong_config_type() {
315 let factory = BetfairExecutionClientFactory::new();
316 let wrong_config = data_config();
317 let cache = Rc::new(RefCell::new(Cache::default()));
318
319 let result = factory.create(
320 TraderId::from("TRADER-001"),
321 BETFAIR,
322 &wrong_config,
323 cache.into(),
324 );
325 assert!(result.is_err());
326 assert!(
327 result
328 .err()
329 .unwrap()
330 .to_string()
331 .contains("Invalid config type")
332 );
333 }
334
335 #[rstest]
336 fn test_betfair_data_client_factory_rejects_missing_credentials() {
337 let factory = BetfairDataClientFactory::new();
338 let config = BetfairDataClientConfig {
339 username: Some("testuser".to_string()),
340 ..Default::default()
341 };
342 let cache = Rc::new(RefCell::new(Cache::default()));
343 let clock = Rc::new(RefCell::new(TestClock::new()));
344
345 let result = factory.create(BETFAIR, &config, cache.into(), clock);
346 assert!(result.is_err());
347 assert!(
348 result
349 .err()
350 .unwrap()
351 .to_string()
352 .contains("password is missing")
353 );
354 }
355}