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