1use nautilus_core::{UUID4, nanos::UnixNanos};
17use nautilus_model::{
18 enums::{
19 AccountType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, TriggerType,
20 },
21 events::AccountState,
22 identifiers::{AccountId, ClientOrderId, Symbol, TradeId, VenueOrderId},
23 instruments::{CryptoPerpetual, CurrencyPair, any::InstrumentAny},
24 reports::{FillReport, OrderStatusReport, PositionStatusReport},
25 types::{AccountBalance, Currency, Money, Price, Quantity},
26};
27use rust_decimal::Decimal;
28
29use super::models::{
30 CoinbaseIntxBalance, CoinbaseIntxFill, CoinbaseIntxInstrument, CoinbaseIntxOrder,
31 CoinbaseIntxPosition,
32};
33use crate::common::{
34 enums::{CoinbaseIntxInstrumentType, CoinbaseIntxOrderEventType, CoinbaseIntxOrderStatus},
35 parse::{get_currency, parse_instrument_id, parse_notional, parse_position_side},
36};
37
38pub fn parse_spot_instrument(
45 definition: &CoinbaseIntxInstrument,
46 margin_init: Option<Decimal>,
47 margin_maint: Option<Decimal>,
48 maker_fee: Option<Decimal>,
49 taker_fee: Option<Decimal>,
50 ts_init: UnixNanos,
51) -> anyhow::Result<InstrumentAny> {
52 let instrument_id = parse_instrument_id(definition.symbol);
53 let raw_symbol = Symbol::from_ustr_unchecked(definition.symbol);
54
55 let base_currency = get_currency(&definition.base_asset_name);
56 let quote_currency = get_currency(&definition.quote_asset_name);
57
58 let price_increment = Price::from(&definition.quote_increment);
59 let size_increment = Quantity::from(&definition.base_increment);
60 let multiplier = None;
61 let lot_size = None;
62
63 let max_quantity = None;
64 let min_quantity = None;
65 let max_notional = None;
66 let min_notional = parse_notional(&definition.min_notional_value, quote_currency)?;
67 let max_price = None;
68 let min_price = None;
69
70 let instrument = CurrencyPair::new(
71 instrument_id,
72 raw_symbol,
73 base_currency,
74 quote_currency,
75 price_increment.precision,
76 size_increment.precision,
77 price_increment,
78 size_increment,
79 multiplier,
80 lot_size,
81 max_quantity,
82 min_quantity,
83 max_notional,
84 min_notional,
85 max_price,
86 min_price,
87 margin_init,
88 margin_maint,
89 maker_fee,
90 taker_fee,
91 UnixNanos::from(definition.quote.timestamp),
92 ts_init,
93 );
94
95 Ok(InstrumentAny::CurrencyPair(instrument))
96}
97
98pub fn parse_perp_instrument(
105 definition: &CoinbaseIntxInstrument,
106 margin_init: Option<Decimal>,
107 margin_maint: Option<Decimal>,
108 maker_fee: Option<Decimal>,
109 taker_fee: Option<Decimal>,
110 ts_init: UnixNanos,
111) -> anyhow::Result<InstrumentAny> {
112 let instrument_id = parse_instrument_id(definition.symbol);
113 let raw_symbol = Symbol::from_ustr_unchecked(definition.symbol);
114
115 let base_currency = get_currency(&definition.base_asset_name);
116 let quote_currency = get_currency(&definition.quote_asset_name);
117 let settlement_currency = quote_currency;
118
119 let price_increment = Price::from(&definition.quote_increment);
120 let size_increment = Quantity::from(&definition.base_increment);
121
122 let multiplier = Some(Quantity::from(&definition.base_asset_multiplier));
123
124 let lot_size = None;
125 let max_quantity = None;
126 let min_quantity = None;
127 let max_notional = None;
128 let min_notional = parse_notional(&definition.min_notional_value, quote_currency)?;
129 let max_price = None;
130 let min_price = None;
131
132 let is_inverse = false;
133
134 let instrument = CryptoPerpetual::new(
135 instrument_id,
136 raw_symbol,
137 base_currency,
138 quote_currency,
139 settlement_currency,
140 is_inverse,
141 price_increment.precision,
142 size_increment.precision,
143 price_increment,
144 size_increment,
145 multiplier,
146 lot_size,
147 max_quantity,
148 min_quantity,
149 max_notional,
150 min_notional,
151 max_price,
152 min_price,
153 margin_init,
154 margin_maint,
155 maker_fee,
156 taker_fee,
157 UnixNanos::from(definition.quote.timestamp),
158 ts_init,
159 );
160
161 Ok(InstrumentAny::CryptoPerpetual(instrument))
162}
163
164#[must_use]
165pub fn parse_instrument_any(
166 instrument: &CoinbaseIntxInstrument,
167 ts_init: UnixNanos,
168) -> Option<InstrumentAny> {
169 let result = match instrument.instrument_type {
170 CoinbaseIntxInstrumentType::Spot => {
171 parse_spot_instrument(instrument, None, None, None, None, ts_init).map(Some)
172 }
173 CoinbaseIntxInstrumentType::Perp => {
174 parse_perp_instrument(instrument, None, None, None, None, ts_init).map(Some)
175 }
176 CoinbaseIntxInstrumentType::Index => Ok(None), };
178
179 match result {
180 Ok(instrument) => instrument,
181 Err(e) => {
182 tracing::warn!(
183 "Failed to parse instrument {}: {e}",
184 instrument.instrument_id,
185 );
186 None
187 }
188 }
189}
190
191pub fn parse_account_state(
197 coinbase_balances: Vec<CoinbaseIntxBalance>,
198 account_id: AccountId,
199 ts_event: UnixNanos,
200) -> anyhow::Result<AccountState> {
201 let mut balances = Vec::new();
202 for b in coinbase_balances {
203 let currency = Currency::from(b.asset_name);
204 let total = Money::new(b.quantity.parse::<f64>()?, currency);
205 let locked = Money::new(b.hold.parse::<f64>()?, currency);
206 let free = total - locked;
207 let balance = AccountBalance::new(total, locked, free);
208 balances.push(balance);
209 }
210 let margins = vec![]; let account_type = AccountType::Margin;
213 let is_reported = true;
214 let event_id = UUID4::new();
215
216 Ok(AccountState::new(
217 account_id,
218 account_type,
219 balances,
220 margins,
221 is_reported,
222 event_id,
223 ts_event,
224 ts_event,
225 None,
226 ))
227}
228
229fn parse_order_status(coinbase_order: &CoinbaseIntxOrder) -> anyhow::Result<OrderStatus> {
230 let exec_qty = coinbase_order
231 .exec_qty
232 .parse::<Decimal>()
233 .map_err(|e| anyhow::anyhow!("Invalid value for `exec_qty`: {e}"))?;
234
235 let status = match coinbase_order.order_status {
236 CoinbaseIntxOrderStatus::Working => {
237 if exec_qty > Decimal::ZERO {
238 return Ok(OrderStatus::PartiallyFilled);
239 }
240
241 match coinbase_order.event_type {
242 CoinbaseIntxOrderEventType::New => OrderStatus::Accepted,
243 CoinbaseIntxOrderEventType::PendingNew => OrderStatus::Submitted,
244 CoinbaseIntxOrderEventType::PendingCancel => OrderStatus::PendingCancel,
245 CoinbaseIntxOrderEventType::PendingReplace => OrderStatus::PendingUpdate,
246 CoinbaseIntxOrderEventType::StopTriggered => OrderStatus::Triggered,
247 CoinbaseIntxOrderEventType::Replaced => OrderStatus::Accepted,
248 _ => {
250 tracing::debug!(
251 "Unexpected order status and last event type: {:?} {:?}",
252 coinbase_order.order_status,
253 coinbase_order.event_type
254 );
255 OrderStatus::Accepted
256 }
257 }
258 }
259 CoinbaseIntxOrderStatus::Done => {
260 if exec_qty > Decimal::ZERO {
261 return Ok(OrderStatus::Filled);
262 }
263
264 match coinbase_order.event_type {
265 CoinbaseIntxOrderEventType::Canceled => OrderStatus::Canceled,
266 CoinbaseIntxOrderEventType::Rejected => OrderStatus::Rejected,
267 CoinbaseIntxOrderEventType::Expired => OrderStatus::Expired,
268 _ => {
270 tracing::debug!(
271 "Unexpected order status and last event type: {:?} {:?}",
272 coinbase_order.order_status,
273 coinbase_order.event_type
274 );
275 OrderStatus::Canceled
276 }
277 }
278 }
279 };
280 Ok(status)
281}
282
283fn parse_price(value: &str, precision: u8) -> anyhow::Result<Price> {
284 let v = value
285 .parse::<f64>()
286 .map_err(|e| anyhow::anyhow!("Invalid value for `Price`: {e}"))?;
287 Ok(Price::new(v, precision))
288}
289
290fn parse_quantity(value: &str, precision: u8) -> anyhow::Result<Quantity> {
291 let v = value
292 .parse::<f64>()
293 .map_err(|e| anyhow::anyhow!("Invalid value for `Quantity`: {e}"))?;
294 Ok(Quantity::new(v, precision))
295}
296
297pub fn parse_order_status_report(
303 coinbase_order: CoinbaseIntxOrder,
304 account_id: AccountId,
305 price_precision: u8,
306 size_precision: u8,
307 ts_init: UnixNanos,
308) -> anyhow::Result<OrderStatusReport> {
309 let filled_qty = parse_quantity(&coinbase_order.exec_qty, size_precision)?;
310 let order_status: OrderStatus = parse_order_status(&coinbase_order)?;
311
312 let instrument_id = parse_instrument_id(coinbase_order.symbol);
313 let client_order_id = ClientOrderId::new(coinbase_order.client_order_id);
314 let venue_order_id = VenueOrderId::new(coinbase_order.order_id);
315 let order_side: OrderSide = coinbase_order.side.into();
316 let order_type: OrderType = coinbase_order.order_type.into();
317 let time_in_force: TimeInForce = coinbase_order.tif.into();
318 let quantity = parse_quantity(&coinbase_order.size, size_precision)?;
319 let ts_accepted = UnixNanos::from(coinbase_order.submit_time.unwrap_or_default());
320 let ts_last = UnixNanos::from(coinbase_order.event_time.unwrap_or_default());
321
322 let mut report = OrderStatusReport::new(
323 account_id,
324 instrument_id,
325 Some(client_order_id),
326 venue_order_id,
327 order_side,
328 order_type,
329 time_in_force,
330 order_status,
331 quantity,
332 filled_qty,
333 ts_accepted,
334 ts_init,
335 ts_last,
336 None, );
338
339 if let Some(price) = coinbase_order.price {
340 let price = parse_price(&price, price_precision)?;
341 report = report.with_price(price);
342 }
343
344 if let Some(stop_price) = coinbase_order.stop_price {
345 let stop_price = parse_price(&stop_price, price_precision)?;
346 report = report.with_trigger_price(stop_price);
347 report = report.with_trigger_type(TriggerType::Default); }
349
350 if let Some(expire_time) = coinbase_order.expire_time {
351 report = report.with_expire_time(expire_time.into());
352 }
353
354 if let Some(avg_price) = coinbase_order.avg_price {
355 let avg_px = avg_price
356 .parse::<f64>()
357 .map_err(|e| anyhow::anyhow!("Invalid value for `avg_px`: {e}"))?;
358 report = report.with_avg_px(avg_px);
359 }
360
361 if let Some(text) = coinbase_order.text {
362 report = report.with_cancel_reason(text);
363 }
364
365 report = report.with_post_only(coinbase_order.post_only);
366 report = report.with_reduce_only(coinbase_order.close_only);
367
368 Ok(report)
369}
370
371pub fn parse_fill_report(
377 coinbase_fill: CoinbaseIntxFill,
378 account_id: AccountId,
379 price_precision: u8,
380 size_precision: u8,
381 ts_init: UnixNanos,
382) -> anyhow::Result<FillReport> {
383 let instrument_id = parse_instrument_id(coinbase_fill.symbol);
384 let client_order_id = ClientOrderId::new(coinbase_fill.client_order_id);
385 let venue_order_id = VenueOrderId::new(coinbase_fill.order_id);
386 let trade_id = TradeId::from(coinbase_fill.fill_id);
387 let order_side: OrderSide = coinbase_fill.side.into();
388 let last_px = parse_price(&coinbase_fill.fill_price, price_precision)?;
389 let last_qty = parse_quantity(&coinbase_fill.fill_qty, size_precision)?;
390 let commission = Money::from(&format!(
391 "{} {}",
392 coinbase_fill.fee, coinbase_fill.fee_asset
393 ));
394 let liquidity = LiquiditySide::Maker; let ts_event = UnixNanos::from(coinbase_fill.event_time);
396
397 Ok(FillReport::new(
398 account_id,
399 instrument_id,
400 venue_order_id,
401 trade_id,
402 order_side,
403 last_qty,
404 last_px,
405 commission,
406 liquidity,
407 Some(client_order_id),
408 None, ts_event,
410 ts_init,
411 None, ))
413}
414
415pub fn parse_position_status_report(
421 coinbase_position: CoinbaseIntxPosition,
422 account_id: AccountId,
423 size_precision: u8,
424 ts_init: UnixNanos,
425) -> anyhow::Result<PositionStatusReport> {
426 let instrument_id = parse_instrument_id(coinbase_position.symbol);
427 let net_size = coinbase_position
428 .net_size
429 .parse::<f64>()
430 .map_err(|e| anyhow::anyhow!("Invalid value for `net_size`: {e}"))?;
431 let position_side = parse_position_side(Some(net_size)).as_specified();
432 let quantity = Quantity::new(net_size.abs(), size_precision);
433
434 Ok(PositionStatusReport::new(
435 account_id,
436 instrument_id,
437 position_side,
438 quantity,
439 None, ts_init,
441 ts_init,
442 None, ))
444}
445
446#[cfg(test)]
450mod tests {
451 use nautilus_model::types::Money;
452 use rstest::rstest;
453
454 use super::*;
455 use crate::common::testing::load_test_json;
456
457 #[rstest]
458 fn test_parse_spot_instrument() {
459 let json_data = load_test_json("http_get_instruments_BTC-USDC.json");
460 let parsed: CoinbaseIntxInstrument = serde_json::from_str(&json_data).unwrap();
461
462 let ts_init = UnixNanos::default();
463 let instrument = parse_spot_instrument(&parsed, None, None, None, None, ts_init).unwrap();
464
465 if let InstrumentAny::CurrencyPair(pair) = instrument {
466 assert_eq!(pair.id.to_string(), "BTC-USDC.COINBASE_INTX");
467 assert_eq!(pair.raw_symbol.to_string(), "BTC-USDC");
468 assert_eq!(pair.base_currency.to_string(), "BTC");
469 assert_eq!(pair.quote_currency.to_string(), "USDC");
470 assert_eq!(pair.price_increment.to_string(), "0.01");
471 assert_eq!(pair.size_increment.to_string(), "0.00001");
472 assert_eq!(
473 pair.min_notional,
474 Some(Money::new(10.0, pair.quote_currency))
475 );
476 assert_eq!(pair.ts_event, UnixNanos::from(parsed.quote.timestamp));
477 assert_eq!(pair.ts_init, ts_init);
478 assert_eq!(pair.lot_size, None);
479 assert_eq!(pair.max_quantity, None);
480 assert_eq!(pair.min_quantity, None);
481 assert_eq!(pair.max_notional, None);
482 assert_eq!(pair.max_price, None);
483 assert_eq!(pair.min_price, None);
484 assert_eq!(pair.margin_init, Decimal::ZERO);
485 assert_eq!(pair.margin_maint, Decimal::ZERO);
486 assert_eq!(pair.maker_fee, Decimal::ZERO);
487 assert_eq!(pair.taker_fee, Decimal::ZERO);
488 } else {
489 panic!("Expected `CurrencyPair` variant");
490 }
491 }
492
493 #[rstest]
494 fn test_parse_perp_instrument() {
495 let json_data = load_test_json("http_get_instruments_BTC-PERP.json");
496 let parsed: CoinbaseIntxInstrument = serde_json::from_str(&json_data).unwrap();
497
498 let ts_init = UnixNanos::default();
499 let instrument = parse_perp_instrument(&parsed, None, None, None, None, ts_init).unwrap();
500
501 if let InstrumentAny::CryptoPerpetual(perp) = instrument {
502 assert_eq!(perp.id.to_string(), "BTC-PERP.COINBASE_INTX");
503 assert_eq!(perp.raw_symbol.to_string(), "BTC-PERP");
504 assert_eq!(perp.base_currency.to_string(), "BTC");
505 assert_eq!(perp.quote_currency.to_string(), "USDC");
506 assert_eq!(perp.settlement_currency.to_string(), "USDC");
507 assert!(!perp.is_inverse);
508 assert_eq!(perp.price_increment.to_string(), "0.1");
509 assert_eq!(perp.size_increment.to_string(), "0.0001");
510 assert_eq!(perp.multiplier.to_string(), "1.0");
511 assert_eq!(
512 perp.min_notional,
513 Some(Money::new(10.0, perp.quote_currency))
514 );
515 assert_eq!(perp.ts_event, UnixNanos::from(parsed.quote.timestamp));
516 assert_eq!(perp.ts_init, ts_init);
517 assert_eq!(perp.lot_size, Quantity::from(1));
518 assert_eq!(perp.max_quantity, None);
519 assert_eq!(perp.min_quantity, None);
520 assert_eq!(perp.max_notional, None);
521 assert_eq!(perp.max_price, None);
522 assert_eq!(perp.min_price, None);
523 assert_eq!(perp.margin_init, Decimal::ZERO);
524 assert_eq!(perp.margin_maint, Decimal::ZERO);
525 assert_eq!(perp.maker_fee, Decimal::ZERO);
526 assert_eq!(perp.taker_fee, Decimal::ZERO);
527 } else {
528 panic!("Expected `CryptoPerpetual` variant");
529 }
530 }
531}