1use std::{cell::RefCell, rc::Rc};
19
20use nautilus_common::{
21 cache::Cache,
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,
30};
31
32use crate::{
33 common::consts::{BETFAIR, BETFAIR_VENUE},
34 config::{BetfairDataConfig, BetfairExecConfig},
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.core.nautilus_pyo3.betfair", from_py_object)
45)]
46#[cfg_attr(
47 feature = "python",
48 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.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: Rc<RefCell<Cache>>,
72 _clock: Rc<RefCell<dyn Clock>>,
73 ) -> anyhow::Result<Box<dyn DataClient>> {
74 let betfair_config = config
75 .as_any()
76 .downcast_ref::<BetfairDataConfig>()
77 .ok_or_else(|| {
78 anyhow::anyhow!(
79 "Invalid config type for BetfairDataClientFactory. Expected BetfairDataConfig, 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!(BetfairDataConfig)
122 }
123}
124
125#[derive(Debug, Clone)]
127#[cfg_attr(
128 feature = "python",
129 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.betfair", from_py_object)
130)]
131#[cfg_attr(
132 feature = "python",
133 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.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 name: &str,
155 config: &dyn ClientConfig,
156 cache: Rc<RefCell<Cache>>,
157 ) -> anyhow::Result<Box<dyn ExecutionClient>> {
158 let betfair_config = config
159 .as_any()
160 .downcast_ref::<BetfairExecConfig>()
161 .ok_or_else(|| {
162 anyhow::anyhow!(
163 "Invalid config type for BetfairExecutionClientFactory. Expected BetfairExecConfig, was {config:?}",
164 )
165 })?
166 .clone();
167
168 betfair_config.validate()?;
169
170 let credential = betfair_config.credential()?;
171 let stream_config = betfair_config.stream_config();
172 let currency = betfair_config.currency()?;
173
174 let http_client = BetfairHttpClient::new(
175 credential.clone(),
176 None,
177 None,
178 None,
179 betfair_config.proxy_url.clone(),
180 Some(betfair_config.request_rate_per_second),
181 Some(betfair_config.order_request_rate_per_second),
182 )?;
183
184 let core = ExecutionClientCore::new(
185 betfair_config.trader_id,
186 ClientId::from(name),
187 *BETFAIR_VENUE,
188 OmsType::Netting,
189 betfair_config.account_id,
190 AccountType::Betting,
191 None,
192 cache,
193 );
194
195 let client = BetfairExecutionClient::new(
196 core,
197 http_client,
198 credential,
199 stream_config,
200 betfair_config,
201 currency,
202 );
203
204 Ok(Box::new(client))
205 }
206
207 fn name(&self) -> &'static str {
208 BETFAIR
209 }
210
211 fn config_type(&self) -> &'static str {
212 stringify!(BetfairExecConfig)
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use std::{cell::RefCell, rc::Rc};
219
220 use nautilus_common::{
221 cache::Cache,
222 clock::TestClock,
223 factories::{ClientConfig, DataClientFactory, ExecutionClientFactory},
224 live::runner::set_data_event_sender,
225 };
226 use rstest::rstest;
227
228 use super::*;
229 use crate::config::{BetfairDataConfig, BetfairExecConfig};
230
231 fn data_config() -> BetfairDataConfig {
232 BetfairDataConfig {
233 username: Some("testuser".to_string()),
234 password: Some("testpass".to_string()),
235 app_key: Some("testappkey".to_string()),
236 ..Default::default()
237 }
238 }
239
240 fn exec_config() -> BetfairExecConfig {
241 BetfairExecConfig {
242 username: Some("testuser".to_string()),
243 password: Some("testpass".to_string()),
244 app_key: Some("testappkey".to_string()),
245 ..Default::default()
246 }
247 }
248
249 #[rstest]
250 fn test_betfair_data_client_factory_creation() {
251 let factory = BetfairDataClientFactory::new();
252 assert_eq!(factory.name(), "BETFAIR");
253 assert_eq!(factory.config_type(), "BetfairDataConfig");
254 }
255
256 #[rstest]
257 fn test_betfair_execution_client_factory_creation() {
258 let factory = BetfairExecutionClientFactory::new();
259 assert_eq!(factory.name(), "BETFAIR");
260 assert_eq!(factory.config_type(), "BetfairExecConfig");
261 }
262
263 #[rstest]
264 fn test_betfair_data_config_implements_client_config() {
265 let config = BetfairDataConfig::default();
266 let boxed_config: Box<dyn ClientConfig> = Box::new(config);
267 let downcasted = boxed_config.as_any().downcast_ref::<BetfairDataConfig>();
268 assert!(downcasted.is_some());
269 }
270
271 #[rstest]
272 fn test_betfair_exec_config_implements_client_config() {
273 let config = BetfairExecConfig::default();
274 let boxed_config: Box<dyn ClientConfig> = Box::new(config);
275 let downcasted = boxed_config.as_any().downcast_ref::<BetfairExecConfig>();
276 assert!(downcasted.is_some());
277 }
278
279 #[rstest]
280 fn test_betfair_data_client_factory_creates_client() {
281 let factory = BetfairDataClientFactory::new();
282 let config = data_config();
283 let cache = Rc::new(RefCell::new(Cache::default()));
284 let clock = Rc::new(RefCell::new(TestClock::new()));
285 let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
286 set_data_event_sender(tx);
287
288 let result = factory.create("BETFAIR", &config, cache, clock);
289 assert!(result.is_ok());
290
291 let client = result.unwrap();
292 assert_eq!(client.client_id(), ClientId::from("BETFAIR"));
293 }
294
295 #[rstest]
296 fn test_betfair_execution_client_factory_creates_client() {
297 let factory = BetfairExecutionClientFactory::new();
298 let config = exec_config();
299 let cache = Rc::new(RefCell::new(Cache::default()));
300
301 let result = factory.create("BETFAIR", &config, cache);
302 assert!(result.is_ok());
303
304 let client = result.unwrap();
305 assert_eq!(client.client_id(), ClientId::from("BETFAIR"));
306 }
307
308 #[rstest]
309 fn test_betfair_execution_client_factory_rejects_wrong_config_type() {
310 let factory = BetfairExecutionClientFactory::new();
311 let wrong_config = data_config();
312 let cache = Rc::new(RefCell::new(Cache::default()));
313
314 let result = factory.create("BETFAIR", &wrong_config, cache);
315 assert!(result.is_err());
316 assert!(
317 result
318 .err()
319 .unwrap()
320 .to_string()
321 .contains("Invalid config type")
322 );
323 }
324
325 #[rstest]
326 fn test_betfair_data_client_factory_rejects_missing_credentials() {
327 let factory = BetfairDataClientFactory::new();
328 let config = BetfairDataConfig {
329 username: Some("testuser".to_string()),
330 ..Default::default()
331 };
332 let cache = Rc::new(RefCell::new(Cache::default()));
333 let clock = Rc::new(RefCell::new(TestClock::new()));
334
335 let result = factory.create("BETFAIR", &config, cache, clock);
336 assert!(result.is_err());
337 assert!(
338 result
339 .err()
340 .unwrap()
341 .to_string()
342 .contains("password is missing")
343 );
344 }
345}