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