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