nautilus_coinbase_intx/common/
parse.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.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    currencies::CURRENCY_MAP,
21    data::{
22        BarSpecification,
23        bar::{
24            BAR_SPEC_1_DAY_LAST, BAR_SPEC_1_MINUTE_LAST, BAR_SPEC_2_HOUR_LAST,
25            BAR_SPEC_5_MINUTE_LAST, BAR_SPEC_30_MINUTE_LAST,
26        },
27    },
28    enums::{AggressorSide, CurrencyType, LiquiditySide, OrderSide, PositionSide},
29    identifiers::{InstrumentId, Symbol},
30    types::{Currency, Money, Price, Quantity},
31};
32use serde::{Deserialize, Deserializer};
33use ustr::Ustr;
34
35use crate::{
36    common::{
37        consts::COINBASE_INTX_VENUE,
38        enums::{CoinbaseIntxExecType, CoinbaseIntxSide},
39    },
40    websocket::enums::CoinbaseIntxWsChannel,
41};
42
43/// Custom deserializer for strings to u64.
44///
45/// # Errors
46///
47/// Returns a deserialization error if the JSON string is invalid or cannot be parsed to u64.
48pub fn deserialize_optional_string_to_u64<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
49where
50    D: Deserializer<'de>,
51{
52    let s: Option<String> = Option::deserialize(deserializer)?;
53    match s {
54        Some(s) if s.is_empty() => Ok(None),
55        Some(s) => s.parse().map(Some).map_err(serde::de::Error::custom),
56        None => Ok(None),
57    }
58}
59
60/// Returns the currency either from the internal currency map or creates a default crypto.
61/// Returns the currency either from the internal currency map or creates a default crypto.
62///
63/// # Panics
64///
65/// Panics if the internal currency map lock is poisoned.
66pub fn get_currency(code: &str) -> Currency {
67    CURRENCY_MAP
68        .lock()
69        .unwrap()
70        .get(code)
71        .copied()
72        .unwrap_or(Currency::new(code, 8, 0, code, CurrencyType::Crypto))
73}
74
75/// Parses a Posei instrument ID from the given Coinbase `symbol` value.
76#[must_use]
77pub fn parse_instrument_id(symbol: Ustr) -> InstrumentId {
78    InstrumentId::new(Symbol::from_ustr_unchecked(symbol), *COINBASE_INTX_VENUE)
79}
80
81/// Parses a timestamp in milliseconds since epoch into `UnixNanos`.
82///
83/// # Errors
84///
85/// Returns an error if the input string is not a valid unsigned integer.
86pub fn parse_millisecond_timestamp(timestamp: &str) -> anyhow::Result<UnixNanos> {
87    let millis: u64 = timestamp.parse()?;
88    Ok(UnixNanos::from(millis * NANOSECONDS_IN_MILLISECOND))
89}
90
91/// Parses an RFC3339 timestamp string into `UnixNanos`.
92///
93/// # Errors
94///
95/// Returns an error if the input string is not a valid RFC3339 timestamp or is out of range.
96pub fn parse_rfc3339_timestamp(timestamp: &str) -> anyhow::Result<UnixNanos> {
97    let dt = chrono::DateTime::parse_from_rfc3339(timestamp)?;
98    let nanos = dt
99        .timestamp_nanos_opt()
100        .ok_or_else(|| anyhow::anyhow!("RFC3339 timestamp out of range: {timestamp}"))?;
101    Ok(UnixNanos::from(nanos as u64))
102}
103
104/// Parses a string into a `Price`.
105///
106/// # Errors
107///
108/// Returns an error if the string cannot be parsed into a floating point value.
109pub fn parse_price(value: &str) -> anyhow::Result<Price> {
110    Price::from_str(value).map_err(|e| anyhow::anyhow!(e))
111}
112
113/// Parses a string into a `Quantity` with the given precision.
114///
115/// # Errors
116///
117/// Returns an error if the string cannot be parsed into a floating point value.
118pub fn parse_quantity(value: &str, precision: u8) -> anyhow::Result<Quantity> {
119    Quantity::new_checked(value.parse::<f64>()?, precision)
120}
121
122/// Parses a notional string into `Money`, returning `None` if the value is zero.
123///
124/// # Errors
125///
126/// Returns an error if the string cannot be parsed into a floating point value.
127pub fn parse_notional(value: &str, currency: Currency) -> anyhow::Result<Option<Money>> {
128    let parsed = value.trim().parse::<f64>()?;
129    Ok(if parsed == 0.0 {
130        None
131    } else {
132        Some(Money::new(parsed, currency))
133    })
134}
135
136#[must_use]
137pub const fn parse_aggressor_side(side: &Option<CoinbaseIntxSide>) -> AggressorSide {
138    match side {
139        Some(CoinbaseIntxSide::Buy) => nautilus_model::enums::AggressorSide::Buyer,
140        Some(CoinbaseIntxSide::Sell) => nautilus_model::enums::AggressorSide::Seller,
141        None => nautilus_model::enums::AggressorSide::NoAggressor,
142    }
143}
144
145#[must_use]
146pub const fn parse_execution_type(liquidity: &Option<CoinbaseIntxExecType>) -> LiquiditySide {
147    match liquidity {
148        Some(CoinbaseIntxExecType::Maker) => nautilus_model::enums::LiquiditySide::Maker,
149        Some(CoinbaseIntxExecType::Taker) => nautilus_model::enums::LiquiditySide::Taker,
150        _ => nautilus_model::enums::LiquiditySide::NoLiquiditySide,
151    }
152}
153
154#[must_use]
155pub const fn parse_position_side(current_qty: Option<f64>) -> PositionSide {
156    match current_qty {
157        Some(qty) if qty.is_sign_positive() => PositionSide::Long,
158        Some(qty) if qty.is_sign_negative() => PositionSide::Short,
159        _ => PositionSide::Flat,
160    }
161}
162
163#[must_use]
164pub const fn parse_order_side(order_side: &Option<CoinbaseIntxSide>) -> OrderSide {
165    match order_side {
166        Some(CoinbaseIntxSide::Buy) => OrderSide::Buy,
167        Some(CoinbaseIntxSide::Sell) => OrderSide::Sell,
168        None => OrderSide::NoOrderSide,
169    }
170}
171
172/// Converts a `BarSpecification` into the corresponding Coinbase WebSocket channel.
173///
174/// # Errors
175///
176/// Returns an error if the specification is not one of the supported candle intervals.
177pub fn bar_spec_as_coinbase_channel(
178    bar_spec: BarSpecification,
179) -> anyhow::Result<CoinbaseIntxWsChannel> {
180    let channel = match bar_spec {
181        BAR_SPEC_1_MINUTE_LAST => CoinbaseIntxWsChannel::CandlesOneMinute,
182        BAR_SPEC_5_MINUTE_LAST => CoinbaseIntxWsChannel::CandlesFiveMinute,
183        BAR_SPEC_30_MINUTE_LAST => CoinbaseIntxWsChannel::CandlesThirtyMinute,
184        BAR_SPEC_2_HOUR_LAST => CoinbaseIntxWsChannel::CandlesTwoHour,
185        BAR_SPEC_1_DAY_LAST => CoinbaseIntxWsChannel::CandlesOneDay,
186        _ => anyhow::bail!("Invalid `BarSpecification` for channel, was {bar_spec}"),
187    };
188    Ok(channel)
189}
190
191/// Converts a Coinbase WebSocket channel into the corresponding `BarSpecification`.
192///
193/// # Errors
194///
195/// Returns an error if the channel is not one of the supported candle channels.
196pub fn coinbase_channel_as_bar_spec(
197    channel: &CoinbaseIntxWsChannel,
198) -> anyhow::Result<BarSpecification> {
199    let bar_spec = match channel {
200        CoinbaseIntxWsChannel::CandlesOneMinute => BAR_SPEC_1_MINUTE_LAST,
201        CoinbaseIntxWsChannel::CandlesFiveMinute => BAR_SPEC_5_MINUTE_LAST,
202        CoinbaseIntxWsChannel::CandlesThirtyMinute => BAR_SPEC_30_MINUTE_LAST,
203        CoinbaseIntxWsChannel::CandlesTwoHour => BAR_SPEC_2_HOUR_LAST,
204        CoinbaseIntxWsChannel::CandlesOneDay => BAR_SPEC_1_DAY_LAST,
205        _ => anyhow::bail!("Invalid channel for `BarSpecification`, was {channel}"),
206    };
207    Ok(bar_spec)
208}