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
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/// Factory for creating Betfair execution clients.
129#[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    /// Creates a new [`BetfairExecutionClientFactory`] instance.
142    #[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}