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 _clock: Rc<RefCell<dyn Clock>>,
147 ) -> anyhow::Result<Box<dyn ExecutionClient>> {
148 let coinbase_config = config
149 .as_any()
150 .downcast_ref::<CoinbaseExecutionClientConfig>()
151 .ok_or_else(|| {
152 anyhow::anyhow!(
153 "Invalid config type for CoinbaseExecutionClientFactory. Expected CoinbaseExecutionClientConfig, was {config:?}",
154 )
155 })?
156 .clone();
157
158 let account_type = coinbase_config.account_type;
159 if !matches!(account_type, AccountType::Cash | AccountType::Margin) {
160 anyhow::bail!(
161 "Unsupported account_type {account_type:?} for Coinbase; expected Cash (spot) or Margin (CFM derivatives)"
162 );
163 }
164
165 let core = ExecutionClientCore::new(
166 trader_id,
167 ClientId::from(name),
168 *COINBASE_VENUE,
169 OmsType::Netting,
170 coinbase_config.account_id,
171 account_type,
172 None,
173 cache,
174 );
175
176 let client = CoinbaseExecutionClient::new(core, coinbase_config)?;
177
178 Ok(Box::new(client))
179 }
180
181 fn name(&self) -> &'static str {
182 COINBASE
183 }
184
185 fn config_type(&self) -> &'static str {
186 "CoinbaseExecutionClientConfig"
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use std::{cell::RefCell, rc::Rc};
193
194 use nautilus_common::{
195 cache::Cache,
196 clock::VirtualClock,
197 factories::{ClientConfig, DataClientFactory},
198 live::runner::set_data_event_sender,
199 messages::DataEvent,
200 };
201 use rstest::rstest;
202
203 use super::*;
204
205 fn setup_test_env() {
206 let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
207 set_data_event_sender(sender);
208 }
209
210 #[rstest]
211 fn test_coinbase_data_client_factory_creation() {
212 let factory = CoinbaseDataClientFactory::new();
213 assert_eq!(factory.name(), COINBASE);
214 assert_eq!(factory.config_type(), "CoinbaseDataClientConfig");
215 }
216
217 #[rstest]
218 fn test_coinbase_exec_client_config_implements_client_config() {
219 let config = CoinbaseExecutionClientConfig::default();
220 let boxed_config: Box<dyn ClientConfig> = Box::new(config);
221 let downcasted = boxed_config
222 .as_any()
223 .downcast_ref::<CoinbaseExecutionClientConfig>();
224 assert!(downcasted.is_some());
225 }
226
227 #[rstest]
228 fn test_coinbase_data_client_config_implements_client_config() {
229 let config = CoinbaseDataClientConfig::default();
230 let boxed_config: Box<dyn ClientConfig> = Box::new(config);
231 let downcasted = boxed_config
232 .as_any()
233 .downcast_ref::<CoinbaseDataClientConfig>();
234 assert!(downcasted.is_some());
235 }
236
237 #[rstest]
238 fn test_coinbase_data_client_factory_creates_client() {
239 setup_test_env();
240
241 let factory = CoinbaseDataClientFactory::new();
242 let config = CoinbaseDataClientConfig::default();
243 let cache = Rc::new(RefCell::new(Cache::default()));
244 let clock = Rc::new(RefCell::new(VirtualClock::new()));
245
246 let result = factory.create("COINBASE-TEST", &config, cache.into(), clock);
247 assert!(result.is_ok());
248
249 let client = result.unwrap();
250 assert_eq!(client.client_id(), ClientId::from("COINBASE-TEST"));
251 }
252
253 #[rstest]
254 fn test_coinbase_data_client_factory_rejects_wrong_config_type() {
255 #[derive(Debug)]
256 struct WrongConfig;
257
258 impl ClientConfig for WrongConfig {
259 fn as_any(&self) -> &dyn std::any::Any {
260 self
261 }
262 }
263
264 let factory = CoinbaseDataClientFactory::new();
265 let cache = Rc::new(RefCell::new(Cache::default()));
266 let clock = Rc::new(RefCell::new(VirtualClock::new()));
267
268 let result = factory.create("COINBASE-TEST", &WrongConfig, cache.into(), clock);
269 let err = match result {
270 Ok(_) => panic!("wrong config type should be rejected"),
271 Err(e) => e,
272 };
273 let msg = err.to_string();
274 assert!(
275 msg.contains("CoinbaseDataClientFactory"),
276 "error should name the factory, was: {msg}"
277 );
278 assert!(
279 msg.contains("CoinbaseDataClientConfig"),
280 "error should name the expected config type, was: {msg}"
281 );
282 }
283
284 fn make_test_exec_config() -> CoinbaseExecutionClientConfig {
285 CoinbaseExecutionClientConfig {
286 api_key: Some("organizations/test-org/apiKeys/test-key".into()),
287 api_secret: Some("test-pem-placeholder".into()),
288 ..CoinbaseExecutionClientConfig::default()
289 }
290 }
291
292 fn setup_exec_test_env() {
293 use nautilus_common::{live::runner::replace_exec_event_sender, messages::ExecutionEvent};
294 let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel::<ExecutionEvent>();
295 replace_exec_event_sender(sender);
296 }
297
298 #[rstest]
299 fn test_coinbase_execution_client_factory_creation() {
300 let factory = CoinbaseExecutionClientFactory::new();
301 assert_eq!(factory.name(), COINBASE);
302 assert_eq!(factory.config_type(), "CoinbaseExecutionClientConfig");
303 }
304
305 #[rstest]
306 fn test_coinbase_execution_client_factory_creates_cash_client() {
307 setup_exec_test_env();
308
309 let factory = CoinbaseExecutionClientFactory::new();
310 let config = make_test_exec_config();
311 let cache = Rc::new(RefCell::new(Cache::default()));
312
313 let client = factory
314 .create(
315 TraderId::from("TRADER-001"),
316 "COINBASE-TEST",
317 &config,
318 cache.into(),
319 Rc::new(RefCell::new(VirtualClock::new())),
320 )
321 .expect("factory should create exec client with valid config");
322
323 assert_eq!(client.client_id(), ClientId::from("COINBASE-TEST"));
324 assert_eq!(client.account_id(), AccountId::from("COINBASE-001"));
325 assert_eq!(client.venue(), *COINBASE_VENUE);
326 assert_eq!(client.oms_type(), OmsType::Netting);
327 }
328
329 #[rstest]
330 fn test_coinbase_execution_client_factory_creates_margin_client() {
331 setup_exec_test_env();
332
333 let factory = CoinbaseExecutionClientFactory::new();
334 let config = CoinbaseExecutionClientConfig {
335 account_type: AccountType::Margin,
336 ..make_test_exec_config()
337 };
338 let cache = Rc::new(RefCell::new(Cache::default()));
339
340 let client = factory
341 .create(
342 TraderId::from("TRADER-001"),
343 "COINBASE-DERIV",
344 &config,
345 cache.into(),
346 Rc::new(RefCell::new(VirtualClock::new())),
347 )
348 .expect("factory should create margin exec client when configured for derivatives");
349
350 assert_eq!(client.client_id(), ClientId::from("COINBASE-DERIV"));
351 assert_eq!(client.account_id(), AccountId::from("COINBASE-001"));
352 assert_eq!(client.venue(), *COINBASE_VENUE);
353 assert_eq!(client.oms_type(), OmsType::Netting);
354 }
355
356 #[rstest]
357 fn test_coinbase_execution_client_factory_rejects_unsupported_account_type() {
358 setup_exec_test_env();
359
360 let factory = CoinbaseExecutionClientFactory::new();
361 let config = CoinbaseExecutionClientConfig {
362 account_type: AccountType::Betting,
363 ..make_test_exec_config()
364 };
365 let cache = Rc::new(RefCell::new(Cache::default()));
366
367 let err = factory
368 .create(
369 TraderId::from("TRADER-001"),
370 "COINBASE-TEST",
371 &config,
372 cache.into(),
373 Rc::new(RefCell::new(VirtualClock::new())),
374 )
375 .err()
376 .expect("unsupported account type must be rejected");
377 let msg = err.to_string();
378 assert!(
379 msg.contains("Unsupported account_type"),
380 "error should mention unsupported account type, was: {msg}"
381 );
382 }
383
384 #[rstest]
385 fn test_coinbase_execution_client_factory_rejects_wrong_config_type() {
386 setup_exec_test_env();
387
388 let factory = CoinbaseExecutionClientFactory::new();
389 let wrong_config = CoinbaseDataClientConfig::default();
390 let cache = Rc::new(RefCell::new(Cache::default()));
391
392 let result = factory.create(
393 TraderId::from("TRADER-001"),
394 "COINBASE-TEST",
395 &wrong_config,
396 cache.into(),
397 Rc::new(RefCell::new(VirtualClock::new())),
398 );
399 let err = match result {
400 Ok(_) => panic!("wrong config type should be rejected"),
401 Err(e) => e,
402 };
403 let msg = err.to_string();
404 assert!(
405 msg.contains("CoinbaseExecutionClientFactory"),
406 "error should name the factory, was: {msg}"
407 );
408 assert!(
409 msg.contains("CoinbaseExecutionClientConfig"),
410 "error should name the expected config type, was: {msg}"
411 );
412 }
413}