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