nautilus_coinbase_intx/common/
parse.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use std::str::FromStr;
17
18use nautilus_core::{datetime::NANOSECONDS_IN_MILLISECOND, nanos::UnixNanos};
19use nautilus_model::{
20    data::{
21        BarSpecification,
22        bar::{
23            BAR_SPEC_1_DAY_LAST, BAR_SPEC_1_MINUTE_LAST, BAR_SPEC_2_HOUR_LAST,
24            BAR_SPEC_5_MINUTE_LAST, BAR_SPEC_30_MINUTE_LAST,
25        },
26    },
27    enums::{AggressorSide, LiquiditySide, PositionSide},
28    identifiers::{InstrumentId, Symbol},
29    types::{Currency, Money, Price, Quantity},
30};
31use serde::{Deserialize, Deserializer};
32use ustr::Ustr;
33
34use crate::{
35    common::{
36        consts::COINBASE_INTX_VENUE,
37        enums::{CoinbaseIntxExecType, CoinbaseIntxSide},
38    },
39    websocket::enums::CoinbaseIntxWsChannel,
40};
41
42/// Custom deserializer for strings to u64.
43///
44/// # Errors
45///
46/// Returns a deserialization error if the JSON string is invalid or cannot be parsed to u64.
47pub fn deserialize_optional_string_to_u64<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
48where
49    D: Deserializer<'de>,
50{
51    let s: Option<String> = Option::deserialize(deserializer)?;
52    match s {
53        Some(s) if s.is_empty() => Ok(None),
54        Some(s) => s.parse().map(Some).map_err(serde::de::Error::custom),
55        None => Ok(None),
56    }
57}
58
59/// Returns a currency from the internal map or creates a new crypto currency.
60///
61/// Uses [`Currency::get_or_create_crypto`] to handle unknown currency codes,
62/// which automatically registers newly listed Coinbase International Exchange assets.
63pub fn get_currency(code: &str) -> Currency {
64    Currency::get_or_create_crypto(code)
65}
66
67/// Parses a Nautilus instrument ID from the given Coinbase `symbol` value.
68#[must_use]
69pub fn parse_instrument_id(symbol: Ustr) -> InstrumentId {
70    InstrumentId::new(Symbol::from_ustr_unchecked(symbol), *COINBASE_INTX_VENUE)
71}
72
73/// Parses a timestamp in milliseconds since epoch into `UnixNanos`.
74///
75/// # Errors
76///
77/// Returns an error if the input string is not a valid unsigned integer.
78pub fn parse_millisecond_timestamp(timestamp: &str) -> anyhow::Result<UnixNanos> {
79    let millis: u64 = timestamp.parse()?;
80    Ok(UnixNanos::from(millis * NANOSECONDS_IN_MILLISECOND))
81}
82
83/// Parses an RFC3339 timestamp string into `UnixNanos`.
84///
85/// # Errors
86///
87/// Returns an error if the input string is not a valid RFC3339 timestamp or is out of range.
88pub fn parse_rfc3339_timestamp(timestamp: &str) -> anyhow::Result<UnixNanos> {
89    let dt = chrono::DateTime::parse_from_rfc3339(timestamp)?;
90    let nanos = dt
91        .timestamp_nanos_opt()
92        .ok_or_else(|| anyhow::anyhow!("RFC3339 timestamp out of range: {timestamp}"))?;
93    Ok(UnixNanos::from(nanos as u64))
94}
95
96/// Parses a string into a `Price`.
97///
98/// # Errors
99///
100/// Returns an error if the string cannot be parsed into a floating point value.
101pub fn parse_price(value: &str) -> anyhow::Result<Price> {
102    Price::from_str(value).map_err(|e| anyhow::anyhow!(e))
103}
104
105/// Parses a string into a `Quantity` with the given precision.
106///
107/// # Errors
108///
109/// Returns an error if the string cannot be parsed into a floating point value.
110pub fn parse_quantity(value: &str, precision: u8) -> anyhow::Result<Quantity> {
111    Quantity::new_checked(value.parse::<f64>()?, precision)
112}
113
114/// Parses a notional string into `Money`, returning `None` if the value is zero.
115///
116/// # Errors
117///
118/// Returns an error if the string cannot be parsed into a floating point value.
119pub fn parse_notional(value: &str, currency: Currency) -> anyhow::Result<Option<Money>> {
120    let parsed = value.trim().parse::<f64>()?;
121    Ok(if parsed == 0.0 {
122        None
123    } else {
124        Some(Money::new(parsed, currency))
125    })
126}
127
128#[must_use]
129pub const fn parse_aggressor_side(side: &Option<CoinbaseIntxSide>) -> AggressorSide {
130    match side {
131        Some(CoinbaseIntxSide::Buy) => nautilus_model::enums::AggressorSide::Buyer,
132        Some(CoinbaseIntxSide::Sell) => nautilus_model::enums::AggressorSide::Seller,
133        None => nautilus_model::enums::AggressorSide::NoAggressor,
134    }
135}
136
137#[must_use]
138pub const fn parse_execution_type(liquidity: &Option<CoinbaseIntxExecType>) -> LiquiditySide {
139    match liquidity {
140        Some(CoinbaseIntxExecType::Maker) => nautilus_model::enums::LiquiditySide::Maker,
141        Some(CoinbaseIntxExecType::Taker) => nautilus_model::enums::LiquiditySide::Taker,
142        _ => nautilus_model::enums::LiquiditySide::NoLiquiditySide,
143    }
144}
145
146#[must_use]
147pub const fn parse_position_side(current_qty: Option<f64>) -> PositionSide {
148    match current_qty {
149        Some(qty) if qty.is_sign_positive() => PositionSide::Long,
150        Some(qty) if qty.is_sign_negative() => PositionSide::Short,
151        _ => PositionSide::Flat,
152    }
153}
154
155/// Converts a `BarSpecification` into the corresponding Coinbase WebSocket channel.
156///
157/// # Errors
158///
159/// Returns an error if the specification is not one of the supported candle intervals.
160pub fn bar_spec_as_coinbase_channel(
161    bar_spec: BarSpecification,
162) -> anyhow::Result<CoinbaseIntxWsChannel> {
163    let channel = match bar_spec {
164        BAR_SPEC_1_MINUTE_LAST => CoinbaseIntxWsChannel::CandlesOneMinute,
165        BAR_SPEC_5_MINUTE_LAST => CoinbaseIntxWsChannel::CandlesFiveMinute,
166        BAR_SPEC_30_MINUTE_LAST => CoinbaseIntxWsChannel::CandlesThirtyMinute,
167        BAR_SPEC_2_HOUR_LAST => CoinbaseIntxWsChannel::CandlesTwoHour,
168        BAR_SPEC_1_DAY_LAST => CoinbaseIntxWsChannel::CandlesOneDay,
169        _ => anyhow::bail!("Invalid `BarSpecification` for channel, was {bar_spec}"),
170    };
171    Ok(channel)
172}
173
174/// Converts a Coinbase WebSocket channel into the corresponding `BarSpecification`.
175///
176/// # Errors
177///
178/// Returns an error if the channel is not one of the supported candle channels.
179pub fn coinbase_channel_as_bar_spec(
180    channel: &CoinbaseIntxWsChannel,
181) -> anyhow::Result<BarSpecification> {
182    let bar_spec = match channel {
183        CoinbaseIntxWsChannel::CandlesOneMinute => BAR_SPEC_1_MINUTE_LAST,
184        CoinbaseIntxWsChannel::CandlesFiveMinute => BAR_SPEC_5_MINUTE_LAST,
185        CoinbaseIntxWsChannel::CandlesThirtyMinute => BAR_SPEC_30_MINUTE_LAST,
186        CoinbaseIntxWsChannel::CandlesTwoHour => BAR_SPEC_2_HOUR_LAST,
187        CoinbaseIntxWsChannel::CandlesOneDay => BAR_SPEC_1_DAY_LAST,
188        _ => anyhow::bail!("Invalid channel for `BarSpecification`, was {channel}"),
189    };
190    Ok(bar_spec)
191}