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
98 .proxy_url
99 .as_ref()
100 .map(|value| value.expose_secret().to_owned()),
101 Some(betfair_config.request_rate_per_second),
102 None,
103 )?;
104
105 let client = BetfairDataClient::new(
106 ClientId::from(name),
107 http_client,
108 credential,
109 stream_config,
110 betfair_config,
111 nav_filter,
112 currency,
113 min_notional,
114 );
115
116 Ok(Box::new(client))
117 }
118
119 fn name(&self) -> &'static str {
120 BETFAIR
121 }
122
123 fn config_type(&self) -> &'static str {
124 stringify!(BetfairDataClientConfig)
125 }
126}
127
128#[derive(Debug, Clone)]
130#[cfg_attr(
131 feature = "python",
132 pyo3::pyclass(module = "nautilus_trader.adapters.betfair", from_py_object)
133)]
134#[cfg_attr(
135 feature = "python",
136 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.betfair")
137)]
138pub struct BetfairExecutionClientFactory;
139
140impl BetfairExecutionClientFactory {
141 #[must_use]
143 pub const fn new() -> Self {
144 Self
145 }
146}
147
148impl Default for BetfairExecutionClientFactory {
149 fn default() -> Self {
150 Self::new()
151 }
152}
153
154impl ExecutionClientFactory for BetfairExecutionClientFactory {
155 fn create(
156 &self,
157 trader_id: TraderId,
158 name: &str,
159 config: &dyn ClientConfig,
160 cache: CacheView,
161 _clock: Rc<RefCell<dyn Clock>>,
162 ) -> anyhow::Result<Box<dyn ExecutionClient>> {
163 let betfair_config = config
164 .as_any()
165 .downcast_ref::<BetfairExecutionClientConfig>()
166 .ok_or_else(|| {
167 anyhow::anyhow!(
168 "Invalid config type for BetfairExecutionClientFactory. Expected BetfairExecutionClientConfig, was {config:?}",
169 )
170 })?
171 .clone();
172
173 betfair_config.validate()?;
174
175 let credential = betfair_config.credential()?;
176 let stream_config = betfair_config.stream_config();
177 let currency = betfair_config.currency()?;
178
179 let http_client = BetfairHttpClient::new(
180 credential.clone(),
181 None,
182 None,
183 None,
184 betfair_config
185 .proxy_url
186 .as_ref()
187 .map(|value| value.expose_secret().to_owned()),
188 Some(betfair_config.request_rate_per_second),
189 Some(betfair_config.order_request_rate_per_second),
190 )?;
191
192 let core = ExecutionClientCore::new(
193 trader_id,
194 ClientId::from(name),
195 *BETFAIR_VENUE,
196 OmsType::Netting,
197 betfair_config.account_id,
198 AccountType::Betting,
199 None,
200 cache,
201 );
202
203 let client = BetfairExecutionClient::new(
204 core,
205 http_client,
206 credential,
207 stream_config,
208 betfair_config,
209 currency,
210 );
211
212 Ok(Box::new(client))
213 }
214
215 fn name(&self) -> &'static str {
216 BETFAIR
217 }
218
219 fn config_type(&self) -> &'static str {
220 stringify!(BetfairExecutionClientConfig)
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use std::{cell::RefCell, rc::Rc};
227
228 use nautilus_common::{
229 cache::Cache,
230 clock::VirtualClock,
231 factories::{ClientConfig, DataClientFactory, ExecutionClientFactory},
232 live::runner::set_data_event_sender,
233 };
234 use rstest::rstest;
235
236 use super::*;
237 use crate::config::{BetfairDataClientConfig, BetfairExecutionClientConfig};
238
239 fn data_config() -> BetfairDataClientConfig {
240 BetfairDataClientConfig {
241 username: Some("testuser".into()),
242 password: Some("testpass".into()),
243 app_key: Some("testappkey".into()),
244 ..Default::default()
245 }
246 }
247
248 fn exec_config() -> BetfairExecutionClientConfig {
249 BetfairExecutionClientConfig {
250 username: Some("testuser".into()),
251 password: Some("testpass".into()),
252 app_key: Some("testappkey".into()),
253 ..Default::default()
254 }
255 }
256
257 #[rstest]
258 fn test_betfair_data_client_factory_creation() {
259 let factory = BetfairDataClientFactory::new();
260 assert_eq!(factory.name(), BETFAIR);
261 assert_eq!(factory.config_type(), "BetfairDataClientConfig");
262 }
263
264 #[rstest]
265 fn test_betfair_execution_client_factory_creation() {
266 let factory = BetfairExecutionClientFactory::new();
267 assert_eq!(factory.name(), BETFAIR);
268 assert_eq!(factory.config_type(), "BetfairExecutionClientConfig");
269 }
270
271 #[rstest]
272 fn test_betfair_data_config_implements_client_config() {
273 let config = BetfairDataClientConfig::default();
274 let boxed_config: Box<dyn ClientConfig> = Box::new(config);
275 let downcasted = boxed_config
276 .as_any()
277 .downcast_ref::<BetfairDataClientConfig>();
278 assert!(downcasted.is_some());
279 }
280
281 #[rstest]
282 fn test_betfair_exec_config_implements_client_config() {
283 let config = BetfairExecutionClientConfig::default();
284 let boxed_config: Box<dyn ClientConfig> = Box::new(config);
285 let downcasted = boxed_config
286 .as_any()
287 .downcast_ref::<BetfairExecutionClientConfig>();
288 assert!(downcasted.is_some());
289 }
290
291 #[rstest]
292 fn test_betfair_data_client_factory_creates_client() {
293 let factory = BetfairDataClientFactory::new();
294 let config = data_config();
295 let cache = Rc::new(RefCell::new(Cache::default()));
296 let clock = Rc::new(RefCell::new(VirtualClock::new()));
297 let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
298 set_data_event_sender(tx);
299
300 let result = factory.create(BETFAIR, &config, cache.into(), clock);
301 assert!(result.is_ok());
302
303 let client = result.unwrap();
304 assert_eq!(client.client_id(), ClientId::from(BETFAIR));
305 }
306
307 #[rstest]
308 fn test_betfair_execution_client_factory_creates_client() {
309 let factory = BetfairExecutionClientFactory::new();
310 let config = exec_config();
311 let cache = Rc::new(RefCell::new(Cache::default()));
312
313 let result = factory.create(
314 TraderId::from("TRADER-001"),
315 BETFAIR,
316 &config,
317 cache.into(),
318 Rc::new(RefCell::new(VirtualClock::new())),
319 );
320 assert!(result.is_ok());
321
322 let client = result.unwrap();
323 assert_eq!(client.client_id(), ClientId::from(BETFAIR));
324 }
325
326 #[rstest]
327 fn test_betfair_execution_client_factory_rejects_wrong_config_type() {
328 let factory = BetfairExecutionClientFactory::new();
329 let wrong_config = data_config();
330 let cache = Rc::new(RefCell::new(Cache::default()));
331
332 let result = factory.create(
333 TraderId::from("TRADER-001"),
334 BETFAIR,
335 &wrong_config,
336 cache.into(),
337 Rc::new(RefCell::new(VirtualClock::new())),
338 );
339 assert!(result.is_err());
340 assert!(
341 result
342 .err()
343 .unwrap()
344 .to_string()
345 .contains("Invalid config type")
346 );
347 }
348
349 #[rstest]
350 fn test_betfair_data_client_factory_rejects_missing_credentials() {
351 let factory = BetfairDataClientFactory::new();
352 let config = BetfairDataClientConfig {
353 username: Some("testuser".into()),
354 ..Default::default()
355 };
356 let cache = Rc::new(RefCell::new(Cache::default()));
357 let clock = Rc::new(RefCell::new(VirtualClock::new()));
358
359 let result = factory.create(BETFAIR, &config, cache.into(), clock);
360 assert!(result.is_err());
361 assert!(
362 result
363 .err()
364 .unwrap()
365 .to_string()
366 .contains("password is missing")
367 );
368 }
369}