1use std::{any::Any, 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::{AccountId, ClientId, TraderId},
30};
31
32use crate::{
33 common::consts::COINBASE_VENUE,
34 config::{CoinbaseDataClientConfig, CoinbaseExecClientConfig},
35 data::CoinbaseDataClient,
36 execution::CoinbaseExecutionClient,
37};
38
39impl ClientConfig for CoinbaseDataClientConfig {
40 fn as_any(&self) -> &dyn Any {
41 self
42 }
43}
44
45impl ClientConfig for CoinbaseExecClientConfig {
46 fn as_any(&self) -> &dyn Any {
47 self
48 }
49}
50
51#[derive(Debug, Clone)]
53#[cfg_attr(
54 feature = "python",
55 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.coinbase", from_py_object)
56)]
57#[cfg_attr(
58 feature = "python",
59 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.coinbase")
60)]
61pub struct CoinbaseDataClientFactory;
62
63impl CoinbaseDataClientFactory {
64 #[must_use]
66 pub const fn new() -> Self {
67 Self
68 }
69}
70
71impl Default for CoinbaseDataClientFactory {
72 fn default() -> Self {
73 Self::new()
74 }
75}
76
77impl DataClientFactory for CoinbaseDataClientFactory {
78 fn create(
79 &self,
80 name: &str,
81 config: &dyn ClientConfig,
82 _cache: Rc<RefCell<Cache>>,
83 _clock: Rc<RefCell<dyn Clock>>,
84 ) -> anyhow::Result<Box<dyn DataClient>> {
85 let coinbase_config = config
86 .as_any()
87 .downcast_ref::<CoinbaseDataClientConfig>()
88 .ok_or_else(|| {
89 anyhow::anyhow!(
90 "Invalid config type for CoinbaseDataClientFactory. Expected CoinbaseDataClientConfig, was {config:?}",
91 )
92 })?
93 .clone();
94
95 let client_id = ClientId::from(name);
96 let client = CoinbaseDataClient::new(client_id, coinbase_config)?;
97 Ok(Box::new(client))
98 }
99
100 fn name(&self) -> &'static str {
101 "COINBASE"
102 }
103
104 fn config_type(&self) -> &'static str {
105 "CoinbaseDataClientConfig"
106 }
107}
108
109#[derive(Debug, Clone)]
119#[cfg_attr(
120 feature = "python",
121 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.coinbase", from_py_object)
122)]
123#[cfg_attr(
124 feature = "python",
125 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.coinbase")
126)]
127pub struct CoinbaseExecutionClientFactory {
128 trader_id: TraderId,
129 account_id: AccountId,
130}
131
132impl CoinbaseExecutionClientFactory {
133 #[must_use]
135 pub const fn new(trader_id: TraderId, account_id: AccountId) -> Self {
136 Self {
137 trader_id,
138 account_id,
139 }
140 }
141}
142
143impl ExecutionClientFactory for CoinbaseExecutionClientFactory {
144 fn create(
145 &self,
146 name: &str,
147 config: &dyn ClientConfig,
148 cache: Rc<RefCell<Cache>>,
149 ) -> anyhow::Result<Box<dyn ExecutionClient>> {
150 let coinbase_config = config
151 .as_any()
152 .downcast_ref::<CoinbaseExecClientConfig>()
153 .ok_or_else(|| {
154 anyhow::anyhow!(
155 "Invalid config type for CoinbaseExecutionClientFactory. Expected CoinbaseExecClientConfig, was {config:?}",
156 )
157 })?
158 .clone();
159
160 let account_type = coinbase_config.account_type;
161 if !matches!(account_type, AccountType::Cash | AccountType::Margin) {
162 anyhow::bail!(
163 "Unsupported account_type {account_type:?} for Coinbase; expected Cash (spot) or Margin (CFM derivatives)"
164 );
165 }
166
167 let core = ExecutionClientCore::new(
168 self.trader_id,
169 ClientId::from(name),
170 *COINBASE_VENUE,
171 OmsType::Netting,
172 self.account_id,
173 account_type,
174 None,
175 cache,
176 );
177
178 let client = CoinbaseExecutionClient::new(core, coinbase_config)?;
179
180 Ok(Box::new(client))
181 }
182
183 fn name(&self) -> &'static str {
184 "COINBASE"
185 }
186
187 fn config_type(&self) -> &'static str {
188 "CoinbaseExecClientConfig"
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use std::{cell::RefCell, rc::Rc};
195
196 use nautilus_common::{
197 cache::Cache,
198 clock::TestClock,
199 factories::{ClientConfig, DataClientFactory},
200 live::runner::set_data_event_sender,
201 messages::DataEvent,
202 };
203 use rstest::rstest;
204
205 use super::*;
206
207 fn setup_test_env() {
208 let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
209 set_data_event_sender(sender);
210 }
211
212 #[rstest]
213 fn test_coinbase_data_client_factory_creation() {
214 let factory = CoinbaseDataClientFactory::new();
215 assert_eq!(factory.name(), "COINBASE");
216 assert_eq!(factory.config_type(), "CoinbaseDataClientConfig");
217 }
218
219 #[rstest]
220 fn test_coinbase_exec_client_config_implements_client_config() {
221 let config = CoinbaseExecClientConfig::default();
222 let boxed_config: Box<dyn ClientConfig> = Box::new(config);
223 let downcasted = boxed_config
224 .as_any()
225 .downcast_ref::<CoinbaseExecClientConfig>();
226 assert!(downcasted.is_some());
227 }
228
229 #[rstest]
230 fn test_coinbase_data_client_config_implements_client_config() {
231 let config = CoinbaseDataClientConfig::default();
232 let boxed_config: Box<dyn ClientConfig> = Box::new(config);
233 let downcasted = boxed_config
234 .as_any()
235 .downcast_ref::<CoinbaseDataClientConfig>();
236 assert!(downcasted.is_some());
237 }
238
239 #[rstest]
240 fn test_coinbase_data_client_factory_creates_client() {
241 setup_test_env();
242
243 let factory = CoinbaseDataClientFactory::new();
244 let config = CoinbaseDataClientConfig::default();
245 let cache = Rc::new(RefCell::new(Cache::default()));
246 let clock = Rc::new(RefCell::new(TestClock::new()));
247
248 let result = factory.create("COINBASE-TEST", &config, cache, clock);
249 assert!(result.is_ok());
250
251 let client = result.unwrap();
252 assert_eq!(client.client_id(), ClientId::from("COINBASE-TEST"));
253 }
254
255 #[rstest]
256 fn test_coinbase_data_client_factory_rejects_wrong_config_type() {
257 #[derive(Debug)]
258 struct WrongConfig;
259
260 impl ClientConfig for WrongConfig {
261 fn as_any(&self) -> &dyn std::any::Any {
262 self
263 }
264 }
265
266 let factory = CoinbaseDataClientFactory::new();
267 let cache = Rc::new(RefCell::new(Cache::default()));
268 let clock = Rc::new(RefCell::new(TestClock::new()));
269
270 let result = factory.create("COINBASE-TEST", &WrongConfig, cache, clock);
271 let err = match result {
272 Ok(_) => panic!("wrong config type should be rejected"),
273 Err(e) => e,
274 };
275 let msg = err.to_string();
276 assert!(
277 msg.contains("CoinbaseDataClientFactory"),
278 "error should name the factory, was: {msg}"
279 );
280 assert!(
281 msg.contains("CoinbaseDataClientConfig"),
282 "error should name the expected config type, was: {msg}"
283 );
284 }
285
286 fn make_test_exec_config() -> CoinbaseExecClientConfig {
287 CoinbaseExecClientConfig {
288 api_key: Some("organizations/test-org/apiKeys/test-key".to_string()),
289 api_secret: Some("test-pem-placeholder".to_string()),
290 ..CoinbaseExecClientConfig::default()
291 }
292 }
293
294 fn setup_exec_test_env() {
295 use nautilus_common::{live::runner::replace_exec_event_sender, messages::ExecutionEvent};
296 let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
297 replace_exec_event_sender(sender);
298 }
299
300 #[rstest]
301 fn test_coinbase_execution_client_factory_creation() {
302 let factory = CoinbaseExecutionClientFactory::new(
303 TraderId::from("TRADER-001"),
304 AccountId::from("COINBASE-001"),
305 );
306 assert_eq!(factory.name(), "COINBASE");
307 assert_eq!(factory.config_type(), "CoinbaseExecClientConfig");
308 }
309
310 #[rstest]
311 fn test_coinbase_execution_client_factory_creates_cash_client() {
312 setup_exec_test_env();
313
314 let factory = CoinbaseExecutionClientFactory::new(
315 TraderId::from("TRADER-001"),
316 AccountId::from("COINBASE-001"),
317 );
318 let config = make_test_exec_config();
319 let cache = Rc::new(RefCell::new(Cache::default()));
320
321 let client = factory
322 .create("COINBASE-TEST", &config, cache)
323 .expect("factory should create exec client with valid config");
324
325 assert_eq!(client.client_id(), ClientId::from("COINBASE-TEST"));
326 assert_eq!(client.account_id(), AccountId::from("COINBASE-001"));
327 assert_eq!(client.venue(), *COINBASE_VENUE);
328 assert_eq!(client.oms_type(), OmsType::Netting);
329 }
330
331 #[rstest]
332 fn test_coinbase_execution_client_factory_creates_margin_client() {
333 setup_exec_test_env();
334
335 let factory = CoinbaseExecutionClientFactory::new(
336 TraderId::from("TRADER-001"),
337 AccountId::from("COINBASE-001"),
338 );
339 let config = CoinbaseExecClientConfig {
340 account_type: AccountType::Margin,
341 ..make_test_exec_config()
342 };
343 let cache = Rc::new(RefCell::new(Cache::default()));
344
345 let client = factory
346 .create("COINBASE-DERIV", &config, cache)
347 .expect("factory should create margin exec client when configured for derivatives");
348
349 assert_eq!(client.client_id(), ClientId::from("COINBASE-DERIV"));
350 assert_eq!(client.account_id(), AccountId::from("COINBASE-001"));
351 assert_eq!(client.venue(), *COINBASE_VENUE);
352 assert_eq!(client.oms_type(), OmsType::Netting);
353 }
354
355 #[rstest]
356 fn test_coinbase_execution_client_factory_rejects_unsupported_account_type() {
357 setup_exec_test_env();
358
359 let factory = CoinbaseExecutionClientFactory::new(
360 TraderId::from("TRADER-001"),
361 AccountId::from("COINBASE-001"),
362 );
363 let config = CoinbaseExecClientConfig {
364 account_type: AccountType::Betting,
365 ..make_test_exec_config()
366 };
367 let cache = Rc::new(RefCell::new(Cache::default()));
368
369 let err = factory
370 .create("COINBASE-TEST", &config, cache)
371 .err()
372 .expect("unsupported account type must be rejected");
373 let msg = err.to_string();
374 assert!(
375 msg.contains("Unsupported account_type"),
376 "error should mention unsupported account type, was: {msg}"
377 );
378 }
379
380 #[rstest]
381 fn test_coinbase_execution_client_factory_rejects_wrong_config_type() {
382 setup_exec_test_env();
383
384 let factory = CoinbaseExecutionClientFactory::new(
385 TraderId::from("TRADER-001"),
386 AccountId::from("COINBASE-001"),
387 );
388 let wrong_config = CoinbaseDataClientConfig::default();
389 let cache = Rc::new(RefCell::new(Cache::default()));
390
391 let result = factory.create("COINBASE-TEST", &wrong_config, cache);
392 let err = match result {
393 Ok(_) => panic!("wrong config type should be rejected"),
394 Err(e) => e,
395 };
396 let msg = err.to_string();
397 assert!(
398 msg.contains("CoinbaseExecutionClientFactory"),
399 "error should name the factory, was: {msg}"
400 );
401 assert!(
402 msg.contains("CoinbaseExecClientConfig"),
403 "error should name the expected config type, was: {msg}"
404 );
405 }
406}