Skip to main content

nautilus_betfair/
factories.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Factory functions for creating Betfair clients and components.
17
18use 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/// Factory for creating Betfair data clients.
41#[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    /// Creates a new [`BetfairDataClientFactory`] instance.
54    #[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/// Factory for creating Betfair execution clients.
126#[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    /// Creates a new [`BetfairExecutionClientFactory`] instance.
139    #[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}